1+ import base64
2+ import binascii
13import json
24import mimetypes
35import os
@@ -19,6 +21,18 @@ def _mimetype_to_ext(asset_mime_type: str):
1921 return ext
2022
2123
24+ def format_claim_generator (name : str ) -> str :
25+ """
26+ Claim generator must be underscore-separated Pascal case
27+ ex. Numbers_Protocol
28+ """
29+ # Split the name into words based on spaces or other delimiters
30+ words = name .replace ('-' , ' ' ).replace ('_' , ' ' ).split ()
31+
32+ # Capitalize each word and join with underscores
33+ return '_' .join (word .capitalize () for word in words )
34+
35+
2236def format_datetime (date : Optional [datetime ], to_timestamp = False ) -> Optional [Union [str , int ]]:
2337 if not date :
2438 return None
@@ -36,6 +50,8 @@ def c2patool_inject(
3650 manifest_path : str ,
3751 output_path : str ,
3852 force_overwrite : bool ,
53+ * ,
54+ parent_path : Optional [str ] = None ,
3955 private_key : Optional [str ] = None ,
4056 sign_cert : Optional [str ] = None ,
4157):
@@ -46,6 +62,8 @@ def c2patool_inject(
4662 env_vars ['C2PA_SIGN_CERT' ] = sign_cert
4763 command = f"c2patool '{ file_path } ' -m '{ manifest_path } ' -o '{ output_path } '"
4864
65+ if parent_path :
66+ command += f" -p '{ parent_path } '"
4967 if force_overwrite :
5068 command += ' -f'
5169 try :
@@ -60,18 +78,70 @@ def c2patool_inject(
6078 raise UnknownError (e .stderr ) from e
6179
6280
81+ def create_assertion_asset_tree (
82+ asset_tree_cid : Optional [str ] = None ,
83+ asset_tree_sha256 : Optional [str ] = None ,
84+ asset_tree_signature : Optional [str ] = None ,
85+ committer : Optional [str ] = None ,
86+ ) -> Optional [Dict [str , Any ]]:
87+ if not asset_tree_cid or not asset_tree_sha256 or not asset_tree_signature or not committer :
88+ return None
89+
90+ return {
91+ 'label' : 'io.numbersprotocol.asset-tree' ,
92+ 'data' : {
93+ 'assetTreeCid' : asset_tree_cid ,
94+ 'assetTreeSha256' : asset_tree_sha256 ,
95+ 'assetTreeSignature' : asset_tree_signature ,
96+ 'committer' : committer ,
97+ }
98+ }
99+
100+
101+ def create_assertion_creative_work (
102+ nid : str ,
103+ creator_name : str ,
104+ date_created : Optional [datetime ] = None ,
105+ location_created : Optional [str ] = None ,
106+ ):
107+ data = {
108+ '@context' : 'https://schema.org' ,
109+ '@type' : 'CreativeWork' ,
110+ 'url' : f'https://verify.numbersprotocol.io/asset-profile/{ nid } ' ,
111+ 'identifier' : nid ,
112+ }
113+ if creator_name :
114+ data ['author' ] = [
115+ {
116+ '@type' : 'Person' ,
117+ 'name' : creator_name ,
118+ }
119+ ]
120+ if isinstance (date_created , datetime ):
121+ data ['dateCreated' ] = date_created .strftime ('%Y-%m-%dT%H:%M:%SZ' )
122+ if location_created :
123+ data ['locationCreated' ] = location_created
124+
125+ return {
126+ 'label' : 'stds.schema-org.CreativeWork' ,
127+ 'data' : data
128+ }
129+
130+
63131def create_c2pa_manifest (
64132 nid : str ,
133+ creator_name : str ,
65134 creator_public_key : str ,
66135 asset_hash : str ,
67- date_created : datetime ,
136+ * ,
137+ date_created : Optional [datetime ] = None ,
68138 latitude : Optional [str ] = None ,
69139 longitude : Optional [str ] = None ,
70140 date_captured : Optional [datetime ] = None ,
71141 alg : str = 'es256' ,
72142 ta_url : str = 'http://timestamp.digicert.com' ,
73143 vendor : str = 'numbersprotocol' ,
74- claim_generator : str = 'Numbers_Protocol ' ,
144+ claim_generator_name : str = 'Numbers Protocol ' ,
75145 digital_source_type : Optional [str ] = None ,
76146 generated_by : Optional [str ] = None ,
77147 asset_tree_cid : Optional [str ] = None ,
@@ -83,51 +153,34 @@ def create_c2pa_manifest(
83153 f'{ format_geolocation (latitude )} , { format_geolocation (longitude )} '
84154 if latitude and longitude else None
85155 )
156+
86157 manifest = {
87158 'alg' : alg ,
88159 'ta_url' : ta_url ,
89160 'vendor' : vendor ,
90- 'claim_generator' : claim_generator ,
91- 'title' : nid ,
92- 'assertions' : [
161+ 'claim_generator' : format_claim_generator (claim_generator_name ),
162+ 'claim_generator_info' : [
93163 {
94- 'label' : 'stds.schema-org.CreativeWork' ,
95- 'data' : {
96- '@context' : 'https://schema.org' ,
97- '@type' : 'CreativeWork' ,
98- 'url' : f'https://verify.numbersprotocol.io/asset-profile/{ nid } ' ,
99- 'author' : [
100- {
101- '@type' : 'Person' ,
102- 'name' : creator_public_key ,
103- }
104- ],
105- 'dateCreated' : date_created .strftime ('%Y-%m-%dT%H:%M:%SZ' ),
106- 'locationCreated' : location_created ,
107- 'identifier' : nid ,
108- }
164+ 'name' : claim_generator_name ,
109165 },
166+ ],
167+ 'title' : nid ,
168+ 'assertions' : [
169+ create_assertion_creative_work (
170+ nid , creator_name , date_created , location_created ,
171+ ),
110172 {
111- 'label' : 'c2pa.actions' ,
173+ 'label' : 'c2pa.actions.v2 ' ,
112174 'data' : {
113175 'actions' : [
114- {
115- 'action' : 'c2pa.opened' ,
116- }
176+ create_action_c2pa_opened (
177+ asset_hash , digital_source_type , generated_by ,
178+ ),
117179 ],
118180 }
119181 },
120182 {
121- 'label' : 'numbers.assetTree' ,
122- 'data' : {
123- 'assetTreeCid' : asset_tree_cid ,
124- 'assetTreeSha256' : asset_tree_sha256 ,
125- 'assetTreeSignature' : asset_tree_signature ,
126- 'committer' : committer ,
127- }
128- },
129- {
130- 'label' : 'numbers.integrity.json' ,
183+ 'label' : 'io.numbersprotocol.integrity' ,
131184 'data' : {
132185 'nid' : nid ,
133186 'publicKey' : creator_public_key ,
@@ -155,18 +208,15 @@ def create_c2pa_manifest(
155208 },
156209 ]
157210 }
158- if digital_source_type :
159- manifest ['assertions' ][1 ]['data' ]['actions' ][0 ].update ({
160- 'digitalSourceType' : f'http://cv.iptc.org/newscodes/digitalsourcetype/{ digital_source_type } ' ,
161- })
162- if generated_by :
163- manifest ['assertions' ][1 ]['data' ]['actions' ][0 ].update ({
164- 'softwareAgent' : f'{ generated_by } '
165- })
211+ if assertion_asset_tree := create_assertion_asset_tree (
212+ asset_tree_cid , asset_tree_sha256 , asset_tree_signature , committer ,
213+ ):
214+ manifest ['assertions' ].append (assertion_asset_tree )
166215 return manifest
167216
168217
169218def create_custom_c2pa_manifest (
219+ * ,
170220 alg : str = 'es256' ,
171221 ta_url : str = 'http://timestamp.digicert.com' ,
172222 vendor : str = 'numbersprotocol' ,
@@ -219,10 +269,41 @@ def create_custom_c2pa_manifest(
219269 return manifest
220270
221271
272+ def create_action_c2pa_opened (
273+ asset_hex_hash : str ,
274+ digital_source_type : Optional [str ] = None ,
275+ software_agent : Optional [str ] = None ,
276+ ) -> Dict [str , Any ]:
277+ base64_hash = base64 .b64encode (binascii .unhexlify (asset_hex_hash )).decode ()
278+ action = {
279+ 'action' : 'c2pa.opened' ,
280+ 'parameters' : {
281+ 'ingredients' : [
282+ {
283+ 'url' : 'self#jumbf=c2pa.assertions/c2pa.ingredient' ,
284+ 'alg' : 'sha256' ,
285+ 'hash' : base64_hash ,
286+ },
287+ ],
288+ },
289+ }
290+ if digital_source_type :
291+ action ['digitalSourceType' ] = (
292+ 'http://cv.iptc.org/newscodes/digitalsourcetype/'
293+ f'{ digital_source_type } '
294+ )
295+ if software_agent :
296+ action ['softwareAgent' ] = {
297+ 'name' : software_agent ,
298+ }
299+ return action
300+
301+
222302def inject (
223303 asset_bytes : bytes ,
224304 asset_mime_type : str ,
225305 manifest : Dict ,
306+ * ,
226307 private_key : Optional [str ] = None ,
227308 sign_cert : Optional [str ] = None ,
228309 force_overwrite : bool = True ,
@@ -258,6 +339,8 @@ def inject_file(
258339 asset_file : str ,
259340 c2pa_output_file : str ,
260341 manifest : Dict [str , Any ],
342+ * ,
343+ parent_path : Optional [str ] = None ,
261344 private_key : Optional [str ] = None ,
262345 sign_cert : Optional [str ] = None ,
263346 force_overwrite : bool = True ,
@@ -290,6 +373,7 @@ def inject_file(
290373 manifest_file .name ,
291374 c2pa_output_file ,
292375 force_overwrite = force_overwrite ,
376+ parent_path = parent_path ,
293377 private_key = private_key ,
294378 sign_cert = sign_cert ,
295379 )
0 commit comments