@@ -97,12 +97,13 @@ def _sign_jcs(body: dict, seed: bytes = BACKEND_SEED) -> str:
9797def _decision_receipt_body (
9898 payload_hash : str = PAYLOAD_HASH ,
9999 * ,
100+ audit_id : str = "audit-1" ,
100101 risk_class : str = "write" ,
101102 policy_context_hash : str = POLICY_CONTEXT_HASH ,
102103) -> dict :
103104 return {
104105 "schema_version" : "decision_receipt/2" ,
105- "audit_id" : "audit-1" ,
106+ "audit_id" : audit_id ,
106107 "agent_did" : "did:key:z6Mkagent" ,
107108 "decision" : "WAITING_FOR_HUMAN_APPROVAL" ,
108109 "payload_hash" : payload_hash ,
@@ -115,12 +116,14 @@ def _decision_receipt(
115116 payload_hash : str = PAYLOAD_HASH ,
116117 seed : bytes = BACKEND_SEED ,
117118 * ,
119+ audit_id : str = "audit-1" ,
118120 risk_class : str = "write" ,
119121 policy_context_hash : str = POLICY_CONTEXT_HASH ,
120122) -> str :
121123 return _sign_jcs (
122124 _decision_receipt_body (
123125 payload_hash ,
126+ audit_id = audit_id ,
124127 risk_class = risk_class ,
125128 policy_context_hash = policy_context_hash ,
126129 ),
@@ -466,6 +469,16 @@ def test_verify_rejects_receipt_with_missing_audit_id(tmp_path):
466469 verify_evidence_bundle (bundle )
467470
468471
472+ def test_verify_rejects_receipt_missing_audit_id_when_record_has_one (tmp_path ):
473+ body = _decision_receipt_body ()
474+ body .pop ("audit_id" )
475+ bundle = _bundle_with_receipt (tmp_path , receipt_jcs = _sign_jcs (body ))
476+ assert bundle ["records" ][0 ]["decision_audit_id" ] == "audit-1"
477+
478+ with pytest .raises (EvidenceVerificationError , match = "audit_id missing" ):
479+ verify_evidence_bundle (bundle )
480+
481+
469482def test_verify_rejects_receipt_missing_payload_hash_when_referenced (tmp_path ):
470483 body = _decision_receipt_body ()
471484 body .pop ("payload_hash" )
@@ -493,6 +506,129 @@ def test_verify_rejects_receipt_missing_client_policy_context_hash_when_referenc
493506 verify_evidence_bundle (bundle )
494507
495508
509+ def test_verify_rejects_receipt_audit_id_mismatch_with_record (tmp_path ):
510+ receipt_jcs = _decision_receipt (audit_id = "audit-Y" )
511+ bundle = _bundle_with_receipt (tmp_path , receipt_jcs = receipt_jcs )
512+
513+ with pytest .raises (EvidenceVerificationError , match = "audit_id mismatch" ):
514+ verify_evidence_bundle (bundle )
515+
516+
517+ def test_verify_accepts_matching_audit_id (tmp_path ):
518+ receipt_jcs = _decision_receipt (audit_id = "audit-X" )
519+ digest = hashlib .sha256 (receipt_jcs .encode ("utf-8" )).hexdigest ()
520+ with _store (tmp_path ) as store :
521+ store .write_pending (_record (
522+ "req-audit-match" ,
523+ decision_audit_id = "audit-X" ,
524+ decision_receipt_sha256 = digest ,
525+ ))
526+ bundle = build_evidence_bundle (
527+ store ,
528+ proxy_identity_did = "did:key:z6Mkproxy" ,
529+ trusted_signer_dids = [BACKEND_DID ],
530+ receipt_fetcher = lambda _audit_id : receipt_jcs ,
531+ )
532+
533+ assert verify_evidence_bundle (bundle ).valid is True
534+
535+
536+ def test_verify_skips_audit_id_check_for_cache_hit_records (tmp_path ):
537+ with _store (tmp_path ) as store :
538+ store .write_pending (_record (
539+ "req-cache-hit" ,
540+ decision_audit_id = None ,
541+ decision_receipt_sha256 = None ,
542+ ))
543+ bundle = build_evidence_bundle (
544+ store ,
545+ proxy_identity_did = "did:key:z6Mkproxy" ,
546+ trusted_signer_dids = [BACKEND_DID ],
547+ )
548+
549+ assert verify_evidence_bundle (bundle ).valid is True
550+
551+
552+ def test_verify_rejects_bundle_with_duplicate_receipt_references (tmp_path ):
553+ receipt_jcs = _decision_receipt ()
554+ digest = hashlib .sha256 (receipt_jcs .encode ("utf-8" )).hexdigest ()
555+ with _store (tmp_path ) as store :
556+ store .write_pending (_record (
557+ "req-first" ,
558+ created_at = 1_700_000_000 ,
559+ decision_audit_id = "audit-1" ,
560+ decision_receipt_sha256 = digest ,
561+ ))
562+ store .write_pending (_record (
563+ "req-second" ,
564+ created_at = 1_700_000_001 ,
565+ decision_audit_id = "audit-1" ,
566+ decision_receipt_sha256 = digest ,
567+ ))
568+ bundle = build_evidence_bundle (
569+ store ,
570+ proxy_identity_did = "did:key:z6Mkproxy" ,
571+ trusted_signer_dids = [BACKEND_DID ],
572+ receipt_fetcher = lambda _audit_id : receipt_jcs ,
573+ )
574+
575+ with pytest .raises (EvidenceVerificationError , match = "referenced by multiple records" ):
576+ verify_evidence_bundle (bundle )
577+
578+
579+ def test_verify_accepts_bundle_with_distinct_receipt_per_record (tmp_path ):
580+ receipts = {
581+ "audit-1" : _decision_receipt (audit_id = "audit-1" ),
582+ "audit-2" : _decision_receipt (audit_id = "audit-2" ),
583+ }
584+ digests = {
585+ audit_id : hashlib .sha256 (receipt .encode ("utf-8" )).hexdigest ()
586+ for audit_id , receipt in receipts .items ()
587+ }
588+ with _store (tmp_path ) as store :
589+ for index , audit_id in enumerate (("audit-1" , "audit-2" )):
590+ store .write_pending (_record (
591+ f"req-{ index } " ,
592+ created_at = 1_700_000_000 + index ,
593+ decision_audit_id = audit_id ,
594+ decision_receipt_sha256 = digests [audit_id ],
595+ ))
596+ bundle = build_evidence_bundle (
597+ store ,
598+ proxy_identity_did = "did:key:z6Mkproxy" ,
599+ trusted_signer_dids = [BACKEND_DID ],
600+ receipt_fetcher = receipts .__getitem__ ,
601+ )
602+
603+ assert verify_evidence_bundle (bundle ).valid is True
604+
605+
606+ def test_verify_accepts_bundle_with_cache_hit_records_no_receipt_reference (tmp_path ):
607+ receipt_jcs = _decision_receipt ()
608+ digest = hashlib .sha256 (receipt_jcs .encode ("utf-8" )).hexdigest ()
609+ with _store (tmp_path ) as store :
610+ store .write_pending (_record (
611+ "req-receipt" ,
612+ created_at = 1_700_000_000 ,
613+ decision_audit_id = "audit-1" ,
614+ decision_receipt_sha256 = digest ,
615+ ))
616+ store .write_pending (_record (
617+ "req-cache-hit" ,
618+ created_at = 1_700_000_001 ,
619+ decision_audit_id = None ,
620+ decision_receipt_sha256 = None ,
621+ ))
622+ bundle = build_evidence_bundle (
623+ store ,
624+ proxy_identity_did = "did:key:z6Mkproxy" ,
625+ trusted_signer_dids = [BACKEND_DID ],
626+ receipt_fetcher = lambda _audit_id : receipt_jcs ,
627+ )
628+
629+ assert verify_evidence_bundle (bundle ).valid is True
630+
631+
496632def test_verify_warns_on_orphan_signed_receipt_not_referenced (tmp_path ):
497633 receipt_jcs = _decision_receipt ()
498634 digest = hashlib .sha256 (receipt_jcs .encode ("utf-8" )).hexdigest ()
0 commit comments