From 138ff74b3fbe9a5a41e5a2142ddf734ed60bf559 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:14:14 -0700 Subject: [PATCH 1/9] feat: c2pa-rs and c2pa_c_ffi 0.68.0 update (#189) * feat: Prepare 0.68.0 update * fix: Load trust for some tests * fix: Trust settings * fix: Update tests * fix: Restore deleted file * fix: Update the examples * fix: Clarify comment * fix: CLean up old tmp test * fix: Switch to json * fix: Add sign all files test with V2 spec * fix: Remove toml example --- c2pa-native-version.txt | 2 +- examples/sign.py | 10 +- examples/sign_info.py | 3 + pyproject.toml | 2 +- tests/__init__.py | 1 + tests/test_unit_tests.py | 626 +++++++++++++++++++++++--- tests/test_unit_tests_threaded.py | 10 - tests/trust_config_test_settings.json | 7 + 8 files changed, 582 insertions(+), 79 deletions(-) create mode 100644 tests/trust_config_test_settings.json diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index c9951732..1c50da98 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.67.1 +c2pa-v0.68.0 diff --git a/examples/sign.py b/examples/sign.py index 7182f99a..3df9fd5b 100644 --- a/examples/sign.py +++ b/examples/sign.py @@ -26,7 +26,7 @@ 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) @@ -43,7 +43,7 @@ with open(fixtures_dir + "es256_private.key", "rb") as key_file: key = key_file.read() -# Define a callback signer function +# 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( @@ -60,7 +60,6 @@ def callback_signer_es256(data: bytes) -> bytes: # 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", @@ -87,7 +86,7 @@ def callback_signer_es256(data: bytes) -> bytes: } # Sign the image with the signer created above, -# which will use the callback signer +# which will use the callback signer. print("\nSigning the image file...") with c2pa.Signer.from_callback( @@ -107,6 +106,9 @@ def callback_signer_es256(data: bytes) -> bytes: print("\nReading signed image metadata:") with open(output_dir + "A_signed.jpg", "rb") as file: with c2pa.Reader("image/jpeg", file) as reader: + # The validation state will depend on loaded trust settings. + # Without loaded trust settings, + # the manifest validation_state will be "Invalid". print(reader.json()) print("\nExample completed successfully!") diff --git a/examples/sign_info.py b/examples/sign_info.py index 0efa68d8..6b256647 100644 --- a/examples/sign_info.py +++ b/examples/sign_info.py @@ -100,6 +100,9 @@ print("\nReading signed image metadata:") with open(output_dir + "C_signed.jpg", "rb") as file: with c2pa.Reader("image/jpeg", file) as reader: + # The validation state will depend on loaded trust settings. + # Without loaded trust settings, + # the manifest validation_state will be "Invalid". print(reader.json()) print("\nExample completed successfully!") diff --git a/pyproject.toml b/pyproject.toml index d4722df1..dc370403 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "c2pa-python" -version = "0.27.1" +version = "0.28.0" requires-python = ">=3.10" description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library" readme = { file = "README.md", content-type = "text/markdown" } diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..867e2c84 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Placeholder \ No newline at end of file diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index e7f2d778..5bd5960a 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -23,6 +23,8 @@ import tempfile import shutil import ctypes +import toml +import threading # Suppress deprecation warnings warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -38,9 +40,34 @@ INGREDIENT_TEST_FILE = os.path.join(FIXTURES_DIR, INGREDIENT_TEST_FILE_NAME) ALTERNATIVE_INGREDIENT_TEST_FILE = os.path.join(FIXTURES_DIR, "cloud.jpg") + +def load_test_settings_json(): + """ + Load default trust configuration test settings from a + JSON config file and return its content as JSON-compatible dict. + The return value is used to load settings. + + Returns: + dict: The parsed JSON content as a Python dictionary (JSON-compatible). + + Raises: + FileNotFoundError: If trust_config_test_settings.json is not found. + json.JSONDecodeError: If the JSON file is malformed. + """ + # Locate the file which contains default settings for tests + tests_dir = os.path.dirname(os.path.abspath(__file__)) + settings_path = os.path.join(tests_dir, 'trust_config_test_settings.json') + + # Load the located default test settings + with open(settings_path, 'r') as f: + settings_data = json.load(f) + + return settings_data + + class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.67.1", sdk_version()) + self.assertIn("0.68.0", sdk_version()) class TestReader(unittest.TestCase): @@ -122,7 +149,44 @@ def test_stream_read_get_validation_state(self): reader = Reader("image/jpeg", file) validation_state = reader.get_validation_state() self.assertIsNotNone(validation_state) - self.assertEqual(validation_state, "Valid") + # Needs trust configuration to be set up to validate as Trusted, otherwise manifest is Invalid + self.assertEqual(validation_state, "Invalid") + + def test_stream_read_get_validation_state_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def read_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(self.testPath, "rb") as file: + reader = Reader("image/jpeg", file) + validation_state = reader.get_validation_state() + result['validation_state'] = validation_state + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=read_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIsNotNone(result.get('validation_state')) + # With trust configuration loaded, manifest is Trusted + self.assertEqual(result.get('validation_state'), "Trusted") def test_stream_read_get_validation_results(self): with open(self.testPath, "rb") as file: @@ -937,7 +1001,6 @@ def test_streams_sign_with_thumbnail_resource(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) output.close() def test_streams_sign_with_es256_alg_v1_manifest(self): @@ -949,7 +1012,10 @@ def test_streams_sign_with_es256_alg_v1_manifest(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) # Write buffer to file # output.seek(0) @@ -974,7 +1040,10 @@ def test_streams_sign_with_es256_alg_v1_manifest_to_existing_empty_file(self): reader = Reader("image/jpeg", target) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) finally: # Clean up... @@ -1000,7 +1069,10 @@ def test_streams_sign_with_es256_alg_v1_manifest_to_new_dest_file(self): reader = Reader("image/jpeg", target) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) finally: # Clean up... @@ -1022,7 +1094,10 @@ def test_streams_sign_with_es256_alg(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() def test_streams_sign_with_es256_alg_2(self): @@ -1034,9 +1109,60 @@ def test_streams_sign_with_es256_alg_2(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() + def test_streams_sign_with_es256_alg_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def sign_and_validate_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(self.testPath, "rb") as file: + builder = Builder(self.manifestDefinitionV2) + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + + # Get validation state with trust config + validation_state = reader.get_validation_state() + + result['json_data'] = json_data + result['validation_state'] = validation_state + output.close() + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=sign_and_validate_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIn("Python Test", result.get('json_data', '')) + # With trust configuration loaded, validation should return "Trusted" + self.assertIsNotNone(result.get('validation_state')) + self.assertEqual(result.get('validation_state'), "Trusted") + + def test_sign_with_ed25519_alg(self): with open(os.path.join(self.data_dir, "ed25519.pub"), "rb") as cert_file: certs = cert_file.read() @@ -1059,9 +1185,72 @@ def test_sign_with_ed25519_alg(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() + def test_sign_with_ed25519_alg_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def sign_and_validate_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(os.path.join(self.data_dir, "ed25519.pub"), "rb") as cert_file: + certs = cert_file.read() + with open(os.path.join(self.data_dir, "ed25519.pem"), "rb") as key_file: + key = key_file.read() + + signer_info = C2paSignerInfo( + alg=b"ed25519", + sign_cert=certs, + private_key=key, + ta_url=b"http://timestamp.digicert.com" + ) + signer = Signer.from_info(signer_info) + + with open(self.testPath, "rb") as file: + builder = Builder(self.manifestDefinitionV2) + output = io.BytesIO(bytearray()) + builder.sign(signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + + # Get validation state with trust config + validation_state = reader.get_validation_state() + + result['json_data'] = json_data + result['validation_state'] = validation_state + output.close() + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=sign_and_validate_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIn("Python Test", result.get('json_data', '')) + # With trust configuration loaded, validation should return "Trusted" + self.assertIsNotNone(result.get('validation_state')) + self.assertEqual(result.get('validation_state'), "Trusted") + def test_sign_with_ed25519_alg_2(self): with open(os.path.join(self.data_dir, "ed25519.pub"), "rb") as cert_file: certs = cert_file.read() @@ -1084,7 +1273,10 @@ def test_sign_with_ed25519_alg_2(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() def test_sign_with_ps256_alg(self): @@ -1109,7 +1301,10 @@ def test_sign_with_ps256_alg(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() def test_sign_with_ps256_alg_2(self): @@ -1134,9 +1329,70 @@ def test_sign_with_ps256_alg_2(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted + # self.assertNotIn("validation_status", json_data) output.close() + def test_sign_with_ps256_alg_2_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def sign_and_validate_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(os.path.join(self.data_dir, "ps256.pub"), "rb") as cert_file: + certs = cert_file.read() + with open(os.path.join(self.data_dir, "ps256.pem"), "rb") as key_file: + key = key_file.read() + + signer_info = C2paSignerInfo( + alg=b"ps256", + sign_cert=certs, + private_key=key, + ta_url=b"http://timestamp.digicert.com" + ) + signer = Signer.from_info(signer_info) + + with open(self.testPath2, "rb") as file: + builder = Builder(self.manifestDefinitionV2) + output = io.BytesIO(bytearray()) + builder.sign(signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + + # Get validation state with trust config + validation_state = reader.get_validation_state() + + result['json_data'] = json_data + result['validation_state'] = validation_state + output.close() + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=sign_and_validate_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIn("Python Test", result.get('json_data', '')) + # With trust configuration loaded, validation should return "Trusted" + self.assertIsNotNone(result.get('validation_state')) + self.assertEqual(result.get('validation_state'), "Trusted") + def test_archive_sign(self): with open(self.testPath, "rb") as file: builder = Builder(self.manifestDefinition) @@ -1149,10 +1405,64 @@ def test_archive_sign(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) archive.close() output.close() + def test_archive_sign_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def sign_and_validate_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(self.testPath, "rb") as file: + builder = Builder(self.manifestDefinition) + archive = io.BytesIO(bytearray()) + builder.to_archive(archive) + builder = Builder.from_archive(archive) + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + + # Get validation state with trust config + validation_state = reader.get_validation_state() + + result['json_data'] = json_data + result['validation_state'] = validation_state + archive.close() + output.close() + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=sign_and_validate_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIn("Python Test", result.get('json_data', '')) + # With trust configuration loaded, validation should return "Trusted" + self.assertIsNotNone(result.get('validation_state')) + self.assertEqual(result.get('validation_state'), "Trusted") + def test_archive_sign_with_added_ingredient(self): with open(self.testPath, "rb") as file: builder = Builder(self.manifestDefinitionV2) @@ -1168,10 +1478,67 @@ def test_archive_sign_with_added_ingredient(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) archive.close() output.close() + def test_archive_sign_with_added_ingredient_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def sign_and_validate_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(self.testPath, "rb") as file: + builder = Builder(self.manifestDefinitionV2) + archive = io.BytesIO(bytearray()) + builder.to_archive(archive) + builder = Builder.from_archive(archive) + output = io.BytesIO(bytearray()) + ingredient_json = '{"test": "ingredient"}' + with open(self.testPath, 'rb') as f: + builder.add_ingredient(ingredient_json, "image/jpeg", f) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + + # Get validation state with trust config + validation_state = reader.get_validation_state() + + result['json_data'] = json_data + result['validation_state'] = validation_state + archive.close() + output.close() + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=sign_and_validate_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIn("Python Test", result.get('json_data', '')) + # With trust configuration loaded, validation should return "Trusted" + self.assertIsNotNone(result.get('validation_state')) + self.assertEqual(result.get('validation_state'), "Trusted") + def test_remote_sign(self): with open(self.testPath, "rb") as file: builder = Builder(self.manifestDefinition) @@ -1199,7 +1566,6 @@ def test_remote_sign_using_returned_bytes(self): with Reader("image/jpeg", read_buffer, manifest_data) as reader: manifest_data = reader.json() self.assertIn("Python Test", manifest_data) - self.assertNotIn("validation_status", manifest_data) def test_remote_sign_using_returned_bytes_V2(self): with open(self.testPath, "rb") as file: @@ -1214,7 +1580,56 @@ def test_remote_sign_using_returned_bytes_V2(self): with Reader("image/jpeg", read_buffer, manifest_data) as reader: manifest_data = reader.json() self.assertIn("Python Test", manifest_data) - self.assertNotIn("validation_status", manifest_data) + + def test_remote_sign_using_returned_bytes_V2_with_trust_config(self): + # Run in a separate thread to isolate thread-local settings + result = {} + exception = {} + + def sign_and_validate_with_trust_config(): + try: + # Load trust configuration from test_settings.toml + settings_dict = load_test_settings_json() + + # Apply the settings (including trust configuration) + # Settings are thread-local, so they won't affect other tests + # And that is why we also run the test in its own thread, so tests are isolated + load_settings(settings_dict) + + with open(self.testPath, "rb") as file: + builder = Builder(self.manifestDefinitionV2) + builder.set_no_embed() + with io.BytesIO() as output_buffer: + manifest_data = builder.sign( + self.signer, "image/jpeg", file, output_buffer) + output_buffer.seek(0) + read_buffer = io.BytesIO(output_buffer.getvalue()) + + with Reader("image/jpeg", read_buffer, manifest_data) as reader: + json_data = reader.json() + + # Get validation state with trust config + validation_state = reader.get_validation_state() + + result['json_data'] = json_data + result['validation_state'] = validation_state + except Exception as e: + exception['error'] = e + + # Create and start thread + thread = threading.Thread(target=sign_and_validate_with_trust_config) + thread.start() + thread.join() + + # Check for exceptions + if 'error' in exception: + raise exception['error'] + + # Assertions run in main thread + self.assertIn("Python Test", result.get('json_data', '')) + # With trust configuration loaded, validation should return "Trusted" + self.assertIsNotNone(result.get('validation_state')) + self.assertEqual(result.get('validation_state'), "Trusted") def test_sign_all_files(self): """Test signing all files in both fixtures directories""" @@ -1273,7 +1688,78 @@ def test_sign_all_files(self): reader = Reader(mime_type, output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) + reader.close() + output.close() + except Error.NotSupported: + continue + except Exception as e: + self.fail(f"Failed to sign {filename}: {str(e)}") + + def test_sign_all_files_V2(self): + """Test signing all files in both fixtures directories""" + signing_dir = os.path.join(self.data_dir, "files-for-signing-tests") + reading_dir = os.path.join(self.data_dir, "files-for-reading-tests") + + # Map of file extensions to MIME types + mime_types = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.heic': 'image/heic', + '.heif': 'image/heif', + '.avif': 'image/avif', + '.tif': 'image/tiff', + '.tiff': 'image/tiff', + '.mp4': 'video/mp4', + '.avi': 'video/x-msvideo', + '.mp3': 'audio/mpeg', + '.m4a': 'audio/mp4', + '.wav': 'audio/wav' + } + + # Skip files that are known to be invalid or unsupported + skip_files = { + 'sample3.invalid.wav', # Invalid file + } + + # Process both directories + for directory in [signing_dir, reading_dir]: + for filename in os.listdir(directory): + if filename in skip_files: + continue + + file_path = os.path.join(directory, filename) + if not os.path.isfile(file_path): + continue + + # Get file extension and corresponding MIME type + _, ext = os.path.splitext(filename) + ext = ext.lower() + if ext not in mime_types: + continue + + mime_type = mime_types[ext] + + try: + with open(file_path, "rb") as file: + builder = Builder(self.manifestDefinitionV2) + output = io.BytesIO(bytearray()) + builder.sign(self.signer, mime_type, file, output) + builder.close() + output.seek(0) + reader = Reader(mime_type, output) + json_data = reader.json() + self.assertIn("Python Test", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) reader.close() output.close() except Error.NotSupported: @@ -1804,7 +2290,10 @@ def test_sign_single(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` (which makes the manifest Invalid) + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() def test_sign_mp4_video_file_single(self): @@ -1819,7 +2308,10 @@ def test_sign_mp4_video_file_single(self): reader = Reader("video/mp4", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() def test_sign_mov_video_file_single(self): @@ -1834,37 +2326,12 @@ def test_sign_mov_video_file_single(self): reader = Reader("mov", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() - def test_sign_file_tmn_wip(self): - temp_dir = tempfile.mkdtemp() - try: - # Create a temporary output file path - output_path = os.path.join(temp_dir, "signed_output.jpg") - - # Use the sign_file method - builder = Builder(self.manifestDefinition) - builder.sign_file( - self.testPath, - output_path, - self.signer - ) - - # Verify the output file was created - self.assertTrue(os.path.exists(output_path)) - - # Read the signed file and verify the manifest - with open(output_path, "rb") as file: - reader = Reader("image/jpeg", file) - json_data = reader.json() - self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) - - finally: - # Clean up the temporary directory - shutil.rmtree(temp_dir) - def test_sign_file_video(self): temp_dir = tempfile.mkdtemp() try: @@ -1887,7 +2354,10 @@ def test_sign_file_video(self): reader = Reader("video/mp4", file) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) finally: # Clean up the temporary directory @@ -1939,7 +2409,10 @@ def test_builder_sign_file_callback_signer_from_callback(self): 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) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -1990,7 +2463,10 @@ def test_builder_sign_file_callback_signer_from_callback_V2(self): 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) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -2043,7 +2519,10 @@ def ed25519_callback(data: bytes) -> bytes: reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) reader.close() output.close() @@ -2067,7 +2546,10 @@ def test_signing_manifest_v2(self): # Basic verification of the manifest self.assertIn("Python Test Image V2", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) output.close() @@ -2100,7 +2582,10 @@ def test_sign_file_mp4_video(self): reader = Reader("video/mp4", file) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) finally: # Clean up the temporary directory @@ -2128,13 +2613,19 @@ def test_sign_file_mov_video(self): reader = Reader("mov", file) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) # Verify also signed file using manifest bytes with Reader("mov", output_path, manifest_bytes) as reader: json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) finally: # Clean up the temporary directory @@ -2162,13 +2653,19 @@ def test_sign_file_mov_video_V2(self): reader = Reader("mov", file) json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) # Verify also signed file using manifest bytes with Reader("mov", output_path, manifest_bytes) as reader: json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertIn("Invalid", json_data) finally: # Clean up the temporary directory @@ -3516,7 +4013,6 @@ def test_sign_file_using_callback_signer_overloads(self): reader = Reader("image/jpeg", file) file_manifest_json = reader.json() self.assertIn("Python Test", file_manifest_json) - self.assertNotIn("validation_status", file_manifest_json) finally: shutil.rmtree(temp_dir) @@ -3687,7 +4183,8 @@ def test_sign_file_callback_signer(self): # 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.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted + # self.assertNotIn("validation_status", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -3736,7 +4233,8 @@ def test_sign_file_callback_signer(self): # 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.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted + # self.assertNotIn("validation_status", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -3782,7 +4280,8 @@ def test_sign_file_callback_signer_managed_single(self): with Reader("image/jpeg", file) as reader: json_data = reader.json() self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # Needs trust configuration to be set up to validate as Trusted + # self.assertNotIn("validation_status", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -3843,7 +4342,8 @@ def test_sign_file_callback_signer_managed_multiple_uses(self): 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) + # Needs trust configuration to be set up to validate as Trusted + # self.assertNotIn("validation_status", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) diff --git a/tests/test_unit_tests_threaded.py b/tests/test_unit_tests_threaded.py index 9a00dd6a..14ef48fe 100644 --- a/tests/test_unit_tests_threaded.py +++ b/tests/test_unit_tests_threaded.py @@ -1429,10 +1429,6 @@ def sign_file(output_stream, manifest_def, thread_id): active_manifest1["title"], active_manifest2["title"]) - # Verify both outputs have valid signatures - self.assertNotIn("validation_status", manifest_store1) - self.assertNotIn("validation_status", manifest_store2) - # Clean up output1.close() output2.close() @@ -1511,12 +1507,6 @@ async def read_manifest(): self.assertTrue(author_found, "Author assertion not found in manifest") - # Verify no validation errors - self.assertNotIn( - "validation_status", - manifest_store, - "Manifest should not have validation errors") - except Exception as e: read_errors.append(f"Read error: {str(e)}") diff --git a/tests/trust_config_test_settings.json b/tests/trust_config_test_settings.json new file mode 100644 index 00000000..82422751 --- /dev/null +++ b/tests/trust_config_test_settings.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "trust": { + "trust_anchors": "-----BEGIN CERTIFICATE-----\nMIICEzCCAcWgAwIBAgIUW4fUnS38162x10PCnB8qFsrQuZgwBQYDK2VwMHcxCzAJ\nBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29tZXdoZXJlMRowGAYD\nVQQKDBFDMlBBIFRlc3QgUm9vdCBDQTEZMBcGA1UECwwQRk9SIFRFU1RJTkdfT05M\nWTEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yMjA2MTAxODQ2NDFaFw0zMjA2MDcxODQ2\nNDFaMHcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29tZXdo\nZXJlMRowGAYDVQQKDBFDMlBBIFRlc3QgUm9vdCBDQTEZMBcGA1UECwwQRk9SIFRF\nU1RJTkdfT05MWTEQMA4GA1UEAwwHUm9vdCBDQTAqMAUGAytlcAMhAGPUgK9q1H3D\neKMGqLGjTXJSpsrLpe0kpxkaFMe7KUAuo2MwYTAdBgNVHQ4EFgQUXuZWArP1jiRM\nfgye6ZqRyGupTowwHwYDVR0jBBgwFoAUXuZWArP1jiRMfgye6ZqRyGupTowwDwYD\nVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwBQYDK2VwA0EA8E79g54u2fUy\ndfVLPyqKmtjenOUMvVQD7waNbetLY7kvUJZCd5eaDghk30/Q1RaNjiP/2RfA/it8\nzGxQnM2hCA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIC2jCCAjygAwIBAgIUYm+LFaltpWbS9kED6RRAamOdUHowCgYIKoZIzj0EAwQw\ndzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUx\nGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElO\nR19PTkxZMRAwDgYDVQQDDAdSb290IENBMB4XDTIyMDYxMDE4NDY0MFoXDTMyMDYw\nNzE4NDY0MFowdzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlT\nb21ld2hlcmUxGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBG\nT1IgVEVTVElOR19PTkxZMRAwDgYDVQQDDAdSb290IENBMIGbMBAGByqGSM49AgEG\nBSuBBAAjA4GGAAQBaifSYJBkf5fgH3FWPxRdV84qwIsLd7RcIDcRJrRkan0xUYP5\nzco7R4fFGaQ9YJB8dauyqiNg00LVuPajvKmhgEMAT4eSfEhYC25F2ggXQlBIK3Q7\nmkXwJTIJSObnbw4S9Jy3W6OVKq351VpgWUcmhvGRRejW7S/D8L2tzqRW7JPI2uSj\nYzBhMB0GA1UdDgQWBBS6OykommTmfYoLJuPN4OU83wjPqjAfBgNVHSMEGDAWgBS6\nOykommTmfYoLJuPN4OU83wjPqjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE\nAwIBhjAKBggqhkjOPQQDBAOBiwAwgYcCQV4B6uKKoCWecEDlzj2xQLFPmnBQIOzD\nnyiSEcYyrCKwMV+HYS39oM+T53NvukLKUTznHwdWc9++HNaqc+IjsDl6AkIB2lXd\n5+s3xf0ioU91GJ4E13o5rpAULDxVSrN34A7BlsaXYQLnSkLMqva6E7nq2JBYjkqf\niwNQm1DDcQPtPTnddOs=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICkTCCAhagAwIBAgIUIngKvNC/BMF3TRIafgweprIbGgAwCgYIKoZIzj0EAwMw\ndzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUx\nGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElO\nR19PTkxZMRAwDgYDVQQDDAdSb290IENBMB4XDTIyMDYxMDE4NDY0MFoXDTMyMDYw\nNzE4NDY0MFowdzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlT\nb21ld2hlcmUxGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBG\nT1IgVEVTVElOR19PTkxZMRAwDgYDVQQDDAdSb290IENBMHYwEAYHKoZIzj0CAQYF\nK4EEACIDYgAEX3FzSTnCcEAP3wteNaiy4GZzZ+ABd2Y7gJpfyZf3kkCuX/I3psFq\nQBRvb3/FEBaDT4VbDNlZ0WLwtw5d3PI42Zufgpxemgfjf31d8H51eU3/IfAz5AFX\ny/OarhObHgVvo2MwYTAdBgNVHQ4EFgQUe+FK5t6/bQGIcGY6kkeIKTX/bJ0wHwYD\nVR0jBBgwFoAUe+FK5t6/bQGIcGY6kkeIKTX/bJ0wDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAPOgmJbVdhDh9KlgQXqE\nFzHiCt347JG4strk22MXzOgxQ0LnXStIh+viC3S1INzuBgIxAI1jiUBX/V7Gg0y6\nY/p6a63Xp2w+ia7vlUaUBWsR3ex9NNSTPLNoDkoTCSDOE2O20w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICUzCCAfmgAwIBAgIUdmkq4byvgk2FSnddHqB2yjoD68gwCgYIKoZIzj0EAwIw\ndzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUx\nGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElO\nR19PTkxZMRAwDgYDVQQDDAdSb290IENBMB4XDTIyMDYxMDE4NDY0MFoXDTMyMDYw\nNzE4NDY0MFowdzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlT\nb21ld2hlcmUxGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBG\nT1IgVEVTVElOR19PTkxZMRAwDgYDVQQDDAdSb290IENBMFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEre/KpcWwGEHt+mD4xso3xotRnRx2IEsMoYwVIKI7iEJrDEye\nPcvJuBywA0qiMw2yvAvGOzW/fqUTu1jABrFIk6NjMGEwHQYDVR0OBBYEFF6ZuIbh\neBvZVxVadQBStikOy6iMMB8GA1UdIwQYMBaAFF6ZuIbheBvZVxVadQBStikOy6iM\nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0gA\nMEUCIHBC1xLwkCWSGhVXFlSnQBx9cGZivXzCbt8BuwRqPSUoAiEAteZQDk685yh9\njgOTkp4H8oAmM1As+qlkRK2b+CHAQ3k=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGezCCBC+gAwIBAgIUDAG5+sfGspprX+hlkn1SuB2f5VQwQQYJKoZIhvcNAQEK\nMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF\nAKIDAgEgMHcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29t\nZXdoZXJlMRowGAYDVQQKDBFDMlBBIFRlc3QgUm9vdCBDQTEZMBcGA1UECwwQRk9S\nIFRFU1RJTkdfT05MWTEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yMjA2MTAxODQ2MjVa\nFw0zMjA2MDcxODQ2MjVaMHcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAG\nA1UEBwwJU29tZXdoZXJlMRowGAYDVQQKDBFDMlBBIFRlc3QgUm9vdCBDQTEZMBcG\nA1UECwwQRk9SIFRFU1RJTkdfT05MWTEQMA4GA1UEAwwHUm9vdCBDQTCCAlYwQQYJ\nKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglg\nhkgBZQMEAgEFAKIDAgEgA4ICDwAwggIKAoICAQC4q3t327HRHDs7Y9NR+ZqernwU\nbZ1EiEBR8vKTZ9StXmSfkzgSnvVfsFanvrKuZvFIWq909t/gH2z0klI2ZtChwLi6\nTFYXQjzQt+x5CpRcdWnB9zfUhOpdUHAhRd03Q14H2MyAiI98mqcVreQOiLDydlhP\nDla7Ign4PqedXBH+NwUCEcbQIEr2LvkZ5fzX1GzBtqymClT/Gqz75VO7zM1oV4gq\nElFHLsTLgzv5PR7pydcHauoTvFWhZNgz5s3olXJDKG/n3h0M3vIsjn11OXkcwq99\nNe5Nm9At2tC1w0Huu4iVdyTLNLIAfM368ookf7CJeNrVJuYdERwLwICpetYvOnid\nVTLSDt/YK131pR32XCkzGnrIuuYBm/k6IYgNoWqUhojGJai6o5hI1odAzFIWr9T0\nsa9f66P6RKl4SUqa/9A/uSS8Bx1gSbTPBruOVm6IKMbRZkSNN/O8dgDa1OftYCHD\nblCCQh9DtOSh6jlp9I6iOUruLls7d4wPDrstPefi0PuwsfWAg4NzBtQ3uGdzl/lm\nyusq6g94FVVq4RXHN/4QJcitE9VPpzVuP41aKWVRM3X/q11IH80rtaEQt54QMJwi\nsIv4eEYW3TYY9iQtq7Q7H9mcz60ClJGYQJvd1DR7lA9LtUrnQJIjNY9v6OuHVXEX\nEFoDH0viraraHozMdwIDAQABo2MwYTAdBgNVHQ4EFgQURW8b4nQuZgIteSw5+foy\nTZQrGVAwHwYDVR0jBBgwFoAURW8b4nQuZgIteSw5+foyTZQrGVAwDwYDVR0TAQH/\nBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwQQYJKoZIhvcNAQEKMDSgDzANBglghkgB\nZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgA4ICAQBB\nWnUOG/EeQoisgC964H5+ns4SDIYFOsNeksJM3WAd0yG2L3CEjUksUYugQzB5hgh4\nBpsxOajrkKIRxXN97hgvoWwbA7aySGHLgfqH1vsGibOlA5tvRQX0WoQ+GMnuliVM\npLjpHdYE2148DfgaDyIlGnHpc4gcXl7YHDYcvTN9NV5Y4P4x/2W/Lh11NC/VOSM9\naT+jnFE7s7VoiRVfMN2iWssh2aihecdE9rs2w+Wt/E/sCrVClCQ1xaAO1+i4+mBS\na7hW+9lrQKSx2bN9c8K/CyXgAcUtutcIh5rgLm2UWOaB9It3iw0NVaxwyAgWXC9F\nqYJsnia4D3AP0TJL4PbpNUaA4f2H76NODtynMfEoXSoG3TYYpOYKZ65lZy3mb26w\nfvBfrlASJMClqdiEFHfGhP/dTAZ9eC2cf40iY3ta84qSJybSYnqst8Vb/Gn+dYI9\nqQm0yVHtJtvkbZtgBK5Vg6f5q7I7DhVINQJUVlWzRo6/Vx+/VBz5tC5aVDdqtBAs\nq6ZcYS50ECvK/oGnVxjpeOafGvaV2UroZoGy7p7bEoJhqOPrW2yZ4JVNp9K6CCRg\nzR6jFN/gUe42P1lIOfcjLZAM1GHixtjP5gLAp6sJS8X05O8xQRBtnOsEwNLj5w0y\nMAdtwAzT/Vfv7b08qfx4FfQPFmtjvdu4s82gNatxSA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\nxPd7wFhjRZHfuWb2cs63xjAGjQ==\n-----END CERTIFICATE-----\n", + "trust_config": "//id-kp-emailProtection\n1.3.6.1.5.5.7.3.4\n//id-kp-documentSigning\n1.3.6.1.5.5.7.3.36\n//id-kp-timeStamping\n1.3.6.1.5.5.7.3.8\n//id-kp-OCSPSigning\n1.3.6.1.5.5.7.3.9\n// MS C2PA Signing\n1.3.6.1.4.1.311.76.59.1.9\n// c2pa-kp-claimSigning\n1.3.6.1.4.1.62558.2.1\n" + } +} \ No newline at end of file From b272adbee2aa25a75a2bed10e864f3a0497ee0a2 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Wed, 5 Nov 2025 08:21:03 -0800 Subject: [PATCH 2/9] chore: Update c2pa version from v0.68.0 to v0.69.0 (#190) * Update c2pa version from v0.68.0 to v0.69.0 * fix: Test version number check --- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index 1c50da98..52ba598b 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.68.0 +c2pa-v0.69.0 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 5bd5960a..488c5901 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.68.0", sdk_version()) + self.assertIn("0.69.0", sdk_version()) class TestReader(unittest.TestCase): From 43b460e23febf4767a436686dc72e883b31caa53 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:07:23 -0800 Subject: [PATCH 3/9] fix: Version bump from c2pa-rs and c2pa-c-ffi from v0.69.0 to v0.70.0 (#191) --- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index 52ba598b..3fecf232 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.69.0 +c2pa-v0.70.0 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 488c5901..a97c1ba7 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.69.0", sdk_version()) + self.assertIn("0.70.0", sdk_version()) class TestReader(unittest.TestCase): From e14197871e599aa069f8cb5140a7e34c3fcb3b6c Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:59:51 -0800 Subject: [PATCH 4/9] fix: Version bump from c2pa-rs v0.70.0 to v0.71.0 (#193) --- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index 3fecf232..b002316e 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.70.0 +c2pa-v0.71.0 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index a97c1ba7..97b5bb8a 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.70.0", sdk_version()) + self.assertIn("0.71.0", sdk_version()) class TestReader(unittest.TestCase): From 4ed3ce84c7c1b77200443ea2b89f53d1d1db4c0d Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:55:50 -0800 Subject: [PATCH 5/9] feat: Add set_intent APIs (#194) * fix: Version bump * fix: New bindings * fix: Initial bindings * fix: Few more tests * fix: Add a test * fix: Renamings * chore: c2pa-v0.71.1 * chore: c2pa-v0.71.1 * fix: Update tests * fix: Clean up logs * fix: Clean up logs from native lib --- c2pa-native-version.txt | 2 +- src/c2pa/__init__.py | 4 + src/c2pa/c2pa.py | 77 +++++++++ tests/test_unit_tests.py | 331 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 402 insertions(+), 12 deletions(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index b002316e..ff0544b3 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.71.0 +c2pa-v0.71.2 diff --git a/src/c2pa/__init__.py b/src/c2pa/__init__.py index c2a89d73..4791b225 100644 --- a/src/c2pa/__init__.py +++ b/src/c2pa/__init__.py @@ -22,6 +22,8 @@ C2paError, Reader, C2paSigningAlg, + C2paDigitalSourceType, + C2paBuilderIntent, C2paSignerInfo, Signer, Stream, @@ -35,6 +37,8 @@ 'C2paError', 'Reader', 'C2paSigningAlg', + 'C2paDigitalSourceType', + 'C2paBuilderIntent', 'C2paSignerInfo', 'Signer', 'Stream', diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index b0252279..82cad35c 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -48,6 +48,7 @@ 'c2pa_builder_free', 'c2pa_builder_set_no_embed', 'c2pa_builder_set_remote_url', + 'c2pa_builder_set_intent', 'c2pa_builder_add_resource', 'c2pa_builder_add_ingredient_from_stream', 'c2pa_builder_add_action', @@ -158,6 +159,37 @@ class C2paSigningAlg(enum.IntEnum): ED25519 = 6 +class C2paDigitalSourceType(enum.IntEnum): + """List of possible digital source types.""" + EMPTY = 0 + TRAINED_ALGORITHMIC_DATA = 1 + DIGITAL_CAPTURE = 2 + COMPUTATIONAL_CAPTURE = 3 + NEGATIVE_FILM = 4 + POSITIVE_FILM = 5 + PRINT = 6 + HUMAN_EDITS = 7 + COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA = 8 + ALGORITHMICALLY_ENHANCED = 9 + DIGITAL_CREATION = 10 + DATA_DRIVEN_MEDIA = 11 + TRAINED_ALGORITHMIC_MEDIA = 12 + ALGORITHMIC_MEDIA = 13 + SCREEN_CAPTURE = 14 + VIRTUAL_RECORDING = 15 + COMPOSITE = 16 + COMPOSITE_CAPTURE = 17 + COMPOSITE_SYNTHETIC = 18 + + +class C2paBuilderIntent(enum.IntEnum): + """Builder intent enumeration. + """ + CREATE = 0 # New digital creation with specified digital source type + EDIT = 1 # Edit of a pre-existing parent asset + UPDATE = 2 # Restricted version of Edit for non-editorial changes + + # Mapping from C2paSigningAlg enum to string representation, # as the enum value currently maps by default to an integer value. _ALG_TO_STRING_BYTES_MAPPING = { @@ -258,6 +290,7 @@ def _clear_error_state(): # Free the error to clear the state _lib.c2pa_string_free(error) + class C2paSignerInfo(ctypes.Structure): """Configuration for a Signer.""" _fields_ = [ @@ -414,6 +447,10 @@ def _setup_function(func, argtypes, restype=None): _setup_function( _lib.c2pa_builder_set_remote_url, [ ctypes.POINTER(C2paBuilder), ctypes.c_char_p], ctypes.c_int) +_setup_function( + _lib.c2pa_builder_set_intent, + [ctypes.POINTER(C2paBuilder), ctypes.c_uint, ctypes.c_uint], + ctypes.c_int) _setup_function(_lib.c2pa_builder_add_resource, [ctypes.POINTER( C2paBuilder), ctypes.c_char_p, ctypes.POINTER(C2paStream)], ctypes.c_int) _setup_function(_lib.c2pa_builder_add_ingredient_from_stream, @@ -2570,6 +2607,46 @@ def set_remote_url(self, remote_url: str): raise C2paError( Builder._ERROR_MESSAGES['url_error'].format("Unknown error")) + def set_intent( + self, + intent: C2paBuilderIntent, + digital_source_type: C2paDigitalSourceType = ( + C2paDigitalSourceType.EMPTY + ) + ): + """Set the intent for the manifest. + + The intent specifies what kind of manifest to create: + - CREATE: New with specified digital source type. + Must not have a parent ingredient. + - EDIT: Edit of a pre-existing parent asset. + Must have a parent ingredient. + - UPDATE: Restricted version of Edit for non-editorial changes. + Must have only one ingredient as a parent. + + Args: + intent: The builder intent (C2paBuilderIntent enum value) + digital_source_type: The digital source type (required + for CREATE intent). Defaults to C2paDigitalSourceType.EMPTY + (for all other cases). + + Raises: + C2paError: If there was an error setting the intent + """ + self._ensure_valid_state() + + result = _lib.c2pa_builder_set_intent( + self._builder, + ctypes.c_uint(intent), + ctypes.c_uint(digital_source_type), + ) + + if result != 0: + error = _parse_operation_result_for_error(_lib.c2pa_error()) + if error: + raise C2paError(error) + raise C2paError("Error setting intent for Builder: Unknown error") + def add_resource(self, uri: str, stream: Any): """Add a resource to the builder. diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 97b5bb8a..f808126a 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -29,7 +29,7 @@ # Suppress deprecation warnings warnings.filterwarnings("ignore", category=DeprecationWarning) -from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version +from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version, C2paBuilderIntent, C2paDigitalSourceType from c2pa.c2pa import Stream, read_ingredient_file, read_file, sign_file, load_settings, create_signer, create_signer_from_info, ed25519_sign, format_embeddable PROJECT_PATH = os.getcwd() @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.71.0", sdk_version()) + self.assertIn("0.71.2", sdk_version()) class TestReader(unittest.TestCase): @@ -1115,6 +1115,215 @@ def test_streams_sign_with_es256_alg_2(self): self.assertIn("Invalid", json_data) output.close() + def test_streams_sign_with_es256_alg_create_intent(self): + """Test signing with CREATE intent and empty manifest.""" + + with open(self.testPath2, "rb") as file: + # Start with an empty manifest + builder = Builder({}) + # Set the intent for creating new content + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.DIGITAL_CREATION + ) + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_str = reader.json() + # Verify the manifest was created + self.assertIsNotNone(json_str) + + # Parse the JSON to verify the structure + manifest_data = json.loads(json_str) + active_manifest_label = manifest_data["active_manifest"] + active_manifest = manifest_data["manifests"][active_manifest_label] + + # Check that assertions exist + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the actions assertion + actions_assertion = None + for assertion in assertions: + if assertion["label"] in ["c2pa.actions", "c2pa.actions.v2"]: + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + + # Verify c2pa.created action exists and there is only one + actions = actions_assertion["data"]["actions"] + created_actions = [ + action for action in actions + if action["action"] == "c2pa.created" + ] + + self.assertEqual(len(created_actions), 1) + + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertEqual(manifest_data["validation_state"], "Invalid") + output.close() + + def test_streams_sign_with_es256_alg_create_intent_2(self): + """Test signing with CREATE intent and manifestDefinitionV2.""" + + with open(self.testPath2, "rb") as file: + # Start with manifestDefinitionV2 which has predefined metadata + builder = Builder(self.manifestDefinitionV2) + # Set the intent for creating new content + # If we provided a full manifest, the digital source type from the full manifest "wins" + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.SCREEN_CAPTURE + ) + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_str = reader.json() + + # Verify the manifest was created + self.assertIsNotNone(json_str) + + # Parse the JSON to verify the structure + manifest_data = json.loads(json_str) + active_manifest_label = manifest_data["active_manifest"] + active_manifest = manifest_data["manifests"][active_manifest_label] + + # Verify title from manifestDefinitionV2 is preserved + self.assertIn("title", active_manifest) + self.assertEqual(active_manifest["title"], "Python Test Image V2") + + # Verify claim_generator_info is present + self.assertIn("claim_generator_info", active_manifest) + claim_generator_info = active_manifest["claim_generator_info"] + self.assertIsInstance(claim_generator_info, list) + self.assertGreater(len(claim_generator_info), 0) + + # Check for the custom claim generator info from manifestDefinitionV2 + has_python_test = any( + gen.get("name") == "python_test" and gen.get("version") == "0.0.1" + for gen in claim_generator_info + ) + self.assertTrue(has_python_test, "Should have python_test claim generator") + + # Verify no ingredients for CREATE intent + ingredients_manifest = active_manifest.get("ingredients", []) + self.assertEqual(len(ingredients_manifest), 0, "CREATE intent should have no ingredients") + + # Check that assertions exist + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the actions assertion + actions_assertion = None + for assertion in assertions: + if assertion["label"] in ["c2pa.actions", "c2pa.actions.v2"]: + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + + # Verify c2pa.created action exists and there is only one + actions = actions_assertion["data"]["actions"] + created_actions = [ + action for action in actions + if action["action"] == "c2pa.created" + ] + + self.assertEqual(len(created_actions), 1) + + # Verify the digitalSourceType is present in the created action + created_action = created_actions[0] + self.assertIn("digitalSourceType", created_action) + self.assertIn("digitalCreation", created_action["digitalSourceType"]) + + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertEqual(manifest_data["validation_state"], "Invalid") + output.close() + + def test_streams_sign_with_es256_alg_edit_intent(self): + """Test signing with EDIT intent and empty manifest.""" + + with open(self.testPath2, "rb") as file: + # Start with an empty manifest + builder = Builder({}) + # Set the intent for editing existing content + builder.set_intent(C2paBuilderIntent.EDIT) + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_str = reader.json() + + # Verify the manifest was created + self.assertIsNotNone(json_str) + + # Parse the JSON to verify the structure + manifest_data = json.loads(json_str) + active_manifest_label = manifest_data["active_manifest"] + active_manifest = manifest_data["manifests"][active_manifest_label] + + # Check that ingredients exist in the active manifest + self.assertIn("ingredients", active_manifest) + ingredients_manifest = active_manifest["ingredients"] + self.assertIsInstance(ingredients_manifest, list) + self.assertEqual(len(ingredients_manifest), 1) + + # Verify the ingredient has relationship "parentOf" + ingredient = ingredients_manifest[0] + self.assertIn("relationship", ingredient) + self.assertEqual( + ingredient["relationship"], + "parentOf" + ) + + # Check that assertions exist + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the actions assertion + actions_assertion = None + for assertion in assertions: + if assertion["label"] in ["c2pa.actions", "c2pa.actions.v2"]: + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + + # Verify c2pa.opened action exists and there is only one + actions = actions_assertion["data"]["actions"] + opened_actions = [ + action for action in actions + if action["action"] == "c2pa.opened" + ] + + self.assertEqual(len(opened_actions), 1) + + # Verify the c2pa.opened action has the correct structure + opened_action = opened_actions[0] + self.assertIn("parameters", opened_action) + self.assertIn("ingredients", opened_action["parameters"]) + ingredients = opened_action["parameters"]["ingredients"] + self.assertIsInstance(ingredients, list) + self.assertGreater(len(ingredients), 0) + + # Verify each ingredient has url and hash + for ingredient in ingredients: + self.assertIn("url", ingredient) + self.assertIn("hash", ingredient) + + # Needs trust configuration to be set up to validate as Trusted, + # or validation_status on read reports `signing certificate untrusted` + # which makes the manifest validation_state become Invalid. + self.assertEqual(manifest_data["validation_state"], "Invalid") + output.close() + def test_streams_sign_with_es256_alg_with_trust_config(self): # Run in a separate thread to isolate thread-local settings result = {} @@ -1162,7 +1371,6 @@ def sign_and_validate_with_trust_config(): self.assertIsNotNone(result.get('validation_state')) self.assertEqual(result.get('validation_state'), "Trusted") - def test_sign_with_ed25519_alg(self): with open(os.path.join(self.data_dir, "ed25519.pub"), "rb") as cert_file: certs = cert_file.read() @@ -1934,6 +2142,107 @@ def test_builder_sign_with_ingredient(self): builder.close() + def test_builder_sign_with_ingredients_edit_intent(self): + """Test signing with EDIT intent and ingredient.""" + builder = Builder.from_json({}) + assert builder._builder is not None + + # Set the intent for editing existing content + builder.set_intent(C2paBuilderIntent.EDIT) + + # Test adding ingredient + ingredient_json = '{ "title": "Test Ingredient" }' + with open(self.testPath3, 'rb') as f: + builder.add_ingredient(ingredient_json, "image/jpeg", f) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify ingredients array exists with exactly 2 ingredients + self.assertIn("ingredients", active_manifest) + ingredients_manifest = active_manifest["ingredients"] + self.assertIsInstance(ingredients_manifest, list) + self.assertEqual(len(ingredients_manifest), 2, "Should have exactly two ingredients") + + # Verify the first ingredient is the one we added manually with componentOf relationship + first_ingredient = ingredients_manifest[0] + self.assertEqual(first_ingredient["title"], "Test Ingredient") + self.assertEqual(first_ingredient["format"], "image/jpeg") + self.assertIn("instance_id", first_ingredient) + self.assertIn("thumbnail", first_ingredient) + self.assertEqual(first_ingredient["thumbnail"]["format"], "image/jpeg") + self.assertIn("identifier", first_ingredient["thumbnail"]) + self.assertEqual(first_ingredient["relationship"], "componentOf") + self.assertIn("label", first_ingredient) + + # Verify the second ingredient is the auto-created parent with parentOf relationship + second_ingredient = ingredients_manifest[1] + # Parent ingredient may not have a title field, or may have an empty one + self.assertEqual(second_ingredient["format"], "image/jpeg") + self.assertIn("instance_id", second_ingredient) + self.assertIn("thumbnail", second_ingredient) + self.assertEqual(second_ingredient["thumbnail"]["format"], "image/jpeg") + self.assertIn("identifier", second_ingredient["thumbnail"]) + self.assertEqual(second_ingredient["relationship"], "parentOf") + self.assertIn("label", second_ingredient) + + # Count ingredients with parentOf relationship - should be exactly one + parent_ingredients = [ + ing for ing in ingredients_manifest + if ing.get("relationship") == "parentOf" + ] + self.assertEqual(len(parent_ingredients), 1, "Should have exactly one parentOf ingredient") + + # Check that assertions exist + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the actions assertion + actions_assertion = None + for assertion in assertions: + if assertion["label"] in ["c2pa.actions", "c2pa.actions.v2"]: + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion, "Should have c2pa.actions assertion") + + # Verify exactly one c2pa.opened action exists for EDIT intent + actions = actions_assertion["data"]["actions"] + opened_actions = [ + action for action in actions + if action["action"] == "c2pa.opened" + ] + self.assertEqual(len(opened_actions), 1, "Should have exactly one c2pa.opened action") + + # Verify the c2pa.opened action has the correct structure with parameters and ingredients + opened_action = opened_actions[0] + self.assertIn("parameters", opened_action, "c2pa.opened action should have parameters") + self.assertIn("ingredients", opened_action["parameters"], "parameters should have ingredients array") + ingredients_params = opened_action["parameters"]["ingredients"] + self.assertIsInstance(ingredients_params, list) + self.assertGreater(len(ingredients_params), 0, "Should have at least one ingredient reference") + + # Verify each ingredient reference has url and hash + for ingredient_ref in ingredients_params: + self.assertIn("url", ingredient_ref, "Ingredient reference should have url") + self.assertIn("hash", ingredient_ref, "Ingredient reference should have hash") + + builder.close() + def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self): builder = Builder.from_json(self.manifestDefinition) assert builder._builder is not None @@ -3029,7 +3338,7 @@ def test_builder_add_action_to_manifest_no_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): # For testing, remove auto-added actions @@ -3110,11 +3419,11 @@ def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_add_action_to_manifest_with_auto_add(self): # For testing, force settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -3199,7 +3508,7 @@ def test_builder_add_action_to_manifest_with_auto_add(self): builder.close() # Reset settings to default - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): # For testing, remove auto-added actions @@ -3264,11 +3573,11 @@ def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -3338,7 +3647,7 @@ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_sign_dicts_no_auto_add(self): # For testing, remove auto-added actions @@ -3419,7 +3728,7 @@ def test_builder_sign_dicts_no_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') class TestStream(unittest.TestCase): From dde66091781b53afcd1c0a9e7c3f7fc95d2f9bdf Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:13:13 -0800 Subject: [PATCH 6/9] chore: Update c2pa version to v0.72.0 (#198) * chore: Update c2pa version to v0.72.0 * chore: Update expected SDK version in unit tests --- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index ff0544b3..541747ad 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.71.2 +c2pa-v0.72.0 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index f808126a..1c0ced7f 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.71.2", sdk_version()) + self.assertIn("0.72.0", sdk_version()) class TestReader(unittest.TestCase): From 838639c634deb3a66ac1b726616af2b2ae8148fb Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:33:00 -0800 Subject: [PATCH 7/9] chore: Update c2pa version to v0.72.1 (#200) * chore: Update c2pa version to v0.72.1 * chore: Update c2pa version to v0.72.1 * fix: Makefile indent typo * fix: Update the tests --- Makefile | 2 +- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 143 +++++++++++++++------------------------ 3 files changed, 55 insertions(+), 92 deletions(-) diff --git a/Makefile b/Makefile index 8fa23345..eca23ade 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ run-examples: # Runs the examples, then the unit tests test: - make run-examples + make run-examples python3 ./tests/test_unit_tests.py python3 ./tests/test_unit_tests_threaded.py diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index 541747ad..0ca86fe2 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.72.0 +c2pa-v0.72.1 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 1c0ced7f..756588ff 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.72.0", sdk_version()) + self.assertIn("0.72.1", sdk_version()) class TestReader(unittest.TestCase): @@ -149,8 +149,7 @@ def test_stream_read_get_validation_state(self): reader = Reader("image/jpeg", file) validation_state = reader.get_validation_state() self.assertIsNotNone(validation_state) - # Needs trust configuration to be set up to validate as Trusted, otherwise manifest is Invalid - self.assertEqual(validation_state, "Invalid") + self.assertEqual(validation_state, "Valid") def test_stream_read_get_validation_state_with_trust_config(self): # Run in a separate thread to isolate thread-local settings @@ -1012,10 +1011,7 @@ def test_streams_sign_with_es256_alg_v1_manifest(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + self.assertIn("Valid", json_data) # Write buffer to file # output.seek(0) @@ -1040,10 +1036,7 @@ def test_streams_sign_with_es256_alg_v1_manifest_to_existing_empty_file(self): reader = Reader("image/jpeg", target) json_data = reader.json() self.assertIn("Python Test", json_data) - # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + self.assertIn("Valid", json_data) finally: # Clean up... @@ -1069,10 +1062,7 @@ def test_streams_sign_with_es256_alg_v1_manifest_to_new_dest_file(self): reader = Reader("image/jpeg", target) json_data = reader.json() self.assertIn("Python Test", json_data) - # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + self.assertIn("Valid", json_data) finally: # Clean up... @@ -1095,9 +1085,8 @@ def test_streams_sign_with_es256_alg(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_streams_sign_with_es256_alg_2(self): @@ -1109,10 +1098,7 @@ def test_streams_sign_with_es256_alg_2(self): reader = Reader("image/jpeg", output) json_data = reader.json() self.assertIn("Python Test", json_data) - # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + self.assertIn("Valid", json_data) output.close() def test_streams_sign_with_es256_alg_create_intent(self): @@ -1162,9 +1148,8 @@ def test_streams_sign_with_es256_alg_create_intent(self): self.assertEqual(len(created_actions), 1) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertEqual(manifest_data["validation_state"], "Invalid") + # or validation_status on read reports `signing certificate untrusted`. + self.assertEqual(manifest_data["validation_state"], "Valid") output.close() def test_streams_sign_with_es256_alg_create_intent_2(self): @@ -1242,9 +1227,8 @@ def test_streams_sign_with_es256_alg_create_intent_2(self): self.assertIn("digitalCreation", created_action["digitalSourceType"]) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertEqual(manifest_data["validation_state"], "Invalid") + # or validation_status on read reports `signing certificate untrusted`. + self.assertEqual(manifest_data["validation_state"], "Valid") output.close() def test_streams_sign_with_es256_alg_edit_intent(self): @@ -1319,9 +1303,8 @@ def test_streams_sign_with_es256_alg_edit_intent(self): self.assertIn("hash", ingredient) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertEqual(manifest_data["validation_state"], "Invalid") + # or validation_status on read reports `signing certificate untrusted`. + self.assertEqual(manifest_data["validation_state"], "Valid") output.close() def test_streams_sign_with_es256_alg_with_trust_config(self): @@ -1394,9 +1377,8 @@ def test_sign_with_ed25519_alg(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_sign_with_ed25519_alg_with_trust_config(self): @@ -1482,9 +1464,8 @@ def test_sign_with_ed25519_alg_2(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_sign_with_ps256_alg(self): @@ -1510,9 +1491,8 @@ def test_sign_with_ps256_alg(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_sign_with_ps256_alg_2(self): @@ -1614,9 +1594,8 @@ def test_archive_sign(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) archive.close() output.close() @@ -1687,9 +1666,8 @@ def test_archive_sign_with_added_ingredient(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) archive.close() output.close() @@ -1897,9 +1875,8 @@ def test_sign_all_files(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) reader.close() output.close() except Error.NotSupported: @@ -1966,8 +1943,7 @@ def test_sign_all_files_V2(self): self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + self.assertIn("Valid", json_data) reader.close() output.close() except Error.NotSupported: @@ -2600,9 +2576,8 @@ def test_sign_single(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` (which makes the manifest Invalid) - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_sign_mp4_video_file_single(self): @@ -2618,9 +2593,8 @@ def test_sign_mp4_video_file_single(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_sign_mov_video_file_single(self): @@ -2636,9 +2610,8 @@ def test_sign_mov_video_file_single(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() def test_sign_file_video(self): @@ -2664,9 +2637,8 @@ def test_sign_file_video(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) finally: # Clean up the temporary directory @@ -2719,9 +2691,8 @@ def test_builder_sign_file_callback_signer_from_callback(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -2773,9 +2744,8 @@ def test_builder_sign_file_callback_signer_from_callback_V2(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) # Parse the JSON and verify the signature algorithm manifest_data = json.loads(json_data) @@ -2829,9 +2799,8 @@ def ed25519_callback(data: bytes) -> bytes: json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) reader.close() output.close() @@ -2856,9 +2825,8 @@ def test_signing_manifest_v2(self): # Basic verification of the manifest self.assertIn("Python Test Image V2", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) output.close() @@ -2892,9 +2860,8 @@ def test_sign_file_mp4_video(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) finally: # Clean up the temporary directory @@ -2923,18 +2890,16 @@ def test_sign_file_mov_video(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) # Verify also signed file using manifest bytes with Reader("mov", output_path, manifest_bytes) as reader: json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) finally: # Clean up the temporary directory @@ -2963,18 +2928,16 @@ def test_sign_file_mov_video_V2(self): json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) # Verify also signed file using manifest bytes with Reader("mov", output_path, manifest_bytes) as reader: json_data = reader.json() self.assertIn("Python Test", json_data) # Needs trust configuration to be set up to validate as Trusted, - # or validation_status on read reports `signing certificate untrusted` - # which makes the manifest validation_state become Invalid. - self.assertIn("Invalid", json_data) + # or validation_status on read reports `signing certificate untrusted`. + self.assertIn("Valid", json_data) finally: # Clean up the temporary directory From 35471a58ade540824bb2cab76f6cd3a641b11c1b Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:01:55 -0800 Subject: [PATCH 8/9] chore: Update c2pa version to v0.73.0 (#201) * chore: Update c2pa version to v0.73.0 * chore: Update expected SDK version in unit test * fix: Remove retired runner * fix: Update retired runner * fix: Update retired runner 2 * fix: Update retired runner 3 --- .github/workflows/build.yml | 4 ++-- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bb6e130..6cab3c53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -368,7 +368,7 @@ jobs: - target: arm64 runs-on: macos-latest - target: x86_64 - runs-on: macos-13 + runs-on: macos-15-intel if: | github.event_name != 'pull_request' || @@ -387,7 +387,7 @@ jobs: - target: arm64 runs-on: macos-latest - target: x86_64 - runs-on: macos-13 + runs-on: macos-15-intel if: | github.event_name != 'pull_request' || diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index 0ca86fe2..c8f95fe2 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.72.1 +c2pa-v0.73.0 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 756588ff..b82eda80 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.72.1", sdk_version()) + self.assertIn("0.73.0", sdk_version()) class TestReader(unittest.TestCase): From 125341a93dfc3586979e278274ecc236c450398d Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:57:52 -0800 Subject: [PATCH 9/9] chore: Update c2pa version to v0.73.1 (#204) * chore: Update c2pa version to v0.73.1 * fix: Update expected SDK version in unit test --- c2pa-native-version.txt | 2 +- tests/test_unit_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index c8f95fe2..560aef5c 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.73.0 +c2pa-v0.73.1 diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 6fa4326e..1c3bfbed 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -67,7 +67,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): - self.assertIn("0.73.0", sdk_version()) + self.assertIn("0.73.1", sdk_version()) class TestReader(unittest.TestCase):