io_v2_provenance.py (6030B)
1 #!/usr/bin/env python3 2 """Real v2 recipe capture, producer removal and fresh explicitly trusted replay. 3 Supply a retained-built candidate sourceBuildGraph bundle; never change defaults. 4 """ 5 import argparse 6 import json 7 import os 8 from pathlib import Path 9 import re 10 import shutil 11 import subprocess 12 import time 13 from provenance import digest, put, object_path, snapshot, RECEIPT, SOURCE 14 from source_order import Runner 15 from http_headers import number 16 17 18 def main(): 19 parser=argparse.ArgumentParser(description=__doc__) 20 parser.add_argument('tricu',type=Path) 21 parser.add_argument('--compiler-bundle',type=Path,required=True) 22 parser.add_argument('--directory',type=Path,required=True) 23 args=parser.parse_args() 24 root=args.directory.resolve(); root.mkdir(parents=True,exist_ok=False) 25 producer=root/'producer'; producer.mkdir() 26 runner=Runner(args.tricu,producer) 27 compiler=runner.imported(args.compiler_bundle.resolve(),'compiler') 28 workspace=producer/'workspace'; workspace.mkdir() 29 (workspace/'tricu.workspace').write_text('tricu.workspace.v1\n') 30 (workspace/'dependency.tri').write_text('!export first\nfirst [x | _] = x\nfirst _ = 0\n') 31 (workspace/'main.tri').write_text('!import dependency as D\n!export main\n-- raw Unicode: λ 😀 é\nmain = D.first [42 7]\n') 32 runner.run('compile',['compile','--file',workspace/'main.tri','--toolchain',compiler, 33 '--toolchain-io-version','2','--tool-store',runner.store,'--store',runner.store, 34 '--output',producer/'program.arboricx']) 35 records=[]; checks=[] 36 def run(label,command,success=True): 37 started=time.monotonic() 38 result=subprocess.run([str(args.tricu.resolve()),*map(str,command),'+RTS','-N1','-M8192m','-T','-s','-RTS'], 39 cwd=root,env=dict(os.environ,tricu_datadir=str(root/'NO-PACKAGED-COMPILER')), 40 capture_output=True,text=True,timeout=180) 41 records.append(dict(label=label,command=list(map(str,command)),seconds=time.monotonic()-started, 42 code=result.returncode,stdout=result.stdout,stderr=result.stderr)) 43 (root/(label+'.log')).write_text(result.stdout+result.stderr) 44 assert (result.returncode==0)==success,(label,result.stdout,result.stderr) 45 return result.stdout+result.stderr 46 pack=root/'pack' 47 output=run('capture',['provenance','capture','--file',workspace/'main.tri','--store',runner.store, 48 '--tool-store',runner.store,'--toolchain',compiler,'--toolchain-io-version','2','--output',pack]) 49 pin=re.search(r'Retained build inputs: ([0-9a-f]{64})',output)[1] 50 data=(pack/'build-inputs').read_bytes() 51 assert pin==digest('arboricx.build-inputs.v2',data) 52 assert b'compiler-io\t2\n' in data 53 compilation_records=runner.records 54 shutil.rmtree(producer) 55 before=snapshot(pack) 56 assert 'Compiler IO version 2' in run('verify',['provenance','verify','--pack',pack,'--pin',pin]) 57 for label,trust,version in [('unauthorized', '0'*64,'2'),('wrong-version',compiler,'1')]: 58 run(label,['provenance','rebuild','--pack',pack,'--pin',pin,'--trust-compiler',trust, 59 '--trust-io-version',version,'--output',root/label],False) 60 assert not (root/label).exists() 61 replay=root/'replayed' 62 response=run('rebuild',['provenance','rebuild','--pack',pack,'--pin',pin,'--trust-compiler',compiler, 63 '--trust-io-version','2','--output',replay]) 64 assert 'Reproduced native module and receipt exactly' in response 65 assert snapshot(pack)==before 66 checks.append('offline-exact-module-and-receipt') 67 module=(replay/'application/cache/source-build/main').read_text().split('\t')[2] 68 # Evaluate the replay's actual stored executable independently, not a copied 69 # packaged output or an expected receipt from the producer. 70 reader=Runner(args.tricu,root/'execution'); reader.root.mkdir() 71 reader.store=replay/'application' 72 assert reader.invoke('value',module,'main',[])==number(42) 73 checks.append('independent-replayed-value') 74 # Retain valid but false producer claims and a matching poisoned cache. Verify 75 # authenticates bytes only; fresh replay must detect the executable mismatch. 76 forged=root/'forged'; shutil.copytree(pack,forged) 77 store=forged/'store' 78 rows=data.decode().splitlines() 79 old_receipt=next(row.split('\t')[1] for row in rows if row.startswith('receipt\t')) 80 receipt_rows=object_path(store,old_receipt).read_text().splitlines() 81 src=put(store,SOURCE,b'!export main\nmain = 99\n') 82 receipt_bytes=('\n'.join([RECEIPT,'source\t'+src,*receipt_rows[2:5]])+'\n').encode() 83 receipt=put(store,RECEIPT,receipt_bytes) 84 descriptor=('\n'.join([rows[0],'entry\tmain','receipt\t'+receipt,rows[3],rows[4], 85 'unit\tmain\t'+receipt])+'\n').encode() 86 (forged/'build-inputs').write_bytes(descriptor) 87 forged_pin=digest('arboricx.build-inputs.v2',descriptor) 88 key_bytes=('\n'.join(['arboricx.source-build-key.v1','source\t'+src,*receipt_rows[2:4]])+'\n').encode() 89 key=digest('arboricx.source-build-key.v1',key_bytes) 90 cache=store/'cache/source-build/main'; cache.parent.mkdir(parents=True) 91 cache.write_text('\t'.join(['arboricx.source-build-cache.v1',key,receipt_rows[4].split('\t')[1],receipt])) 92 run('verify-false-claim',['provenance','verify','--pack',forged,'--pin',forged_pin]) 93 response=run('reject-false-claim',['provenance','rebuild','--pack',forged,'--pin',forged_pin, 94 '--trust-compiler',compiler,'--trust-io-version','2','--output',root/'mismatch'],False) 95 assert 'Rebuild mismatch' in response and 'tree different' in response 96 checks.append('authenticated-false-claim-and-cache-poison-rejected') 97 (root/'report.json').write_text(json.dumps(dict(checks=checks,compiler=compiler,io_version=2, 98 producer_removed=True,pack_unchanged=True,records=records,compilation=compilation_records),indent=2)+'\n') 99 print('PASS v2 offline replay, explicit authority, exact receipts, independent output and false-claim/cache controls') 100 101 102 if __name__=='__main__': main()