c2pa-python 0.12.0__tar.gz → 0.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {c2pa_python-0.12.0/src/c2pa_python.egg-info → c2pa_python-0.12.1}/PKG-INFO +1 -1
  2. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/pyproject.toml +1 -1
  3. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/c2pa.py +52 -0
  4. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/lib.py +3 -7
  5. {c2pa_python-0.12.0 → c2pa_python-0.12.1/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  6. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/tests/test_unit_tests.py +173 -2
  7. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/LICENSE-APACHE +0 -0
  8. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/LICENSE-MIT +0 -0
  9. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/MANIFEST.in +0 -0
  10. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/README.md +0 -0
  11. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/requirements.txt +0 -0
  12. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/scripts/download_artifacts.py +0 -0
  13. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/setup.cfg +0 -0
  14. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/setup.py +0 -0
  15. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/__init__.py +0 -0
  16. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/build.py +0 -0
  17. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.12.0 → c2pa_python-0.12.1}/tests/test_unit_tests_threaded.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
6
  Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "c2pa-python"
7
- version = "0.12.0"
7
+ version = "0.12.1"
8
8
  requires-python = ">=3.10"
9
9
  description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -123,6 +123,19 @@ class C2paSigningAlg(enum.IntEnum):
123
123
  ED25519 = 6
124
124
 
125
125
 
126
+ # Mapping from C2paSigningAlg enum to string representation,
127
+ # as the enum value currently maps by default to an integer value.
128
+ _ALG_TO_STRING_BYTES_MAPPING = {
129
+ C2paSigningAlg.ES256: b"es256",
130
+ C2paSigningAlg.ES384: b"es384",
131
+ C2paSigningAlg.ES512: b"es512",
132
+ C2paSigningAlg.PS256: b"ps256",
133
+ C2paSigningAlg.PS384: b"ps384",
134
+ C2paSigningAlg.PS512: b"ps512",
135
+ C2paSigningAlg.ED25519: b"ed25519",
136
+ }
137
+
138
+
126
139
  # Define callback types
127
140
  ReadCallback = ctypes.CFUNCTYPE(
128
141
  ctypes.c_ssize_t,
@@ -205,6 +218,45 @@ class C2paSignerInfo(ctypes.Structure):
205
218
  ("ta_url", ctypes.c_char_p),
206
219
  ]
207
220
 
221
+ def __init__(self, alg, sign_cert, private_key, ta_url):
222
+ """Initialize C2paSignerInfo with optional parameters.
223
+
224
+ Args:
225
+ alg: The signing algorithm, either as a C2paSigningAlg enum or string or bytes
226
+ (will be converted accordingly to bytes for native library use)
227
+ sign_cert: The signing certificate as a string
228
+ private_key: The private key as a string
229
+ ta_url: The timestamp authority URL as bytes
230
+ """
231
+ # Handle alg parameter: can be C2paSigningAlg enum or string (or bytes), convert as needed
232
+ if isinstance(alg, C2paSigningAlg):
233
+ # Convert enum to string representation
234
+ alg_str = _ALG_TO_STRING_BYTES_MAPPING.get(alg)
235
+ if alg_str is None:
236
+ raise ValueError(f"Unsupported signing algorithm: {alg}")
237
+ alg = alg_str
238
+ elif isinstance(alg, str):
239
+ # String to bytes, as requested by native lib
240
+ alg = alg.encode('utf-8')
241
+ elif isinstance(alg, bytes):
242
+ # In bytes already
243
+ pass
244
+ else:
245
+ raise TypeError(f"alg must be C2paSigningAlg enum, string, or bytes, got {type(alg)}")
246
+
247
+ # Handle ta_url parameter: allow string or bytes, convert string to bytes as needed
248
+ if isinstance(ta_url, str):
249
+ # String to bytes, as requested by native lib
250
+ ta_url = ta_url.encode('utf-8')
251
+ elif isinstance(ta_url, bytes):
252
+ # In bytes already
253
+ pass
254
+ else:
255
+ raise TypeError(f"ta_url must be string or bytes, got {type(ta_url)}")
256
+
257
+ # Call parent constructor with processed values
258
+ super().__init__(alg, sign_cert, private_key, ta_url)
259
+
208
260
 
209
261
  class C2paReader(ctypes.Structure):
210
262
  """Opaque structure for reader context."""
@@ -16,13 +16,9 @@ from enum import Enum
16
16
  # Debug flag for library loading
17
17
  DEBUG_LIBRARY_LOADING = False
18
18
 
