Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
94262bd
test: #80: refactor e2e test to make it cleaner
aasheptunov May 25, 2026
fb379a5
test: #80: add a test to verify that specified exclusion value matche…
aasheptunov May 25, 2026
99d5d15
refactor: #80: refactor c2pa storage data hash exclusion calculation …
aasheptunov May 25, 2026
904cd63
test: #80: modify exclusion cover test so that it covers pdf case
aasheptunov May 26, 2026
03ca728
refactor: #80: refactor c2pa storage data hash exclusion calculation …
aasheptunov May 26, 2026
c751c4a
chore: #80: rename pdf test file so that filename more readability
aasheptunov May 26, 2026
bb26494
refactor: #80: move logic for adding exclusions to C2PA structure int…
aasheptunov May 27, 2026
e0cfcc7
test: #80: add test to align serialized COSE_Sign1
aasheptunov May 27, 2026
f7ad081
feat: #80: add alignment logic for serialized COSE_Sign1
aasheptunov May 27, 2026
836a96c
test: #80: update to Data Hash Assertion test, as pad size has been u…
aasheptunov May 27, 2026
00a5a80
test: #80: update exclusion coverage test to reflect the changes made…
aasheptunov May 27, 2026
a08dccc
docs(readme): bring test coverage score up to date
May 27, 2026
1323202
feat: #80: add handling for CBOR boundary violation cases for TSA tokens
aasheptunov May 28, 2026
31025d9
refactor: #80: rework logic for handling CBOR boundary violations for…
aasheptunov May 28, 2026
c62e2cb
chore: #80: rename serialized_length to serialized_cose_sign1_length …
aasheptunov May 28, 2026
0ef4d7b
fix: #80: correct of a typo in the condition
aasheptunov May 28, 2026
ab941f2
test: #80: add a test that checks for a ValueError exception if the d…
aasheptunov May 28, 2026
556284c
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov May 28, 2026
0edc648
docs(readme): bring test coverage score up to date
May 28, 2026
e9a364d
docs: #80: add a clarifying comment for the additional_exclusions par…
aasheptunov May 28, 2026
b3f25df
test: #80: add a test to verify the CBOR tag in COSE_Sign1_Tagged
aasheptunov May 28, 2026
3c342ae
fix: #80: fix for an error related to the pad not changing when the C…
aasheptunov May 28, 2026
ef985d7
test: #80: move the test for checking additional_exclusions to the ap…
aasheptunov May 28, 2026
98b4d22
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov May 28, 2026
36ec059
docs(readme): bring test coverage score up to date
May 28, 2026
0b0d678
docs: #80: add a comment explaining how JPG_SEGMENT_MAX_PAYLOAD_LENGT…
aasheptunov May 28, 2026
6cdb5ac
refactor: #80: move logic for emplacing the APP11 storage bytes into …
aasheptunov May 28, 2026
1c9f86a
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov May 28, 2026
c949792
docs(readme): bring test coverage score up to date
May 28, 2026
849f3a6
refactor: #80: minimize pad size for unprotected header, since the di…
aasheptunov May 28, 2026
9d3ea98
test: #80: update tests after changing pad length
aasheptunov May 28, 2026
09217a0
fix: #80: correction of a typo in the calculation of the additional l…
aasheptunov May 28, 2026
5f71f19
refactor: #80: refactoring
aasheptunov May 28, 2026
55793d3
test: #80: add tests to verify the calculation of the CBOR length in …
aasheptunov May 28, 2026
b5c9859
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov May 28, 2026
8e316a6
docs: #80: add clarifying comments
aasheptunov May 28, 2026
dd14037
docs(readme): bring test coverage score up to date
May 28, 2026
375074a
fix: #80: formatting corrections
aasheptunov May 28, 2026
60dd676
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov May 28, 2026
778febf
fix: #80: formatting corrections
aasheptunov May 28, 2026
6517e4b
test: #80: add a stress test to verify the signature
aasheptunov May 28, 2026
97cd1a4
fix: #80: fix an error in the calculation of incorrect exclusions in …
aasheptunov May 28, 2026
ffd70bf
docs(readme): bring test coverage score up to date
May 28, 2026
1055767
chore: #80: remove unused file
aasheptunov May 29, 2026
246f0ce
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov May 29, 2026
ed82a61
docs(readme): bring test coverage score up to date
May 29, 2026
218b1a3
docs: #80: add clarifying comment about additional byte of length for…
aasheptunov Jun 1, 2026
388f10a
test: #80: add test for check redundant lenght byte was removed from …
aasheptunov Jun 1, 2026
71d45ed
Merge branch 'feature/#80-investigation-and-correction-of-hash-mismat…
aasheptunov Jun 1, 2026
743808f
test: #80: change hash in the test schema override for Data Hash Asse…
aasheptunov Jun 1, 2026
363151e
fix: #80: fix for error caused by incorrect length specified in the p…
aasheptunov Jun 1, 2026
5538e42
docs: #80: add clarifying comments for value stored in pad field in D…
aasheptunov Jun 1, 2026
fb6cbe1
test: #80: update test after changing value of pad field in unprotect…
aasheptunov Jun 1, 2026
c5c0a10
chore: #80: fix a typo
aasheptunov Jun 1, 2026
f6f130f
docs: #80: add number of issue to todo comment for additional_exclusions
aasheptunov Jun 1, 2026
ef646b4
test: #80: add test for check that pad calcutation was performed corr…
aasheptunov Jun 1, 2026
3a3a0b6
docs(readme): bring test coverage score up to date
Jun 1, 2026
d3ea1d8
fix: #80: remove redundant call of _scan_pdf_to_get_its_data()
aasheptunov Jun 2, 2026
e7941d1
fix: #80: fix a typo
aasheptunov Jun 2, 2026
4b5e6c1
docs(readme): bring test coverage score up to date
Jun 2, 2026
727c181
refactor: #80: change variable name from FIXTURES_DIR to TEST_FILES_DIR
aasheptunov Jun 2, 2026
0816a36
refactor: #80: remove of the unused additional_exclusions parameter a…
aasheptunov Jun 3, 2026
00d1f3c
docs: #80: refactor clarifying test comment
aasheptunov Jun 3, 2026
4e64b60
refactor: #80: formatting changes
aasheptunov Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

