From 6334191790e489b481867c288167cabbbb074895 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 27 Jun 2025 19:40:57 -0700 Subject: [PATCH 1/8] fix: Update examples --- examples/sign.py | 18 +++++++++-------- examples/training.py | 48 +++++++++++++++----------------------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/examples/sign.py b/examples/sign.py index 82070b42..6060d18a 100644 --- a/examples/sign.py +++ b/examples/sign.py @@ -58,15 +58,17 @@ "ingredients": [], "assertions": [ { - 'label': 'stds.schema-org.CreativeWork', - 'data': { - '@context': 'http://schema.org/', - '@type': 'CreativeWork', - 'author': [ - {'@type': 'Person', 'name': 'Example User'} + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "parameters": { + # could hold additional information about this step + } + } ] - }, - 'kind': 'Json' + } } ] } diff --git a/examples/training.py b/examples/training.py index fdacfba2..6c37be83 100644 --- a/examples/training.py +++ b/examples/training.py @@ -42,25 +42,7 @@ def getitem(d, key): return reduce(operator.getitem, key, d) - -# This function signs data with PS256 using a private key -def sign_ps256(data: bytes, key: bytes) -> bytes: - private_key = serialization.load_pem_private_key( - key, - password=None, - ) - signature = private_key.sign( - data, - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA256() - ) - return signature - # First create an asset with a do not train assertion - # Define a manifest with the do not train assertion manifest_json = { "claim_generator_info": [{ @@ -72,19 +54,19 @@ def sign_ps256(data: bytes, key: bytes) -> bytes: "format": "image/jpeg", "identifier": "thumbnail" }, - "assertions": [ - { - "label": "c2pa.training-mining", - "data": { - "entries": { - "c2pa.ai_generative_training": { "use": "notAllowed" }, - "c2pa.ai_inference": { "use": "notAllowed" }, - "c2pa.ai_training": { "use": "notAllowed" }, - "c2pa.data_mining": { "use": "notAllowed" } + "assertions": [{ + "label": "cawg.training-mining", + "data": { + "entries": { + "cawg.ai_inference": { + "use": "notAllowed" + }, + "cawg.ai_generative_training": { + "use": "notAllowed" + } + } } - } - } - ] + }] } ingredient_json = { @@ -145,9 +127,11 @@ def sign_ps256(data: bytes, key: bytes) -> bytes: manifest_store = json.loads(reader.json()) manifest = manifest_store["manifests"][manifest_store["active_manifest"]] + for assertion in manifest["assertions"]: - if assertion["label"] == "c2pa.training-mining": - if getitem(assertion, ("data","entries","c2pa.ai_training","use")) == "notAllowed": + print(assertion) + if assertion["label"] == "cawg.training-mining": + if getitem(assertion, ("data","entries","cawg.ai_generative_training","use")) == "notAllowed": allowed = False # get the ingredient thumbnail and save it to a file using resource_to_stream From dc2022ab8a9e986337cb5800f0a00759be21d885 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 27 Jun 2025 19:46:44 -0700 Subject: [PATCH 2/8] fix: Add a v2 test --- tests/test_unit_tests.py | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index da43f599..4068e9ec 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -299,6 +299,33 @@ def setUp(self): ] } + # Define a V2 manifest as a dictionary + self.manifestDefinitionV2 = { + "claim_generator": "python_test", + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + "claim_version": 2, + "format": "image/jpeg", + "title": "Python Test Image V2", + "ingredients": [], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "parameters": { + } + } + ] + } + } + ] + } + # Define an example ES256 callback signer self.callback_signer_alg = "Es256" def callback_signer_es256(data: bytes) -> bytes: @@ -1198,6 +1225,31 @@ def error_callback_signer(data: bytes) -> bytes: finally: shutil.rmtree(temp_dir) + def test_signing_manifest_v2(self): + """Test signing and reading a V2 manifest. + V2 manifests have a slightly different structure. + """ + with open(self.testPath, "rb") as file: + # Create a builder with the V2 manifest definition using context manager + with Builder(self.manifestDefinitionV2) as builder: + output = io.BytesIO(bytearray()) + + # Sign as usual... + builder.sign(self.signer, "image/jpeg", file, output) + + output.seek(0) + + # Read the signed file and verify the manifest using context manager + with Reader("image/jpeg", output) as reader: + json_data = reader.json() + + # Basic verification of the manifest + self.assertIn("Python Test Image V2", json_data) + self.assertNotIn("validation_status", json_data) + + # Clean up + output.close() + class TestStream(unittest.TestCase): def setUp(self): # Create a temporary file for testing From d868db5a87388030d8cb04de67ec36beeea896f7 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 27 Jun 2025 19:49:54 -0700 Subject: [PATCH 3/8] fix: Add v2 callback signer test --- tests/test_unit_tests.py | 52 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 4068e9ec..1a03dbf7 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -1025,6 +1025,57 @@ def test_builder_sign_file_callback_signer_from_callback(self): finally: shutil.rmtree(temp_dir) + def test_builder_sign_file_callback_signer_from_callback_V2(self): + """Test signing a file using the sign_file method with Signer.from_callback.""" + + temp_dir = tempfile.mkdtemp() + try: + + output_path = os.path.join(temp_dir, "signed_output_from_callback.jpg") + + # Will use the sign_file method + builder = Builder(self.manifestDefinitionV2) + + # Create signer with callback using Signer.from_callback + signer = Signer.from_callback( + callback=self.callback_signer_es256, + alg=SigningAlg.ES256, + certs=self.certs.decode('utf-8'), + tsa_url="http://timestamp.digicert.com" + ) + + manifest_bytes = builder.sign_file( + source_path=self.testPath, + dest_path=output_path, + signer=signer + ) + + # Verify the output file was created + self.assertTrue(os.path.exists(output_path)) + + # Verify results + self.assertIsInstance(manifest_bytes, bytes) + self.assertGreater(len(manifest_bytes), 0) + + # Read the signed file and verify the manifest + with open(output_path, "rb") as file, Reader("image/jpeg", file) as reader: + json_data = reader.json() + self.assertIn("Python Test", json_data) + self.assertNotIn("validation_status", json_data) + + # Parse the JSON and verify the signature algorithm + manifest_data = json.loads(json_data) + active_manifest_id = manifest_data["active_manifest"] + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify the signature_info contains the correct algorithm + self.assertIn("signature_info", active_manifest) + signature_info = active_manifest["signature_info"] + self.assertEqual(signature_info["alg"], self.callback_signer_alg) + + finally: + shutil.rmtree(temp_dir) + def test_sign_file_using_callback_signer_overloads(self): """Test signing a file using the sign_file function with a Signer object.""" # Create a temporary directory for the test @@ -1247,7 +1298,6 @@ def test_signing_manifest_v2(self): self.assertIn("Python Test Image V2", json_data) self.assertNotIn("validation_status", json_data) - # Clean up output.close() class TestStream(unittest.TestCase): From 3a790f09deaa0ae7a4dc551734ec1dfce493271b Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 27 Jun 2025 19:59:29 -0700 Subject: [PATCH 4/8] fix: One more example --- examples/sign.py | 57 +++++++++++++----- examples/sign_using_signer_info.py | 92 ++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 examples/sign_using_signer_info.py diff --git a/examples/sign.py b/examples/sign.py index 6060d18a..7ff47e50 100644 --- a/examples/sign.py +++ b/examples/sign.py @@ -11,15 +11,18 @@ # each license. # This example shows how to sign an image with a C2PA manifest -# and read the metadata added to the image. +# using a callback signer and read the metadata added to the image. import os import c2pa +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.backends import default_backend fixtures_dir = os.path.join(os.path.dirname(__file__), "../tests/fixtures/") output_dir = os.path.join(os.path.dirname(__file__), "../output/") -# ensure the output directory exists +# Ensure the output directory exists if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -33,26 +36,43 @@ reader = c2pa.Reader("image/jpeg", file) print(reader.json()) -# Create a signer from certificate and key files +# Load certificates and private key (here from the test fixtures) +# This is OK for development, but in production you should use a +# secure way to load the certificates and private key. certs = open(fixtures_dir + "es256_certs.pem", "rb").read() key = open(fixtures_dir + "es256_private.key", "rb").read() -signer_info = c2pa.C2paSignerInfo( - alg=b"es256", # Use bytes instead of encoded string - sign_cert=certs, - private_key=key, - ta_url=b"http://timestamp.digicert.com" # Use bytes and add timestamp URL -) +# Define a callback signer function +def callback_signer_es256(data: bytes) -> bytes: + """Callback function that signs data using ES256 algorithm.""" + private_key = serialization.load_pem_private_key( + key, + password=None, + backend=default_backend() + ) + signature = private_key.sign( + data, + ec.ECDSA(hashes.SHA256()) + ) + return signature -signer = c2pa.Signer.from_info(signer_info) +# Create a signer using the callback function we defined +signer = c2pa.Signer.from_callback( + callback=callback_signer_es256, + alg=c2pa.C2paSigningAlg.ES256, + certs=certs.decode('utf-8'), + tsa_url="http://timestamp.digicert.com" +) # Create a manifest definition as a dictionary +# This manifest follows the V2 manifest format manifest_definition = { "claim_generator": "python_example", "claim_generator_info": [{ "name": "python_example", "version": "0.0.1", }], + "claim_version": 2, "format": "image/jpeg", "title": "Python Example Image", "ingredients": [], @@ -73,13 +93,20 @@ ] } +# Create the builder with the manifest definition builder = c2pa.Builder(manifest_definition) -# Sign the image -print("\nSigning the image...") -with open(fixtures_dir + "C.jpg", "rb") as source: - with open(output_dir + "C_signed.jpg", "wb") as dest: - result = builder.sign(signer, "image/jpeg", source, dest) +# Sign the image with the signer created above, +# which will use the callback signer +print("\nSigning the image file...") +builder.sign_file( + source_path=fixtures_dir + "C.jpg", + dest_path=output_dir + "C_signed.jpg", + signer=signer +) + +# Clean up the signer +signer.close() # Read the signed image to verify print("\nReading signed image metadata:") diff --git a/examples/sign_using_signer_info.py b/examples/sign_using_signer_info.py new file mode 100644 index 00000000..a7ba5f7b --- /dev/null +++ b/examples/sign_using_signer_info.py @@ -0,0 +1,92 @@ +# Copyright 2025 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, +# Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +# or the MIT license (http://opensource.org/licenses/MIT), +# at your option. +# Unless required by applicable law or agreed to in writing, +# this software is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +# implied. See the LICENSE-MIT and LICENSE-APACHE files for the +# specific language governing permissions and limitations under +# each license. + +# This example shows how to sign an image with a C2PA manifest +# and read the metadata added to the image. + +import os +import c2pa + +fixtures_dir = os.path.join(os.path.dirname(__file__), "../tests/fixtures/") +output_dir = os.path.join(os.path.dirname(__file__), "../output/") + +# Ensure the output directory exists +if not os.path.exists(output_dir): + os.makedirs(output_dir) + +print("c2pa version:") +version = c2pa.sdk_version() +print(version) + +# Read existing C2PA metadata from the file +print("\nReading existing C2PA metadata:") +with open(fixtures_dir + "C.jpg", "rb") as file: + reader = c2pa.Reader("image/jpeg", file) + print(reader.json()) + +# Create a signer from certificate and key files +certs = open(fixtures_dir + "es256_certs.pem", "rb").read() +key = open(fixtures_dir + "es256_private.key", "rb").read() + +signer_info = c2pa.C2paSignerInfo( + alg=b"es256", # Use bytes instead of encoded string + sign_cert=certs, + private_key=key, + ta_url=b"http://timestamp.digicert.com" # Use bytes and add timestamp URL +) + +signer = c2pa.Signer.from_info(signer_info) + +# Create a manifest definition as a dictionary +# This examples signs using a V1 manifest +manifest_definition = { + "claim_generator": "python_example", + "claim_generator_info": [{ + "name": "python_example", + "version": "0.0.1", + }], + "format": "image/jpeg", + "title": "Python Example Image", + "ingredients": [], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "parameters": { + # could hold additional information about this step + } + } + ] + } + } + ] +} + +builder = c2pa.Builder(manifest_definition) + +# Sign the image +print("\nSigning the image...") +with open(fixtures_dir + "C.jpg", "rb") as source: + with open(output_dir + "C_signed.jpg", "wb") as dest: + result = builder.sign(signer, "image/jpeg", source, dest) + +# Read the signed image to verify +print("\nReading signed image metadata:") +with open(output_dir + "C_signed.jpg", "rb") as file: + reader = c2pa.Reader("image/jpeg", file) + print(reader.json()) + +print("\nExample completed successfully!") + From 14ed825cf7d76878a90213b4e6ae307eb9353b24 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 27 Jun 2025 20:13:48 -0700 Subject: [PATCH 5/8] fix: More examples --- examples/README.md | 9 +++++++- examples/sign.py | 22 ++++++++++--------- ...sign_using_signer_info.py => sign_info.py} | 13 +++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) rename examples/{sign_using_signer_info.py => sign_info.py} (85%) diff --git a/examples/README.md b/examples/README.md index 8186aa48..60e3bc12 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,9 @@ The `examples/training.py` script demonstrates how to add a "Do Not Train" asser ### Signing and Verifying Assets -The `examples/sign.py` script shows how to sign an asset with a C2PA manifest and verify it. +The `examples/sign.py` script shows how to sign an asset with a C2PA manifest and verify it using a callback signer. Callback signers let you define signing logic, eg. where to load keys from. + +The `examples/sign_info.py` script shows how to sign an asset with a C2PA manifest and verify it using a "default" signer created with the needed signer information. ## Running the Examples @@ -21,6 +23,11 @@ Then you can run the examples with the following commands: python examples/training.py # Run the signing and verification example +# In this example, signing is done with a Signer created using SignerInfo +python examples/sign_info.py + +# Run the signing and verification example +# In this example, signing is done using a callback signer python examples/sign.py ``` diff --git a/examples/sign.py b/examples/sign.py index 7ff47e50..74fddb0e 100644 --- a/examples/sign.py +++ b/examples/sign.py @@ -19,6 +19,10 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.backends import default_backend +# Note: Builder, Reader, and Signer support being used as context managers +# (with 'with' statements), but this example shows manual usage which requires +# explicitly calling the close() function to clean up resources. + fixtures_dir = os.path.join(os.path.dirname(__file__), "../tests/fixtures/") output_dir = os.path.join(os.path.dirname(__file__), "../output/") @@ -30,11 +34,6 @@ version = c2pa.sdk_version() print(version) -# Read existing C2PA metadata from the file -print("\nReading existing C2PA metadata:") -with open(fixtures_dir + "C.jpg", "rb") as file: - reader = c2pa.Reader("image/jpeg", file) - print(reader.json()) # Load certificates and private key (here from the test fixtures) # This is OK for development, but in production you should use a @@ -85,6 +84,7 @@ def callback_signer_es256(data: bytes) -> bytes: "action": "c2pa.created", "parameters": { # could hold additional information about this step + # eg. model used, etc. } } ] @@ -100,19 +100,21 @@ def callback_signer_es256(data: bytes) -> bytes: # which will use the callback signer print("\nSigning the image file...") builder.sign_file( - source_path=fixtures_dir + "C.jpg", - dest_path=output_dir + "C_signed.jpg", + source_path=fixtures_dir + "A.jpg", + dest_path=output_dir + "A_signed.jpg", signer=signer ) -# Clean up the signer +# Clean up signer.close() +builder.close() -# Read the signed image to verify +# Re-Read the signed image to verify print("\nReading signed image metadata:") -with open(output_dir + "C_signed.jpg", "rb") as file: +with open(output_dir + "A_signed.jpg", "rb") as file: reader = c2pa.Reader("image/jpeg", file) print(reader.json()) + reader.close() print("\nExample completed successfully!") diff --git a/examples/sign_using_signer_info.py b/examples/sign_info.py similarity index 85% rename from examples/sign_using_signer_info.py rename to examples/sign_info.py index a7ba5f7b..eb177630 100644 --- a/examples/sign_using_signer_info.py +++ b/examples/sign_info.py @@ -19,6 +19,10 @@ fixtures_dir = os.path.join(os.path.dirname(__file__), "../tests/fixtures/") output_dir = os.path.join(os.path.dirname(__file__), "../output/") +# Note: Builder, Reader, and Signer support being used as context managers +# (with 'with' statements), but this example shows manual usage which requires +# explicitly calling the close() function to clean up resources. + # Ensure the output directory exists if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -32,11 +36,13 @@ with open(fixtures_dir + "C.jpg", "rb") as file: reader = c2pa.Reader("image/jpeg", file) print(reader.json()) + reader.close() # Create a signer from certificate and key files certs = open(fixtures_dir + "es256_certs.pem", "rb").read() key = open(fixtures_dir + "es256_private.key", "rb").read() +# Define Signer information signer_info = c2pa.C2paSignerInfo( alg=b"es256", # Use bytes instead of encoded string sign_cert=certs, @@ -44,6 +50,7 @@ ta_url=b"http://timestamp.digicert.com" # Use bytes and add timestamp URL ) +# Create the Signer from the information signer = c2pa.Signer.from_info(signer_info) # Create a manifest definition as a dictionary @@ -74,6 +81,7 @@ ] } +# Create the builder with the manifest definition builder = c2pa.Builder(manifest_definition) # Sign the image @@ -87,6 +95,11 @@ with open(output_dir + "C_signed.jpg", "rb") as file: reader = c2pa.Reader("image/jpeg", file) print(reader.json()) + reader.close() + +# Clean up resources manually, since we are not using with statements +signer.close() +builder.close() print("\nExample completed successfully!") From 1ab3c100d6804329d6adaf004844091ed8619873 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:22:03 -0700 Subject: [PATCH 6/8] fix: Remove debug log --- examples/training.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/training.py b/examples/training.py index 6c37be83..fbcf8cb3 100644 --- a/examples/training.py +++ b/examples/training.py @@ -129,7 +129,6 @@ def getitem(d, key): manifest = manifest_store["manifests"][manifest_store["active_manifest"]] for assertion in manifest["assertions"]: - print(assertion) if assertion["label"] == "cawg.training-mining": if getitem(assertion, ("data","entries","cawg.ai_generative_training","use")) == "notAllowed": allowed = False From 89329f3eb38fe9589ba9de16a3ad5e12d6724ff3 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:20:28 -0700 Subject: [PATCH 7/8] Update README.md --- examples/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index aa65211f..93134ab4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -80,7 +80,7 @@ Run the "do not train" assertion example: python examples/training.py ``` -### Run the signing and verification example: +### Run the signing and verification example In this example, signing is done with a Signer created using SignerInfo: @@ -88,7 +88,7 @@ In this example, signing is done with a Signer created using SignerInfo: python examples/sign_info.py ``` -### Run the signing and verification example +### Run the callback signing and verification example In this example, signing is done using a callback signer: From 384bcb3d2814babd015f6fc77c83f500e27c5dd3 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Mon, 11 Aug 2025 10:57:28 -0700 Subject: [PATCH 8/8] fix: Docs review comments --- examples/README.md | 6 +++--- examples/sign_info.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/README.md b/examples/README.md index 93134ab4..42a57d9c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,7 @@ The examples use asset files from the `tests/fixtures` directory, save the resul The [`examples/sign.py`](https://github.com/contentauth/c2pa-python/blob/main/examples/sign.py) script shows how to sign an asset with a C2PA manifest and verify the asset. -The `examples/sign.py` script shows how to sign an asset with a C2PA manifest and verify it using a callback signer. Callback signers let you define signing logic, eg. where to load keys from. +The `examples/sign.py` script shows how to sign an asset with a C2PA manifest and verify it using a callback signer. Callback signers let you define signing logic, for example where to load keys from. The `examples/sign_info.py` script shows how to sign an asset with a C2PA manifest and verify it using a "default" signer created with the needed signer information. These statements create a `builder` object with the specified manifest JSON (omitted in the snippet below), call `builder.sign()` to sign and attach the manifest to the source file, `tests/fixtures/C.jpg`, and save the signed asset to the output file, `output/C_signed.jpg`: @@ -82,7 +82,7 @@ python examples/training.py ### Run the signing and verification example -In this example, signing is done with a Signer created using SignerInfo: +In this example, `SignerInfo` creates a `Signer` object that signs the manifest. ```bash python examples/sign_info.py @@ -90,7 +90,7 @@ python examples/sign_info.py ### Run the callback signing and verification example -In this example, signing is done using a callback signer: +In this example, a callback signer is the signer: ```bash python examples/sign.py diff --git a/examples/sign_info.py b/examples/sign_info.py index eb177630..48fbd37a 100644 --- a/examples/sign_info.py +++ b/examples/sign_info.py @@ -10,6 +10,12 @@ # specific language governing permissions and limitations under # each license. +############################################################### +# This example shows an "older" way of signing, +# and is left here as reference. +# Please refer to sign.py for the recommended implementation. +############################################################### + # This example shows how to sign an image with a C2PA manifest # and read the metadata added to the image.