@@ -100,11 +100,15 @@ def verify_bundle(bundle: Any, signing_key: str | None = None) -> dict[str, Any]
100100
101101 workflow = bundle .get ("workflow" ) if isinstance (bundle .get ("workflow" ), Mapping ) else {}
102102 history_events = bundle .get ("history_events" )
103+ instance_id = _string_or_none (workflow .get ("instance_id" )) if isinstance (workflow , Mapping ) else None
104+ run_id = _string_or_none (workflow .get ("run_id" )) if isinstance (workflow , Mapping ) else None
103105 bundle_summary = {
104106 "schema" : _string_or_none (bundle .get ("schema" )),
105107 "schema_version" : _int_or_none (bundle .get ("schema_version" )),
106- "instance_id" : _string_or_none (workflow .get ("instance_id" )) if isinstance (workflow , Mapping ) else None ,
107- "run_id" : _string_or_none (workflow .get ("run_id" )) if isinstance (workflow , Mapping ) else None ,
108+ "instance_id" : instance_id ,
109+ "run_id" : run_id ,
110+ "workflow_instance_id" : instance_id ,
111+ "workflow_run_id" : run_id ,
108112 "history_event_count" : len (history_events ) if isinstance (history_events , list ) else 0 ,
109113 }
110114
@@ -151,7 +155,24 @@ def _check_envelope(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -
151155 )
152156 )
153157
154- for section in ("workflow" , "history_events" , "commands" , "payloads" ):
158+ required_sections = {
159+ "workflow" : dict ,
160+ "payloads" : dict ,
161+ "history_events" : list ,
162+ "commands" : list ,
163+ "signals" : list ,
164+ "updates" : list ,
165+ "tasks" : list ,
166+ "activities" : list ,
167+ "timers" : list ,
168+ "failures" : list ,
169+ "links" : dict ,
170+ "redaction" : dict ,
171+ "codec_schemas" : dict ,
172+ "payload_manifest" : dict ,
173+ }
174+
175+ for section , expected_type in required_sections .items ():
155176 if section not in bundle :
156177 findings .append (
157178 _finding (
@@ -163,7 +184,6 @@ def _check_envelope(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -
163184 )
164185 continue
165186 value = bundle [section ]
166- expected_type = list if section in ("history_events" , "commands" ) else dict
167187 if not isinstance (value , expected_type ):
168188 findings .append (
169189 _finding (
@@ -363,7 +383,7 @@ def _check_payload_manifest(bundle: Mapping[str, Any], findings: list[dict[str,
363383 findings .append (
364384 _finding (
365385 "payload_manifest.codec_missing" ,
366- SEVERITY_WARNING ,
386+ SEVERITY_ERROR ,
367387 f"payload_manifest entry [{ path } ] does not declare a codec." ,
368388 {"path" : path },
369389 )
@@ -373,13 +393,13 @@ def _check_payload_manifest(bundle: Mapping[str, Any], findings: list[dict[str,
373393 redacted = bool (entry .get ("redacted" ))
374394 diagnostic = _string_or_none (entry .get ("diagnostic" ))
375395
376- if not available and not redacted and diagnostic == "payload_missing" :
396+ if available and not redacted and diagnostic == "payload_missing" :
377397 findings .append (
378398 _finding (
379399 "payload_manifest.payload_missing" ,
380- SEVERITY_WARNING ,
400+ SEVERITY_ERROR ,
381401 (
382- f"payload_manifest entry [{ path } ] is marked unavailable but flagged "
402+ f"payload_manifest entry [{ path } ] is marked available but flagged "
383403 "as payload_missing — bundle decode will be lossy."
384404 ),
385405 {"path" : path },
@@ -424,7 +444,7 @@ def _check_redaction(bundle: Mapping[str, Any], findings: list[dict[str, Any]])
424444 findings .append (
425445 _finding (
426446 "redaction.empty_paths" ,
427- SEVERITY_WARNING ,
447+ SEVERITY_INFO ,
428448 "redaction.applied=true but redaction.paths is empty — operators cannot tell what was redacted." ,
429449 )
430450 )
@@ -449,7 +469,7 @@ def _check_integrity(
449469 signature = _string_or_none (integrity .get ("signature" ))
450470 key_id = _string_or_none (integrity .get ("key_id" ))
451471
452- if canonicalization is not None and canonicalization not in SUPPORTED_CANONICALIZATIONS :
472+ if canonicalization not in SUPPORTED_CANONICALIZATIONS :
453473 findings .append (
454474 _finding (
455475 "integrity.canonicalization_unsupported" ,
@@ -461,9 +481,18 @@ def _check_integrity(
461481 {"canonicalization" : canonicalization },
462482 )
463483 )
464- return _integrity_report_for (canonicalization , checksum_algorithm , checksum , signature_algorithm , key_id )
484+ return _integrity_report_for (
485+ canonicalization ,
486+ checksum_algorithm ,
487+ checksum ,
488+ signature_algorithm ,
489+ key_id ,
490+ present = True ,
491+ signature_present = signature is not None ,
492+ signing_key_provided = bool (signing_key ),
493+ )
465494
466- if checksum_algorithm is not None and checksum_algorithm not in SUPPORTED_CHECKSUM_ALGORITHMS :
495+ if checksum_algorithm not in SUPPORTED_CHECKSUM_ALGORITHMS :
467496 findings .append (
468497 _finding (
469498 "integrity.checksum_algorithm_unsupported" ,
@@ -475,6 +504,16 @@ def _check_integrity(
475504 {"checksum_algorithm" : checksum_algorithm },
476505 )
477506 )
507+ return _integrity_report_for (
508+ canonicalization ,
509+ checksum_algorithm ,
510+ checksum ,
511+ signature_algorithm ,
512+ key_id ,
513+ present = True ,
514+ signature_present = signature is not None ,
515+ signing_key_provided = bool (signing_key ),
516+ )
478517
479518 if checksum is None :
480519 findings .append (
@@ -491,81 +530,113 @@ def _check_integrity(
491530 f"Bundle could not be canonicalized: { exc } " ,
492531 )
493532 )
494- return _integrity_report_for (canonicalization , checksum_algorithm , checksum , signature_algorithm , key_id )
533+ return _integrity_report_for (
534+ canonicalization ,
535+ checksum_algorithm ,
536+ checksum ,
537+ signature_algorithm ,
538+ key_id ,
539+ present = True ,
540+ signature_present = signature is not None ,
541+ signing_key_provided = bool (signing_key ),
542+ )
495543
496544 checksum_ok : bool | None = None
545+ recomputed_checksum : str | None = None
497546 if checksum_algorithm == "sha256" and checksum is not None :
498- expected = hashlib .sha256 (canonical_json .encode ("utf-8" )).hexdigest ()
499- checksum_ok = hmac .compare_digest (expected , checksum )
547+ recomputed_checksum = hashlib .sha256 (canonical_json .encode ("utf-8" )).hexdigest ()
548+ checksum_ok = hmac .compare_digest (recomputed_checksum , checksum )
500549 if not checksum_ok :
501550 findings .append (
502551 _finding (
503552 "integrity.checksum_mismatch" ,
504553 SEVERITY_ERROR ,
505554 (
506- f"integrity.checksum=[{ checksum } ] does not match recomputed sha256=[{ expected } ]. "
555+ f"integrity.checksum=[{ checksum } ] does not match recomputed sha256=[{ recomputed_checksum } ]. "
507556 "Bundle has been tampered with or canonicalization disagrees."
508557 ),
509- {"expected" : expected , "observed" : checksum },
558+ {"expected" : recomputed_checksum , "observed" : checksum },
510559 )
511560 )
512561
513562 signature_ok : bool | None = None
514- if signature_algorithm is not None :
515- if signature_algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS :
563+ if signature_algorithm is None and signature is None :
564+ return _integrity_report_for (
565+ canonicalization ,
566+ checksum_algorithm ,
567+ checksum ,
568+ signature_algorithm ,
569+ key_id ,
570+ present = True ,
571+ recomputed_checksum = recomputed_checksum ,
572+ checksum_matches = checksum_ok ,
573+ signature_present = False ,
574+ signature_verified = None ,
575+ signing_key_provided = bool (signing_key ),
576+ )
577+
578+ if signature_algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS :
579+ findings .append (
580+ _finding (
581+ "integrity.signature_algorithm_unsupported" ,
582+ SEVERITY_ERROR ,
583+ (
584+ f"integrity.signature_algorithm=[{ signature_algorithm } ] is not supported "
585+ f"(supported: { sorted (SUPPORTED_SIGNATURE_ALGORITHMS )} )."
586+ ),
587+ {"signature_algorithm" : signature_algorithm },
588+ )
589+ )
590+ elif signature is None :
591+ findings .append (
592+ _finding (
593+ "integrity.signature_missing" ,
594+ SEVERITY_ERROR ,
595+ "Bundle declares a signature_algorithm but does not include a signature." ,
596+ )
597+ )
598+ elif not signing_key :
599+ findings .append (
600+ _finding (
601+ "integrity.signature_key_unavailable" ,
602+ SEVERITY_WARNING ,
603+ "Bundle is signed but the verifier was not given a signing key — signature was not validated." ,
604+ {"key_id" : key_id },
605+ )
606+ )
607+ else :
608+ expected = hmac .new (
609+ signing_key .encode ("utf-8" ),
610+ canonical_json .encode ("utf-8" ),
611+ hashlib .sha256 ,
612+ ).hexdigest ()
613+ signature_ok = hmac .compare_digest (expected , signature )
614+ if not signature_ok :
516615 findings .append (
517616 _finding (
518- "integrity.signature_algorithm_unsupported " ,
617+ "integrity.signature_mismatch " ,
519618 SEVERITY_ERROR ,
520619 (
521- f "integrity.signature_algorithm=[ { signature_algorithm } ] is not supported "
522- f"(supported: { sorted ( SUPPORTED_SIGNATURE_ALGORITHMS ) } )."
620+ "integrity.signature does not match HMAC-SHA256 of the canonical bundle "
621+ f"under the provided key (key_id= { key_id or 'unknown' } )."
523622 ),
524- {"signature_algorithm" : signature_algorithm },
525- )
526- )
527- elif signature is None :
528- findings .append (
529- _finding (
530- "integrity.signature_missing" ,
531- SEVERITY_ERROR ,
532- "Bundle declares a signature_algorithm but does not include a signature." ,
533- )
534- )
535- elif not signing_key :
536- findings .append (
537- _finding (
538- "integrity.signature_key_unavailable" ,
539- SEVERITY_WARNING ,
540- "Bundle is signed but the verifier was not given a signing key — signature was not validated." ,
541623 {"key_id" : key_id },
542624 )
543625 )
544- else :
545- expected = hmac .new (
546- signing_key .encode ("utf-8" ),
547- canonical_json .encode ("utf-8" ),
548- hashlib .sha256 ,
549- ).hexdigest ()
550- signature_ok = hmac .compare_digest (expected , signature )
551- if not signature_ok :
552- findings .append (
553- _finding (
554- "integrity.signature_mismatch" ,
555- SEVERITY_ERROR ,
556- (
557- "integrity.signature does not match HMAC-SHA256 of the canonical bundle "
558- f"under the provided key (key_id={ key_id or 'unknown' } )."
559- ),
560- {"key_id" : key_id },
561- )
562- )
563626
564- report = _integrity_report_for (canonicalization , checksum_algorithm , checksum , signature_algorithm , key_id )
565- report ["checksum_ok" ] = checksum_ok
566- report ["signature_ok" ] = signature_ok
567- report ["signing_key_provided" ] = bool (signing_key )
568- return report
627+ return _integrity_report_for (
628+ canonicalization ,
629+ checksum_algorithm ,
630+ checksum ,
631+ signature_algorithm ,
632+ key_id ,
633+ present = True ,
634+ recomputed_checksum = recomputed_checksum ,
635+ checksum_matches = checksum_ok ,
636+ signature_present = signature is not None ,
637+ signature_verified = signature_ok ,
638+ signing_key_provided = bool (signing_key ),
639+ )
569640
570641
571642def _canonicalize (bundle : Mapping [str , Any ]) -> str :
@@ -587,8 +658,17 @@ def _finalize(
587658 bundle_summary : Mapping [str , Any ],
588659 integrity_report : Mapping [str , Any ],
589660) -> dict [str , Any ]:
590- summary = {"findings" : len (findings ), "errors" : 0 , "warnings" : 0 , "info" : 0 }
661+ summary : dict [str , Any ] = {
662+ "findings" : len (findings ),
663+ "errors" : 0 ,
664+ "warnings" : 0 ,
665+ "info" : 0 ,
666+ "findings_by_rule" : {},
667+ }
591668 for finding in findings :
669+ rule = str (finding .get ("rule" , "unknown" ))
670+ summary ["findings_by_rule" ][rule ] = summary ["findings_by_rule" ].get (rule , 0 ) + 1
671+
592672 severity = finding .get ("severity" , SEVERITY_INFO )
593673 if severity == SEVERITY_ERROR :
594674 summary ["errors" ] += 1
@@ -633,17 +713,29 @@ def _integrity_report_for(
633713 checksum : str | None ,
634714 signature_algorithm : str | None ,
635715 key_id : str | None ,
716+ * ,
717+ present : bool = False ,
718+ recomputed_checksum : str | None = None ,
719+ checksum_matches : bool | None = None ,
720+ signature_present : bool = False ,
721+ signature_verified : bool | None = None ,
722+ signing_key_provided : bool = False ,
636723) -> dict [str , Any ]:
637724 return {
725+ "present" : present ,
638726 "canonicalization" : canonicalization ,
639727 "checksum_algorithm" : checksum_algorithm ,
640- "checksum" : checksum ,
728+ "expected_checksum" : checksum ,
729+ "recomputed_checksum" : recomputed_checksum ,
730+ "checksum_matches" : checksum_matches ,
641731 "signature_algorithm" : signature_algorithm ,
642- "signature_present" : signature_algorithm is not None ,
732+ "signature_present" : signature_present ,
733+ "signature_verified" : signature_verified ,
643734 "key_id" : key_id ,
644- "checksum_ok" : None ,
645- "signature_ok" : None ,
646- "signing_key_provided" : False ,
735+ "checksum" : checksum ,
736+ "checksum_ok" : checksum_matches ,
737+ "signature_ok" : signature_verified ,
738+ "signing_key_provided" : signing_key_provided ,
647739 }
648740
649741
@@ -653,6 +745,8 @@ def _empty_bundle_summary() -> dict[str, Any]:
653745 "schema_version" : None ,
654746 "instance_id" : None ,
655747 "run_id" : None ,
748+ "workflow_instance_id" : None ,
749+ "workflow_run_id" : None ,
656750 "history_event_count" : 0 ,
657751 }
658752
0 commit comments