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.
- {c2pa_python-0.12.0/src/c2pa_python.egg-info → c2pa_python-0.12.1}/PKG-INFO +1 -1
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/pyproject.toml +1 -1
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/c2pa.py +52 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/lib.py +3 -7
- {c2pa_python-0.12.0 → c2pa_python-0.12.1/src/c2pa_python.egg-info}/PKG-INFO +1 -1
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/tests/test_unit_tests.py +173 -2
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/LICENSE-APACHE +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/LICENSE-MIT +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/MANIFEST.in +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/README.md +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/requirements.txt +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/setup.cfg +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/setup.py +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa/build.py +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.12.0 → c2pa_python-0.12.1}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
#
|
|
20
|
-
logging.
|
|
21
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|