diff --git a/examples/README.md b/examples/README.md index 4e5b94ab..42a57d9c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,6 +8,9 @@ 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, 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`: ```py @@ -77,7 +80,17 @@ 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, `SignerInfo` creates a `Signer` object that signs the manifest. + +```bash +python examples/sign_info.py +``` + +### Run the callback signing and verification example + +In this example, a callback signer is the signer: ```bash python examples/sign.py diff --git a/examples/sign.py b/examples/sign.py index 82070b42..74fddb0e 100644 --- a/examples/sign.py +++ b/examples/sign.py @@ -11,15 +11,22 @@ # 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 + +# 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/") -# ensure the output directory exists +# Ensure the output directory exists if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -27,63 +34,87 @@ 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 +# 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": [], "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 + # eg. model used, etc. + } + } ] - }, - 'kind': 'Json' + } } ] } +# 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 + "A.jpg", + dest_path=output_dir + "A_signed.jpg", + signer=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_info.py b/examples/sign_info.py new file mode 100644 index 00000000..48fbd37a --- /dev/null +++ b/examples/sign_info.py @@ -0,0 +1,111 @@ +# 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 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. + +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/") + +# 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) + +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()) + 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, + private_key=key, + 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 +# 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 + } + } + ] + } + } + ] +} + +# 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) + +# 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()) + reader.close() + +# Clean up resources manually, since we are not using with statements +signer.close() +builder.close() + +print("\nExample completed successfully!") + diff --git a/examples/training.py b/examples/training.py index fdacfba2..fbcf8cb3 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,10 @@ 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": + 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 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 238b8ef9..e40c27c0 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: @@ -998,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 @@ -1198,6 +1276,30 @@ 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) + + output.close() + class TestStream(unittest.TestCase): def setUp(self): # Create a temporary file for testing