[![Linting](https://github.com/TourmalineCore/c2pie/actions/workflows/lint-on-pull-request.yml/badge.svg?branch=develop)](https://github.com/TourmalineCore/c2pie/actions/workflows/lint-on-pull-request.yml)
[![c2pa](https://img.shields.io/badge/c2pa-v1.4-seagreen.svg)](https://c2pa.org/)
[![coverage](https://img.shields.io/badge/e2e_coverage-71.13%25-yellow)](https://github.com/TourmalineCore/c2pie/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
[![coverage](https://img.shields.io/badge/units_coverage-79.65%25-yellow)](https://github.com/TourmalineCore/c2pie/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
[![coverage](https://img.shields.io/badge/full_coverage-91.80%25-forestgreen)](https://github.com/TourmalineCore/c2pie/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
[![coverage](https://img.shields.io/badge/e2e_coverage-71.15%25-yellow)](https://github.com/TourmalineCore/c2pie/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
[![coverage](https://img.shields.io/badge/units_coverage-85.01%25-olivedrab)](https://github.com/TourmalineCore/c2pie/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
[![coverage](https://img.shields.io/badge/full_coverage-92.51%25-forestgreen)](https://github.com/TourmalineCore/c2pie/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
[![latest](https://img.shields.io/pypi/v/c2pie?label=latest&colorB=fc8021)](https://pypi.org/project/c2pie/)

<br>
Expand Down
82 changes: 51 additions & 31 deletions c2pie/c2pa/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ def __init__(
if not content_boxes:
payload = self.get_payload_from_schema()
box_type_hex = get_assertion_content_box_type(self.type)
content_boxes = [ContentBox(box_type=box_type_hex, payload=payload)]
content_boxes = [
ContentBox(
box_type=box_type_hex,
payload=payload,
)
]

super().__init__(
content_type=get_assertion_content_type(self.type),
Expand All @@ -64,56 +69,71 @@ class HashDataAssertion(Assertion):

def __init__(
self,
cai_offset: int,
hashed_data: bytes,
additional_exclusions: list[dict[str, int]] | None = None,
):
exclusions: list[dict[str, int]] = [
{
"start": cai_offset,
"length": 65535,
},
]

if additional_exclusions:
exclusions.extend(additional_exclusions)
exclusions: list[dict[str, int]] = []

schema: dict[str, Any] = {
"name": "jumbf manifest",
"exclusions": exclusions,
"alg": "sha256",
"hash": hashed_data,
"pad": [],
# The specification recommends setting the pad to at least 16 bytes. We use 64 bytes
# to allow for some extra space before the 23-byte limit is exceeded, since otherwise
# the CBOR header of the pad field would be reduced by 1 byte.
"pad": b"\x00" * 64,
Comment thread
aasheptunov marked this conversation as resolved.
}
super().__init__(C2PA_AssertionTypes.data_hash, schema)

def set_hash_data_length(
super().__init__(
C2PA_AssertionTypes.data_hash,
schema,
)

def add_full_c2pa_structure_exclusion(
self,
offset: int,
length: int,
) -> None:
if self.schema.get("name") != "jumbf manifest":
raise ValueError("c2pa.hash.data: jumbf manifest is missing")
exclusions = self.schema["exclusions"]
previous_exclusion_length = len(cbor_to_bytes(exclusions))

self.schema["exclusions"].extend(
[
{
"start": offset,
"length": length,
},
]
)

# NOTE: If the number of exclusions exceeds 23, an additional length byte
# will be added to the CBOR header of serialized exclusions array. This byte
# is included in the recalculation of the serialized exclusions.
current_exclusion_length = len(cbor_to_bytes(exclusions))

difference = previous_exclusion_length - current_exclusion_length

exclusions = self.schema.get("exclusions", [])
if -difference > len(self.schema["pad"]):
Comment thread
aasheptunov marked this conversation as resolved.
raise ValueError("Difference in length exceeds the predefined pad")

if not exclusions:
raise ValueError("c2pa.hash.data: exclusions are missing")
# If the pad is less than 24 bytes the size of the cbor header
# will change during conversion to cbor and will occupy less than 2 bytes.
updated_pad_length = len(self.schema["pad"]) + difference

exclusions[0]["length"] = int(length)
# If a CBOR overflow is not handled, the extra length byte that
# would be added in this case will not be taken into account.
if updated_pad_length < 24:
updated_pad_length -= 1

self.schema["pad"] = b"\x00" * updated_pad_length

payload = self.get_payload_from_schema()
if self.content_boxes:
self.content_boxes[0] = ContentBox(

self.content_boxes = [
ContentBox(
box_type=get_assertion_content_box_type(self.type),
payload=payload,
)
else:
self.content_boxes = [
ContentBox(
box_type=get_assertion_content_box_type(self.type),
payload=payload,
)
]
]

self.sync_payload()

Expand Down
9 changes: 7 additions & 2 deletions c2pie/c2pa/assertion_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ def __init__(
def get_assertions(self) -> list:
return self.assertions

def set_hash_data_length(
def add_full_c2pa_structure_exclusion(
self,
offset: int,
length: int,
) -> None:
for assertion in self.assertions:
if assertion.type == C2PA_AssertionTypes.data_hash:
assertion.set_hash_data_length(length)
assertion.add_full_c2pa_structure_exclusion(
offset,
length,
)

self.sync_payload()
45 changes: 44 additions & 1 deletion c2pie/c2pa/claim_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def __init__(
self.require_tsa = require_tsa
self.tsa_log_dir = tsa_log_dir

self.serialized_cose_sign1_length = 0

content_boxes = self._generate_payload()

super().__init__(
Expand Down Expand Up @@ -129,10 +131,49 @@ def _generate_unprotected_header(self, serialized_sig_structure: bytes) -> bytes
},
],
},
# The specification recommends setting the pad to at least 16 bytes. We use 64 bytes
# to allow for some extra space before the 23-byte limit is exceeded, since otherwise
# the CBOR header of the pad field would be reduced by 1 byte.
"pad": b"\x00" * 8,
}

return unprotected_header

def serialize_cose_sign1_tagged_with_alignment(
self,
cose_sign1: list,
) -> bytes:
cose_sign1_tagged_cbor = cbor2.dumps(
cbor2.CBORTag(18, cose_sign1),
canonical=True,
)

# The length of a TSA token can be variable. To ensure that a new token does not exceed
# the exclusion boundary for the C2PA structure, we need to align the length of
# the Claim Signature using the pad field, similar to the Data Hash Assertion.
if self.serialized_cose_sign1_length == 0:
self.serialized_cose_sign1_length = len(cose_sign1_tagged_cbor)
elif self.serialized_cose_sign1_length != len(cose_sign1_tagged_cbor):
difference = self.serialized_cose_sign1_length - len(cose_sign1_tagged_cbor)

if -difference > len(cose_sign1[1]["pad"]):
raise ValueError("Difference in length exceeds the predefined pad")

updated_pad_length = len(cose_sign1[1]["pad"]) + difference

# If a CBOR overflow is not handled, the extra length byte that
# would be added in this case will not be taken into account.
if updated_pad_length > 23:
updated_pad_length += 1

cose_sign1[1]["pad"] = b"\x00" * updated_pad_length
cose_sign1_tagged_cbor = cbor2.dumps(
cbor2.CBORTag(18, cose_sign1),
canonical=True,
)

return cose_sign1_tagged_cbor

def _create_cose_sign1_tagged(self) -> bytes:
"""
COSE_Sign1 = [
Expand Down Expand Up @@ -179,4 +220,6 @@ def _create_cose_sign1_tagged(self) -> bytes:

cose_sign1 = [serialized_protected_header, unprotected_header, None, signature]

return cbor2.dumps(cbor2.CBORTag(18, cose_sign1), canonical=True)
cose_sign1_tagged_cbor = self.serialize_cose_sign1_tagged_with_alignment(cose_sign1)

return cose_sign1_tagged_cbor
1 change: 0 additions & 1 deletion c2pie/c2pa/config.py

This file was deleted.

8 changes: 6 additions & 2 deletions c2pie/c2pa/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,20 @@ def get_assertions(self):
return self.assertion_store.get_assertions()
return

def set_hash_data_length(
def add_full_c2pa_structure_exclusion(
self,
offset: int,
length: int,
):
"""
Updates the length of exceptions in HashData, reassembles Claim (assertion hashes)
and ClaimSignature (COSE Sign1 detached over Claim CBOR).
"""
if self.assertion_store and self.claim and self.claim_signature:
self.assertion_store.set_hash_data_length(length)
self.assertion_store.add_full_c2pa_structure_exclusion(
offset,
length,
)
self.claim.set_assertion_store(self.assertion_store)
self.claim_signature.set_claim(self.claim)

Expand Down
8 changes: 6 additions & 2 deletions c2pie/c2pa/manifest_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ def __init__(
def sync_payload(self):
super().sync_payload()

def set_hash_data_length_for_all(
def add_full_c2pa_structure_exclusion(
self,
offset: int,
length: int,
) -> None:
self.manifests[-1].set_hash_data_length(length)
self.manifests[-1].add_full_c2pa_structure_exclusion(
offset,
length,
)

super().sync_payload()

Expand Down
43 changes: 43 additions & 0 deletions c2pie/c2pa_injection/jpg_injection.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
from c2pie.c2pa.manifest_store import ManifestStore

# JPG_SEGMENT_MAX_PAYLOAD_LENGTH =
# 65535 (max segment length)
# - 2 (bytes of length)
# - 2 (bytes of CI)
# - 2 (bytes of EN)
# - 4 (bytes of Z)
# - 4 (bytes of LBox)
# - 4 (bytes of TBox)
JPG_SEGMENT_MAX_PAYLOAD_LENGTH = 65517


Expand Down Expand Up @@ -98,3 +108,36 @@ def serialize(self):

self.serialized_length = len(serialized_storage_data)
return serialized_storage_data


def create_and_serialize_app11_storage(
manifest_store: ManifestStore,
) -> bytes:
serialized_manifest_store = manifest_store.serialize()

app11_storage = JpgSegmentApp11Storage(
app11_segment_box_length=manifest_store.get_length(),
app11_segment_box_type=manifest_store.get_type(),
payload=serialized_manifest_store,
)

return app11_storage.serialize()


def emplace_manifest_into_jpeg(
content_bytes: bytes,
manifest_store: ManifestStore,
c2pa_offset: int,
) -> bytes:
serialized_app11_storage = create_and_serialize_app11_storage(manifest_store)

serialized_app11_storage_length = len(serialized_app11_storage)

manifest_store.add_full_c2pa_structure_exclusion(
c2pa_offset,
serialized_app11_storage_length,
)

tail = create_and_serialize_app11_storage(manifest_store)

return content_bytes[:c2pa_offset] + tail + content_bytes[c2pa_offset:]
Loading
Loading