Files

116 lines
10 KiB
Python

"""Read-only offline verification. Requires the pinned sibling checks/evidence.zip."""
from pathlib import Path,PurePosixPath
from datetime import datetime
import hashlib,io,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 safe(n):return not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n
def members(z,expected):
names=z.namelist();need(len(names)==len(set(names)) and set(names)==set(expected),'exact ZIP membership');need(all(safe(n) for n in names),'safe ZIP members')
def ordered(*times):return list(map(datetime.fromisoformat,times))==sorted(map(datetime.fromisoformat,times))
def main():
files=json.loads((ROOT/'files.json').read_bytes())
need(set(files)=={'evidence.zip','manifest.json','README.md','verify.py','prepare.py'},'outer inventory')
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
m=json.loads((ROOT/'manifest.json').read_bytes())
need(m['checksRelativePath']=='../checks/evidence.zip','fixed sibling evidence location')
checks_bytes=(ROOT/'../checks/evidence.zip').read_bytes()
need(pin(checks_bytes)==m['checksEvidence'],'pinned sibling checks archive')
with zipfile.ZipFile(ROOT/'evidence.zip') as z,zipfile.ZipFile(io.BytesIO(checks_bytes)) as checks:
members(z,m['members'])
for n,p in m['members'].items():need(pin(z.read(n))==p,'native member hash '+n)
def raw(n):return z.read(n)
def obj(n):return json.loads(raw(n))
def directory(prefix):return {n[len(prefix):]:pin(raw(n)) for n in m['members'] if n.startswith(prefix)}
cm=obj('references/checks-manifest.json');members(checks,cm['members'])
def craw(logical):
n=cm['logicalFiles'][logical];b=checks.read(n);need(pin(b)==cm['members'][n],'checks member '+n);return b
def cobj(n):return json.loads(craw(n))
# Source capsules and receipt helper chains are independently checked here.
builds={};artifacts={};pairs=set()
for run in ('footer-windows-build-2','footer-windows-build-3','lifecycle-producer-build-2'):
p='cargo/'+run+'/';r=cobj(p+'result.json');start=cobj(p+'started.json');process=cobj(p+'process.json')
need(all(r[k]==v for k,v in start.items()) and r['exitCode']==0 and r['inputsUnchanged'] is True,run+' build closure')
need(r['command']==process['command'] and ordered(r['start'],process['observed'],r['finish']),run+' build process')
for field,n in [('inputs','inputs.json'),('archive','inputs.zip'),('stdout','stdout.log'),('stderr','stderr.log')]:need(r[field]==pin(craw(p+n)),run+' receipt '+field)
need(r['helper']==pin(craw(cm['cargo'][run]['helper'])),run+' build helper')
inputs=cobj(p+'inputs.json');need(len(inputs)==r['inputCount'] and sum(n.startswith('crates/vendor/iced_widget/') for n in inputs)==71,run+' input inventory')
pair=(r['inputs']['sha256'],r['archive']['sha256'])
if pair not in pairs:
with zipfile.ZipFile(io.BytesIO(craw(p+'inputs.zip'))) as source:
members(source,inputs)
for n,expected in inputs.items():need(pin(source.read(n))==expected,run+' source '+n)
pairs.add(pair)
records=[]
for line in craw(p+'stdout.log').decode('utf-8',errors='replace').splitlines():
try:item=json.loads(line)
except json.JSONDecodeError:continue
if isinstance(item,dict) and item.get('reason')=='compiler-artifact' and item.get('executable'):records.append(item)
need(records==[a['cargo'] for a in r['artifacts']],run+' artifact records')
expected='transition-native-lifecycle' if run.startswith('lifecycle') else 'arcadia-studio'
selected=[a for a in r['artifacts'] if a['cargo']['target']['name']==expected];need(len(selected)==1,run+' unique artifact')
builds[run]=r;artifacts[run]=selected[0]
original=m['originalNativeDirectory'].rstrip('/')+'/'
def mapped(path):
norm=path.replace('\\','/');need(norm.startswith(original),'snapshot path outside captured native root')
rel=norm[len(original):];need(safe(rel),'unsafe snapshot path');return 'native/'+rel
fixtures=directory('fixtures/');snapshots={};reports={};capture_count=image_count=0
need(set(m['runs'])=={'lifecycle-2','lifecycle-3','lifecycle-4','lifecycle-5'},'closed trial membership')
for run,spec in m['runs'].items():
p='native/'+run+'/';r=obj(p+'result.json');start=obj(p+'started.json');process=obj(p+'process.json')
build=builds[spec['guiBuild']];artifact=artifacts[spec['guiBuild']]
need(all(r[k]==v for k,v in start.items()),run+' start/result relationship')
need(r['exitCode']==0 and r['inputUnchanged'] is True and r['binary']==r['binaryAfter']==artifact['readback'],run+' native closure and binary pin')
need(r['buildReceipt']==pin(craw('cargo/'+spec['guiBuild']+'/result.json')) and r['buildHead']==build['head'],run+' GUI build relationship')
need(r['command']==[artifact['cargo']['executable']] and process['pid']>0,run+' native command/PID')
need(ordered(build['finish'],r['start'],process['observed'],r['finish']),run+' native times')
need(r['helper']==pin(raw(spec['helpers']['runner'])) and r['captureHelper']==pin(raw(spec['helpers']['launchCapture'])),run+' launch helper pins')
need(p+'capture-function.js' in m['members'],run+' actual capture-function record')
need(r['inputs']==pin(raw(p+'inputs.json')) and obj(p+'inputs.json')==fixtures,run+' unchanged fixture bytes')
for stream in ('stdout','stderr'):need(not raw(p+stream+'.log') and r[stream]==pin(raw(p+stream+'.log')),run+' empty '+stream)
images=[]
actual_captures=[n for n in m['members'] if n.startswith(p) and n.endswith('.json') and isinstance(obj(n),dict) and 'images' in obj(n)]
need(set(actual_captures)==set(spec['captures']) and len(actual_captures)==len(spec['captures']),run+' exact metadata coverage')
for n in spec['captures']:
frame=obj(n);need(ordered(r['start'],frame['time'],r['finish']),n+' frame timestamp')
need(frame['window']['app'].replace('\\','/')=='process:'+r['command'][0].replace('\\','/'),n+' window executable path')
for image in frame['images']:
name=p+image['file'];b=raw(name)
need(image['width']>0 and image['height']>0 and (b.startswith(b'\xff\xd8') or b.startswith(b'\x89PNG')),n+' image record')
images.append(name)
actual_images={n for n in m['members'] if n.startswith(p) and n.endswith(('.jpg','.png'))}
need(len(images)==len(set(images)) and set(images)==actual_images,run+' all original images')
capture_count+=len(spec['captures']);image_count+=len(images)
actual_snapshots={n.split('/')[3] for n in m['members'] if n.startswith(p+'snapshots/')}
need(actual_snapshots==set(spec['snapshots']),run+' snapshot inventory')
for label,state in spec['snapshots'].items():
q=p+'snapshots/'+label+'/';s=obj(q+'result.json');ss=obj(q+'started.json');report=obj(q+'stdout.log')
checker=artifacts['lifecycle-producer-build-2']
need(all(s[k]==v for k,v in ss.items()) and s['exitCode']==0 and s['binaryUnchanged'] is True and s['workingSourceUnchanged'] is True,label+' snapshot closure')
need(ordered(r['start'],s['time'],r['finish']),label+' snapshot start during trial')
need(s['binary']==checker['readback'] and s['buildReceipt']==pin(craw('cargo/lifecycle-producer-build-2/result.json')),label+' corrected checker build')
need(s['helper']==pin(raw(spec['helpers']['snapshot'])),label+' snapshot helper pin')
need(s['source']==directory(q+'source/'),label+' exact saved source bytes')
for stream in ('stdout','stderr'):need(s[stream]==pin(raw(q+stream+'.log')),label+' '+stream)
need(not raw(q+'stderr.log'),label+' empty snapshot stderr')
cmd=s['command'];need(cmd[:3]==[checker['cargo']['executable'],'check',state] and mapped(cmd[3])==q+'source/original.arcadia',label+' checker command')
need(report['status']=='PASS' and report['comparison']=='complete Project equality' and report['state']==state and report['entry']==cmd[3],label+' accepted model oracle')
opts=cmd[4:];need(len(opts)%2==0,label+' option pairs')
for flag,path in zip(opts[::2],opts[1::2]):
need(flag in ('--identities','--write-identities'),label+' identity-only options')
need(obj(mapped(path))==report['new_ids'],label+' exact identity receipt')
if state.endswith(('-reopened','-noop')):need('--identities' in opts and report['trace_identity_pinned'] is True,label+' continuity identity pin')
if state.endswith('-relinked'):need('--write-identities' in opts and mapped(report['identity_receipt'])==mapped(opts[opts.index('--write-identities')+1]),label+' initial accepted identities')
snapshots[(run,label)]=s['source'];reports[(run,label)]=report
print(run,'captures',len(spec['captures']),'images',len(images),'accepted snapshots',len(spec['snapshots']))
for side,first,later in [('oa','lifecycle-2','lifecycle-3'),('sa','lifecycle-4','lifecycle-5')]:
base=snapshots[(first,side+'-relinked')];ids=reports[(first,side+'-relinked')]['new_ids']
for phase in ('reopened','noop'):
key=(later,side+'-'+phase)
need(snapshots[key]==base and reports[key]['new_ids']==ids,side+' saved bytes and identity continuity '+phase)
print('PASS: 4 closed trials;',len(snapshots),'accepted full-model receipts;',capture_count,'capture records;',image_count,'original images;',len(pairs),'linked source capsules.')
print('No UI history authentication or checker re-execution; snapshot receipts lack completion timestamps. Frame dimensions vary.')
if __name__=='__main__':main()