@@ -85,6 +85,134 @@ def _guess_app_name(original_filename: str) -> str:
8585 safe = name [:80 ]
8686 return safe
8787
88+
89+ def _render_attestation_section (
90+ * ,
91+ uploaded_filename : str ,
92+ file_hash : str ,
93+ is_deb : bool ,
94+ sig_detail : dict ,
95+ sig_valid : bool ,
96+ sig_info : dict ,
97+ clam_detail : dict | None ,
98+ clam_state : bool | None ,
99+ clam_label : str ,
100+ artifacts : dict ,
101+ risk_score : int ,
102+ risk_level : str ,
103+ risk_evidence : list ,
104+ ) -> None :
105+ st .markdown ("---" )
106+ st .subheader ("Attestation (Signed Scan Result)" )
107+ if _ATTEST_IMPORT_ERROR :
108+ st .warning (f"Attestation unavailable: { _ATTEST_IMPORT_ERROR } " )
109+ return
110+
111+ try :
112+ priv , pub , key_id = ensure_keypair ()
113+ payload = build_scan_payload (
114+ original_filename = uploaded_filename ,
115+ file_sha256 = file_hash ,
116+ is_deb = bool (is_deb ),
117+ sig_detail = sig_detail ,
118+ sig_valid = bool (sig_valid ),
119+ sig_info = sig_info if isinstance (sig_info , dict ) else {},
120+ clam_detail = clam_detail if isinstance (clam_detail , dict ) else None ,
121+ clam_state = clam_state ,
122+ clam_label = str (clam_label or "" ),
123+ artifacts = artifacts ,
124+ risk_score = int (risk_score ),
125+ risk_level = str (risk_level ),
126+ risk_evidence = list (risk_evidence ),
127+ )
128+ attestation = build_attestation (payload , private_key = priv , public_key = pub )
129+
130+ att_bytes = json .dumps (attestation , indent = 2 , ensure_ascii = False ).encode ("utf-8" )
131+ pub_key_pem = attestation .get ("signature" , {}).get ("public_key_pem" , "" )
132+
133+ c1 , c2 = st .columns ([1.2 , 1.0 ])
134+ with c1 :
135+ st .download_button (
136+ "Download attestation.json" ,
137+ data = att_bytes ,
138+ file_name = f"attestation_{ file_hash [:12 ]} .json" ,
139+ mime = "application/json" ,
140+ )
141+ st .caption (f"Signed with key id: { key_id } " )
142+ with c2 :
143+ if isinstance (pub_key_pem , str ) and pub_key_pem .strip ():
144+ st .download_button (
145+ "Download public key (PEM)" ,
146+ data = pub_key_pem .encode ("utf-8" ),
147+ file_name = f"provity_attestation_pubkey_{ key_id } .pem" ,
148+ mime = "application/x-pem-file" ,
149+ )
150+
151+ with st .expander ("Attestation preview" ):
152+ st .json (attestation )
153+ except AttestationError as e :
154+ st .error (str (e ))
155+ except Exception as e :
156+ st .error (f"Failed to build attestation: { e } " )
157+
158+
159+ def _render_verify_attestation_tab () -> None :
160+ # ============================================================================
161+ # VERIFY TAB: NO DATABASE LOGGING
162+ # This tab performs read-only verification of attestations.
163+ # It does NOT call insert_scan_event() or write to the database.
164+ # All file uploads here are temporary and only used for verification.
165+ # ============================================================================
166+
167+ st .subheader ("Verify Attestation" )
168+ st .caption (
169+ "Upload attestation JSON and the original file. Public key (PEM) is optional if using the same Provity instance."
170+ )
171+
172+ if _ATTEST_IMPORT_ERROR :
173+ st .error (f"Attestation features unavailable: { _ATTEST_IMPORT_ERROR } " )
174+ return
175+
176+ pubkey_file = st .file_uploader (
177+ "Issuer public key (PEM) - optional" ,
178+ type = ["pem" ],
179+ key = "attestation_verify_pubkey" ,
180+ help = "Optional. If not provided, uses local trusted issuer key (same Provity instance)." ,
181+ )
182+ att_file = st .file_uploader ("Attestation file (JSON)" , type = ["json" ], key = "attestation_verify" )
183+ orig_file = st .file_uploader ("Original file" , key = "attestation_verify_file" )
184+
185+ if att_file is None or orig_file is None :
186+ st .info ("Upload attestation JSON and the original file to verify." )
187+ return
188+
189+ try :
190+ att_obj = parse_attestation_json (att_file .getvalue ())
191+ pubkey_pem = pubkey_file .getvalue ().decode ("utf-8" , errors = "replace" ) if pubkey_file else None
192+ result = verify_attestation (att_obj , file_bytes = orig_file .getvalue (), public_key_pem = pubkey_pem )
193+
194+ if result .get ("ok" ) is True :
195+ st .success ("✅ Attestation verified" )
196+ st .write (f"**Key ID:** { result .get ('key_id' )} " )
197+ st .write (f"**Issuer:** { result .get ('issuer_source' , 'unknown' )} " )
198+ st .write (f"**File SHA-256:** { result .get ('actual_sha256' )} " )
199+
200+ with st .expander ("Verified payload" ):
201+ st .json (result .get ("payload" ) or {})
202+ else :
203+ st .error ("❌ Verification failed" )
204+ st .write (f"**Reason:** { result .get ('reason' )} " )
205+ if result .get ("expected_sha256" ):
206+ st .write (f"**Expected SHA-256:** { result .get ('expected_sha256' )} " )
207+ if result .get ("actual_sha256" ):
208+ st .write (f"**Actual SHA-256:** { result .get ('actual_sha256' )} " )
209+ with st .expander ("Attestation (raw)" ):
210+ st .json (att_obj )
211+ except AttestationError as e :
212+ st .error (str (e ))
213+ except Exception as e :
214+ st .error (f"Verification error: { e } " )
215+
88216# Page Configuration
89217# NOTE: `page_icon` sets the browser tab favicon in Streamlit.
90218st .set_page_config (page_title = "Provity : Trusted Software Validator" , page_icon = "🛡️" , layout = "wide" )
@@ -419,57 +547,21 @@ def _guess_app_name(original_filename: str) -> str:
419547 st .metric ("Risk Score" , f"{ risk_score } /100" )
420548 st .markdown ("\n " .join ([f"- { item } " for item in risk_evidence ]))
421549
422- st .markdown ("---" )
423- st .subheader ("Attestation (Signed Scan Result)" )
424- if _ATTEST_IMPORT_ERROR :
425- st .warning (f"Attestation unavailable: { _ATTEST_IMPORT_ERROR } " )
426- else :
427- try :
428- priv , pub , key_id = ensure_keypair ()
429- payload = build_scan_payload (
430- original_filename = uploaded_file .name ,
431- file_sha256 = file_hash ,
432- is_deb = bool (is_deb ),
433- sig_detail = sig_detail ,
434- sig_valid = bool (sig_valid ),
435- sig_info = sig_info if isinstance (sig_info , dict ) else {},
436- clam_detail = clam_detail if isinstance (clam_detail , dict ) else None ,
437- clam_state = is_clean ,
438- clam_label = str (virus_name or "" ),
439- artifacts = artifacts ,
440- risk_score = int (risk_score ),
441- risk_level = str (risk_level ),
442- risk_evidence = list (risk_evidence ),
443- )
444- attestation = build_attestation (payload , private_key = priv , public_key = pub )
445-
446- att_bytes = json .dumps (attestation , indent = 2 , ensure_ascii = False ).encode ("utf-8" )
447- pub_key_pem = attestation .get ("signature" , {}).get ("public_key_pem" , "" )
448-
449- c1 , c2 = st .columns ([1.2 , 1.0 ])
450- with c1 :
451- st .download_button (
452- "Download attestation.json" ,
453- data = att_bytes ,
454- file_name = f"attestation_{ file_hash [:12 ]} .json" ,
455- mime = "application/json" ,
456- )
457- st .caption (f"Signed with key id: { key_id } " )
458- with c2 :
459- if isinstance (pub_key_pem , str ) and pub_key_pem .strip ():
460- st .download_button (
461- "Download public key (PEM)" ,
462- data = pub_key_pem .encode ("utf-8" ),
463- file_name = f"provity_attestation_pubkey_{ key_id } .pem" ,
464- mime = "application/x-pem-file" ,
465- )
466-
467- with st .expander ("Attestation preview" ):
468- st .json (attestation )
469- except AttestationError as e :
470- st .error (str (e ))
471- except Exception as e :
472- st .error (f"Failed to build attestation: { e } " )
550+ _render_attestation_section (
551+ uploaded_filename = uploaded_file .name ,
552+ file_hash = file_hash ,
553+ is_deb = bool (is_deb ),
554+ sig_detail = sig_detail ,
555+ sig_valid = bool (sig_valid ),
556+ sig_info = sig_info if isinstance (sig_info , dict ) else {},
557+ clam_detail = clam_detail if isinstance (clam_detail , dict ) else None ,
558+ clam_state = is_clean ,
559+ clam_label = str (virus_name or "" ),
560+ artifacts = artifacts ,
561+ risk_score = int (risk_score ),
562+ risk_level = str (risk_level ),
563+ risk_evidence = list (risk_evidence ),
564+ )
473565
474566 # Persist scan event (best-effort)
475567 if db_enabled and db_log_scans :
@@ -510,57 +602,8 @@ def _guess_app_name(original_filename: str) -> str:
510602
511603
512604with tab_verify :
513- # ============================================================================
514- # VERIFY TAB: NO DATABASE LOGGING
515- # This tab performs read-only verification of attestations.
516- # It does NOT call insert_scan_event() or write to the database.
517- # All file uploads here are temporary and only used for verification.
518- # ============================================================================
519-
520- st .subheader ("Verify Attestation" )
521- st .caption ("Upload attestation JSON and the original file. Public key (PEM) is optional if using the same Provity instance." )
522-
523- if _ATTEST_IMPORT_ERROR :
524- st .error (f"Attestation features unavailable: { _ATTEST_IMPORT_ERROR } " )
525- else :
526- pubkey_file = st .file_uploader (
527- "Issuer public key (PEM) - optional" ,
528- type = ["pem" ],
529- key = "attestation_verify_pubkey" ,
530- help = "Optional. If not provided, uses local trusted issuer key (same Provity instance)." ,
531- )
532- att_file = st .file_uploader ("Attestation file (JSON)" , type = ["json" ], key = "attestation_verify" )
533- orig_file = st .file_uploader ("Original file" , key = "attestation_verify_file" )
534605
535- if att_file is None or orig_file is None :
536- st .info ("Upload attestation JSON and the original file to verify." )
537- else :
538- try :
539- att_obj = parse_attestation_json (att_file .getvalue ())
540- pubkey_pem = pubkey_file .getvalue ().decode ("utf-8" , errors = "replace" ) if pubkey_file else None
541- result = verify_attestation (att_obj , file_bytes = orig_file .getvalue (), public_key_pem = pubkey_pem )
542-
543- if result .get ("ok" ) is True :
544- st .success ("✅ Attestation verified" )
545- st .write (f"**Key ID:** { result .get ('key_id' )} " )
546- st .write (f"**Issuer:** { result .get ('issuer_source' , 'unknown' )} " )
547- st .write (f"**File SHA-256:** { result .get ('actual_sha256' )} " )
548-
549- with st .expander ("Verified payload" ):
550- st .json (result .get ("payload" ) or {})
551- else :
552- st .error ("❌ Verification failed" )
553- st .write (f"**Reason:** { result .get ('reason' )} " )
554- if result .get ("expected_sha256" ):
555- st .write (f"**Expected SHA-256:** { result .get ('expected_sha256' )} " )
556- if result .get ("actual_sha256" ):
557- st .write (f"**Actual SHA-256:** { result .get ('actual_sha256' )} " )
558- with st .expander ("Attestation (raw)" ):
559- st .json (att_obj )
560- except AttestationError as e :
561- st .error (str (e ))
562- except Exception as e :
563- st .error (f"Verification error: { e } " )
606+ _render_verify_attestation_tab ()
564607
565608
566609with tab_dashboard :
0 commit comments