19
- # Configure logging
20
- logging.basicConfig(
21
- level=logging.INFO,
22
- format='%(asctime)s - %(levelname)s - %(message)s',
23
- force=True # Force configuration even if already configured
24
- )
25
- logger = logging.getLogger(__name__)
19
+ # Create a module-specific logger with NullHandler to avoid interfering with global configuration
20
+ logger = logging.getLogger("c2pa")
21
+ logger.addHandler(logging.NullHandler())
26
22
 
27
23
 
28
24
  class CPUArchitecture(Enum):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
6
  Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
@@ -39,7 +39,7 @@ ALTERNATIVE_INGREDIENT_TEST_FILE = os.path.join(FIXTURES_DIR, "cloud.jpg")
39
39
 
40
40
  class TestC2paSdk(unittest.TestCase):
41
41
  def test_sdk_version(self):
42
- self.assertIn("0.55.0", sdk_version())
42
+ self.assertIn("0.58.0", sdk_version())
43
43
 
44
44
 
45
45
  class TestReader(unittest.TestCase):
@@ -95,6 +95,13 @@ class TestReader(unittest.TestCase):
95
95
  with self.assertRaises(Error):
96
96
  reader.json()
97
97
 
98
+ def test_reader_streams_with_nested(self):
99
+ with open(self.testPath, "rb") as file:
100
+ with Reader("image/jpeg", file) as reader:
101
+ manifest_store = json.loads(reader.json())
102
+ title = manifest_store["manifests"][manifest_store["active_manifest"]]["title"]
103
+ self.assertEqual(title, DEFAULT_TEST_FILE_NAME)
104
+
98
105
  def test_reader_close_cleanup(self):
99
106
  """Test that close properly cleans up all resources."""
100
107
  with open(self.testPath, "rb") as file:
@@ -292,6 +299,33 @@ class TestBuilder(unittest.TestCase):
292
299
  ]
293
300
  }
294
301
 
302
+ # Define a V2 manifest as a dictionary
303
+ self.manifestDefinitionV2 = {
304
+ "claim_generator": "python_test",
305
+ "claim_generator_info": [{
306
+ "name": "python_test",
307
+ "version": "0.0.1",
308
+ }],
309
+ "claim_version": 2,
310
+ "format": "image/jpeg",
311
+ "title": "Python Test Image V2",
312
+ "ingredients": [],
313
+ "assertions": [
314
+ {
315
+ "label": "c2pa.actions",
316
+ "data": {
317
+ "actions": [
318
+ {
319
+ "action": "c2pa.created",
320
+ "parameters": {
321
+ }
322
+ }
323
+ ]
324
+ }
325
+ }
326
+ ]
327
+ }
328
+
295
329
  # Define an example ES256 callback signer
296
330
  self.callback_signer_alg = "Es256"
297
331
  def callback_signer_es256(data: bytes) -> bytes:
@@ -991,6 +1025,57 @@ class TestBuilder(unittest.TestCase):
991
1025
  finally:
992
1026
  shutil.rmtree(temp_dir)
993
1027
 
1028
+ def test_builder_sign_file_callback_signer_from_callback_V2(self):
1029
+ """Test signing a file using the sign_file method with Signer.from_callback."""
1030
+
1031
+ temp_dir = tempfile.mkdtemp()
1032
+ try:
1033
+
1034
+ output_path = os.path.join(temp_dir, "signed_output_from_callback.jpg")
1035
+
1036
+ # Will use the sign_file method
1037
+ builder = Builder(self.manifestDefinitionV2)
1038
+
1039
+ # Create signer with callback using Signer.from_callback
1040
+ signer = Signer.from_callback(
1041
+ callback=self.callback_signer_es256,
1042
+ alg=SigningAlg.ES256,
1043
+ certs=self.certs.decode('utf-8'),
1044
+ tsa_url="http://timestamp.digicert.com"
1045
+ )
1046
+
1047
+ manifest_bytes = builder.sign_file(
1048
+ source_path=self.testPath,
1049
+ dest_path=output_path,
1050
+ signer=signer
1051
+ )
1052
+
1053
+ # Verify the output file was created
1054
+ self.assertTrue(os.path.exists(output_path))
1055
+
1056
+ # Verify results
1057
+ self.assertIsInstance(manifest_bytes, bytes)
1058
+ self.assertGreater(len(manifest_bytes), 0)
1059
+
1060
+ # Read the signed file and verify the manifest
1061
+ with open(output_path, "rb") as file, Reader("image/jpeg", file) as reader:
1062
+ json_data = reader.json()
1063
+ self.assertIn("Python Test", json_data)
1064
+ self.assertNotIn("validation_status", json_data)
1065
+
1066
+ # Parse the JSON and verify the signature algorithm
1067
+ manifest_data = json.loads(json_data)
1068
+ active_manifest_id = manifest_data["active_manifest"]
1069
+ active_manifest = manifest_data["manifests"][active_manifest_id]
1070
+
1071
+ # Verify the signature_info contains the correct algorithm
1072
+ self.assertIn("signature_info", active_manifest)
1073
+ signature_info = active_manifest["signature_info"]
1074
+ self.assertEqual(signature_info["alg"], self.callback_signer_alg)
1075
+
1076
+ finally:
1077
+ shutil.rmtree(temp_dir)
1078
+
994
1079
  def test_sign_file_using_callback_signer_overloads(self):
