116 lines
10 KiB
Python
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()
|