1111import hashlib
1212import json
1313import re
14- import unicodedata
1514import uuid
1615from datetime import date , datetime , timezone
1716from typing import Any , Callable , Literal , Optional , Union , cast
@@ -403,6 +402,7 @@ def embed_metadata(
403402 distribute_across_targets = distribute_across_targets ,
404403 add_hard_binding = add_hard_binding ,
405404 custom_assertions = custom_assertions ,
405+ custom_metadata = custom_metadata ,
406406 )
407407 # --- Start: Input Validation ---
408408 if not isinstance (text , str ):
@@ -758,6 +758,7 @@ def _embed_c2pa(
758758 claim_generator : Optional [str ],
759759 actions : Optional [list [dict [str , Any ]]],
760760 ingredients : Optional [list [dict [str , Any ]]],
761+ custom_metadata : Optional [dict [str , Any ]],
761762 iso_timestamp : Optional [str ],
762763 target : Optional [Union [str , MetadataTarget ]],
763764 distribute_across_targets : bool ,
@@ -779,6 +780,7 @@ def _embed_c2pa(
779780 claim_generator: A string identifying the software agent creating the claim.
780781 actions: A list of action dictionaries to include in the manifest.
781782 ingredients: A list of ingredient dictionaries for provenance chain.
783+ custom_metadata: Custom metadata to emit as a c2pa.metadata assertion.
782784 iso_timestamp: The ISO 8601 formatted timestamp for the actions.
783785 target: The embedding target strategy.
784786 distribute_across_targets: If True, distribute bits across multiple targets.
@@ -837,15 +839,32 @@ def _embed_c2pa(
837839 "@context" : c2pa_context_url ,
838840 "instance_id" : instance_id ,
839841 "claim_generator" : claim_gen ,
842+ "claim_label" : "c2pa.claim.v2" ,
840843 "assertions" : [],
841844 }
842845
843846 # Add ingredients for provenance chain (if provided)
844847 if ingredients :
845848 c2pa_manifest ["ingredients" ] = ingredients
849+ c2pa_manifest ["assertions" ].append (
850+ {
851+ "label" : "c2pa.ingredient.v3" ,
852+ "data" : {"ingredients" : copy .deepcopy (ingredients )},
853+ "kind" : "Ingredient" ,
854+ }
855+ )
846856
847857 actions_data : dict [str , Any ] = {"actions" : copy .deepcopy (base_actions )}
848- c2pa_manifest ["assertions" ].append ({"label" : "c2pa.actions.v1" , "data" : actions_data , "kind" : "Actions" })
858+ c2pa_manifest ["assertions" ].append ({"label" : "c2pa.actions.v2" , "data" : actions_data , "kind" : "Actions" })
859+
860+ if custom_metadata :
861+ c2pa_manifest ["assertions" ].append (
862+ {
863+ "label" : "c2pa.metadata" ,
864+ "data" : copy .deepcopy (custom_metadata ),
865+ "kind" : "Metadata" ,
866+ }
867+ )
849868
850869 # Add custom assertions if provided
851870 if custom_assertions :
@@ -883,7 +902,7 @@ def _embed_c2pa(
883902 manifest_for_hashing ["assertions" ].append (placeholder_soft_binding )
884903
885904 actions_data_copy = next (
886- (a ["data" ] for a in manifest_for_hashing ["assertions" ] if a .get ("label" ) == "c2pa.actions.v1 " ),
905+ (a ["data" ] for a in manifest_for_hashing ["assertions" ] if a .get ("label" ) == "c2pa.actions.v2 " ),
887906 None ,
888907 )
889908 if actions_data_copy and isinstance (actions_data_copy .get ("actions" ), list ):
@@ -1007,23 +1026,13 @@ def verify_metadata(
10071026 logger .warning ("C2PA format indicated but no text wrapper found." )
10081027 return False , signer_id , None
10091028
1010- wrapper_segment = text [span [0 ] : span [1 ]]
1011- normalized_full_text = unicodedata .normalize ("NFC" , text )
1012- normalized_index = normalized_full_text .rfind (wrapper_segment )
1013- if normalized_index < 0 :
1014- logger .warning ("Unable to locate wrapper segment in normalized text during verification." )
1015- return False , signer_id , None
1016-
1017- exclusion_start = len (normalized_full_text [:normalized_index ].encode ("utf-8" ))
1018- exclusion_length = len (wrapper_segment .encode ("utf-8" ))
1019-
10201029 return cls ._verify_c2pa (
10211030 original_text = text ,
10221031 outer_payload = outer_payload ,
10231032 public_key_resolver = public_key_resolver ,
10241033 return_payload_on_failure = return_payload_on_failure ,
10251034 require_hard_binding = require_hard_binding ,
1026- wrapper_exclusion = ( exclusion_start , exclusion_length ) ,
1035+ wrapper_exclusion = span ,
10271036 )
10281037
10291038 # --- Legacy Format Verification ('basic', 'manifest', 'cbor_manifest') ---
@@ -1222,14 +1231,18 @@ def _verify_c2pa(
12221231 # b) Check for mandatory assertions
12231232 assertions = c2pa_manifest .get ("assertions" , [])
12241233 assertion_labels = {a .get ("label" ) for a in assertions if isinstance (a , dict )}
1225- required_assertions = {"c2pa.actions.v1" , "c2pa. soft_binding.v1" }
1234+ required_assertions = {"c2pa.soft_binding.v1" }
12261235 if require_hard_binding :
12271236 required_assertions .add ("c2pa.hash.data.v1" )
12281237 if not required_assertions .issubset (assertion_labels ):
12291238 missing = required_assertions - assertion_labels
12301239 logger .warning (f"C2PA verification: Manifest missing required assertions: { missing } " )
12311240 return False , signer_id , c2pa_manifest
12321241
1242+ if not ("c2pa.actions.v1" in assertion_labels or "c2pa.actions.v2" in assertion_labels ):
1243+ logger .warning ("C2PA verification: Manifest missing actions assertion (v1 or v2)." )
1244+ return False , signer_id , c2pa_manifest
1245+
12331246 # --- 3. Soft Binding Verification (Deterministic Hashing) ---
12341247 soft_binding_assertion = next ((a for a in assertions if isinstance (a , dict ) and a .get ("label" ) == "c2pa.soft_binding.v1" ), None )
12351248 if soft_binding_assertion is None :
0 commit comments