995
1080
  """Test signing a file using the sign_file function with a Signer object."""
996
1081
  # Create a temporary directory for the test
@@ -1191,6 +1276,30 @@ class TestBuilder(unittest.TestCase):
1191
1276
  finally:
1192
1277
  shutil.rmtree(temp_dir)
1193
1278
 
1279
+ def test_signing_manifest_v2(self):
1280
+ """Test signing and reading a V2 manifest.
1281
+ V2 manifests have a slightly different structure.
1282
+ """
1283
+ with open(self.testPath, "rb") as file:
1284
+ # Create a builder with the V2 manifest definition using context manager
1285
+ with Builder(self.manifestDefinitionV2) as builder:
1286
+ output = io.BytesIO(bytearray())
1287
+
1288
+ # Sign as usual...
1289
+ builder.sign(self.signer, "image/jpeg", file, output)
1290
+
1291
+ output.seek(0)
1292
+
1293
+ # Read the signed file and verify the manifest using context manager
1294
+ with Reader("image/jpeg", output) as reader:
1295
+ json_data = reader.json()
1296
+
1297
+ # Basic verification of the manifest
1298
+ self.assertIn("Python Test Image V2", json_data)
1299
+ self.assertNotIn("validation_status", json_data)
1300
+
1301
+ output.close()
1302
+
1194
1303
  class TestStream(unittest.TestCase):
1195
1304
  def setUp(self):
1196
1305
  # Create a temporary file for testing
@@ -1384,7 +1493,69 @@ class TestLegacyAPI(unittest.TestCase):
1384
1493
  self.assertIn("manifests", file_data)
1385
1494
  self.assertIn(expected_manifest_id, file_data["manifests"])
1386
1495
 
1387
- def test_sign_file(self):
1496
+ def test_sign_file_alg_as_enum(self):
1497
+ """Test signing a file with C2PA manifest."""
1498
+ # Set up test paths
1499
+ temp_data_dir = os.path.join(self.data_dir, "temp_data")
1500
+ os.makedirs(temp_data_dir, exist_ok=True)
1501
+ output_path = os.path.join(temp_data_dir, "signed_output.jpg")
1502
+
1503
+ # Load test certificates and key
1504
+ with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
1505
+ certs = cert_file.read()
1506
+ with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
1507
+ key = key_file.read()
1508
+
1509
+ # Create signer info
1510
+ signer_info = C2paSignerInfo(
1511
+ alg=SigningAlg.ES256,
1512
+ sign_cert=certs,
1513
+ private_key=key,
1514
+ ta_url=b"http://timestamp.digicert.com"
1515
+ )
1516
+
1517
+ # Create a simple manifest
1518
+ manifest = {
1519
+ "claim_generator": "python_internals_test",
1520
+ "claim_generator_info": [{
1521
+ "name": "python_internals_test",
1522
+ "version": "0.0.1",
1523
+ }],
1524
+ "format": "image/jpeg",
1525
+ "title": "Python Test Signed Image",
1526
+ "ingredients": [],
1527
+ "assertions": [
1528
+ {
1529
+ "label": "c2pa.actions",
1530
+ "data": {
1531
+ "actions": [
1532
+ {
1533
+ "action": "c2pa.opened"
1534
+ }
1535
+ ]
1536
+ }
1537
+ }
1538
+ ]
1539
+ }
1540
+
1541
+ # Convert manifest to JSON string
1542
+ manifest_json = json.dumps(manifest)
1543
+
1544
+ try:
1545
+ # Sign the file
1546
+ result_json = sign_file(
1547
+ self.testPath,
1548
+ output_path,
1549
+ manifest_json,
1550
+ signer_info
1551
+ )
1552
+
1553
+ finally:
1554
+ # Clean up
1555
+ if os.path.exists(output_path):
1556
+ os.remove(output_path)
1557
+
1558
+ def test_sign_file_alg_as_bytes(self):
1388
1559
  """Test signing a file with C2PA manifest."""
1389
1560
  # Set up test paths
1390
1561
  temp_data_dir = os.path.join(self.data_dir, "temp_data")
File without changes
File without changes
File without changes
File without changes
File without changes