64 lines
5.0 KiB
Python
64 lines
5.0 KiB
Python
"""Read-only portable receipt and frame verification, without archived execution."""
|
|
from pathlib import Path, PurePosixPath
|
|
from datetime import datetime
|
|
import hashlib,json,zipfile
|
|
ROOT=Path(__file__).resolve().parent
|
|
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
|
def need(ok,msg):
|
|
if not ok:raise ValueError(msg)
|
|
def main():
|
|
files=json.loads((ROOT/'files.json').read_bytes())
|
|
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
|
|
manifest=json.loads((ROOT/'manifest.json').read_bytes())
|
|
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
|
|
names=z.namelist()
|
|
need(len(names)==len(set(names)) and set(names)==set(manifest['members']),'exact ZIP membership')
|
|
need(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n for n in names),'safe members')
|
|
for n,p in manifest['members'].items():need(pin(z.read(n))==p,'member hash '+n)
|
|
def obj(n):return json.loads(z.read(n))
|
|
input_names={n[len('native/input/'):]:n for n in names if n.startswith('native/input/')}
|
|
reference=None;frame_count=0;image_count=0
|
|
for run,spec in manifest['runs'].items():
|
|
prefix='native/'+run+'/'
|
|
start=obj(prefix+'started.json');result=obj(prefix+'result.json');process=obj(prefix+'process.json')
|
|
need(all(result[k]==v for k,v in start.items()),run+' start/result relationship')
|
|
need(result['exitCode']==spec['exitCode'],run+' exit code')
|
|
need(isinstance(process['pid'],int) and process['pid']>0,run+' process ID')
|
|
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']),run+' process time order')
|
|
need(result['helper']==pin(z.read(spec['helper'])),run+' launch helper')
|
|
need(result['inputs']==pin(z.read(prefix+'inputs.json')) and result['inputUnchanged'] is True,run+' input receipt')
|
|
inputs=obj(prefix+'inputs.json')
|
|
need(set(inputs)==set(input_names),run+' input inventory')
|
|
for n,p in inputs.items():need(pin(z.read(input_names[n]))==p,run+' input bytes '+n)
|
|
if reference is None:reference=inputs
|
|
need(inputs==reference,run+' all trials identical fixture')
|
|
for stream in ['stdout','stderr']:
|
|
b=z.read(prefix+stream+'.log');need(not b and result[stream]==pin(b),run+' empty stream '+stream)
|
|
build=obj(spec['build'])
|
|
need(result['buildReceipt']==pin(z.read(spec['build'])) and result['buildHead']==build['head'],run+' build receipt')
|
|
need(build['exitCode']==0 and build['inputsUnchanged'] is True,run+' build status')
|
|
need(datetime.fromisoformat(build['start'])<=datetime.fromisoformat(build['finish'])<=datetime.fromisoformat(start['start']),run+' build before launch')
|
|
gui=[a for a in build['artifacts'] if a['cargo']['target']['name']=='arcadia-studio']
|
|
need(len(gui)==1,run+' unique GUI artifact')
|
|
need(result['binary']==result['binaryAfter']==gui[0]['readback'],run+' unchanged GUI pin')
|
|
need(result['command']==[gui[0]['cargo']['executable']],run+' exact launch command')
|
|
images=[]
|
|
for metadata in spec['captures']:
|
|
frame=obj(metadata)
|
|
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(frame['time'])<=datetime.fromisoformat(result['finish']),metadata+' capture interval')
|
|
need(frame['window']['app'].replace('\\','/')=='process:'+result['command'][0].replace('\\','/'),metadata+' window application path')
|
|
for image in frame['images']:
|
|
n=prefix+image['file'];need(n in manifest['members'] and z.read(n).startswith(b'\xff\xd8'),metadata+' JPEG reference')
|
|
need(image['width']>0 and image['height']>0,metadata+' frame dimensions')
|
|
images.append(n)
|
|
actual_images=[n for n in names if n.startswith(prefix) and n.endswith('.jpg')]
|
|
need(len(images)==len(set(images)) and set(images)==set(actual_images),run+' all images referenced once')
|
|
actual_metadata=[n for n in names if n.startswith(prefix) and n.endswith('.json') and 'images' in obj(n)]
|
|
need(set(actual_metadata)==set(spec['captures']) and len(actual_metadata)==len(spec['captures']),run+' all capture metadata')
|
|
frame_count+=len(actual_metadata);image_count+=len(images)
|
|
print(run,'exit',result['exitCode'],'captures',len(actual_metadata),'images',len(images))
|
|
need(frame_count==manifest['captureCount'] and image_count==manifest['imageCount'],'aggregate frame counts')
|
|
print('PASS:',len(names),'raw members;',len(reference),'unchanged fixture files;',frame_count,'capture records;',image_count,'original JPEGs.')
|
|
print('Build source inventories are external. Frames prove recorded states, not action execution or PID attestation. Capture helper was not start-pinned.')
|
|
if __name__=='__main__':main()
|