Unsere Backstube öffnet um sechs.
\n\n\n" +) + + +def _manifest(source_type: str) -> dict[str, object]: + return { + "claim_generator_info": [{"name": "markproof-fixtures", "version": "1"}], + "title": "index.html", + "format": "text/html", + "assertions": [ + { + "label": "c2pa.actions", + "data": {"actions": [{"action": "c2pa.created", "digitalSourceType": source_type}]}, + }, + { + "label": "c2pa.ai-disclosure", + "data": {"modelType": "text", "humanOversightLevel": "prompt_guided"}, + }, + ], + } + + +def _sign(document: bytes, source_type: str) -> bytes: + """A detached manifest bound to these exact bytes. + + ``set_no_embed`` is what makes this possible at all: c2pa-rs refuses to embed + into HTML, but it will hash the bytes and hand back a manifest that stands + beside them — which is exactly the A.7 arrangement. + """ + with media._builder_context(): + builder = c2pa.Builder(_manifest(source_type)) + builder.set_no_embed() + sink = io.BytesIO() + signed: bytes = builder.sign(media.build_signer(), "text/html", io.BytesIO(document), sink) + return signed + + +def write() -> None: + document = PAGE.encode("utf-8") + + (_HERE / "signed-valid.html").write_bytes(document) + (_HERE / "signed-valid.html.c2pa").write_bytes(_sign(document, _AI)) + + # Same manifest, document edited afterwards. Four characters, no visible + # damage, and the provenance claim no longer holds. + (_HERE / "tampered.html").write_bytes(document.replace(b"1932", b"1888")) + + wrong = document.replace(b"Mustermann", b"Musterfrau") + (_HERE / "signed-wrong-type.html").write_bytes(wrong) + (_HERE / "signed-wrong-type.html.c2pa").write_bytes(_sign(wrong, _CAMERA)) + + (_HERE / "unsigned.html").write_bytes( + document.replace(b'\n', b"") + ) + + (_HERE / "MANIFEST.json").write_text( + json.dumps( + { + "_comment": "Generated by generate.py. Signatures are not reproducible.", + "c2pa_python": c2pa.version("c2pa-python"), + "c2pa_rs_sdk": c2pa.sdk_version(), + "cases": { + "signed-valid.html": "manifest bound to these bytes, AI source type -> PASS", + "tampered.html": "signed-valid's manifest, document edited after -> FAIL", + "signed-wrong-type.html": "validly bound, declares a camera capture -> FAIL", + "unsigned.html": "no manifest and no link to one -> FAIL", + }, + }, + indent=2, + ) + + "\n" + ) + + +if __name__ == "__main__": + write() + for path in sorted(_HERE.glob("*")): + if path.name != "generate.py": + print(f" {path.name} {path.stat().st_size} B") diff --git a/tests/fixtures/documents/signed-valid.html b/tests/fixtures/documents/signed-valid.html new file mode 100644 index 0000000..6c62baa --- /dev/null +++ b/tests/fixtures/documents/signed-valid.html @@ -0,0 +1,11 @@ + + + +Unsere Backstube öffnet um sechs.
+ + diff --git a/tests/fixtures/documents/signed-valid.html.c2pa b/tests/fixtures/documents/signed-valid.html.c2pa new file mode 100644 index 0000000..8cc8381 Binary files /dev/null and b/tests/fixtures/documents/signed-valid.html.c2pa differ diff --git a/tests/fixtures/documents/signed-wrong-type.html b/tests/fixtures/documents/signed-wrong-type.html new file mode 100644 index 0000000..8f56f60 --- /dev/null +++ b/tests/fixtures/documents/signed-wrong-type.html @@ -0,0 +1,11 @@ + + + +Unsere Backstube öffnet um sechs.
+ + diff --git a/tests/fixtures/documents/signed-wrong-type.html.c2pa b/tests/fixtures/documents/signed-wrong-type.html.c2pa new file mode 100644 index 0000000..983c5de Binary files /dev/null and b/tests/fixtures/documents/signed-wrong-type.html.c2pa differ diff --git a/tests/fixtures/documents/tampered.html b/tests/fixtures/documents/tampered.html new file mode 100644 index 0000000..e779baa --- /dev/null +++ b/tests/fixtures/documents/tampered.html @@ -0,0 +1,11 @@ + + + +Unsere Backstube öffnet um sechs.
+ + diff --git a/tests/fixtures/documents/unsigned.html b/tests/fixtures/documents/unsigned.html new file mode 100644 index 0000000..9462049 --- /dev/null +++ b/tests/fixtures/documents/unsigned.html @@ -0,0 +1,10 @@ + + + +Unsere Backstube öffnet um sechs.
+ + diff --git a/tests/golden/chat-conformant/expected_report.json b/tests/golden/chat-conformant/expected_report.json index 64e13c5..94e0b3d 100644 --- a/tests/golden/chat-conformant/expected_report.json +++ b/tests/golden/chat-conformant/expected_report.json @@ -70,7 +70,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/chat-near-miss/expected_report.json b/tests/golden/chat-near-miss/expected_report.json index 858a315..b501ca8 100644 --- a/tests/golden/chat-near-miss/expected_report.json +++ b/tests/golden/chat-near-miss/expected_report.json @@ -72,7 +72,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/chat-silent/expected_report.json b/tests/golden/chat-silent/expected_report.json index 3cab8e1..7b2c356 100644 --- a/tests/golden/chat-silent/expected_report.json +++ b/tests/golden/chat-silent/expected_report.json @@ -64,7 +64,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/document-marked/evidence.json b/tests/golden/document-marked/evidence.json new file mode 100644 index 0000000..37ec9e5 --- /dev/null +++ b/tests/golden/document-marked/evidence.json @@ -0,0 +1,36 @@ +{ + "description": "An HTML page bound to an external C2PA manifest. The format cannot embed one, so the binding is a hash over the delivered bytes.", + "evidences": [ + { + "lang": "de", + "probe_id": "page", + "probe_kind": "document", + "target_name": "golden", + "turns": [ + { + "artifacts": [ + { + "_fixture": "documents/signed-valid.html", + "_sidecar": "documents/signed-valid.html.c2pa", + "id": "document-fetch-document", + "media_type": "text/html", + "sha256": "8cc23ef8d1e33877c433382a4793455df5609c8615951526079a4f8e5fbea960", + "sidecar_source": "link-header", + "size_bytes": 257, + "source_url": "https://pages.example.invalid/index.html" + } + ], + "prompt_id": "document-fetch", + "request": [], + "response": { + "content": "257 byte(s) of text/html", + "role": "assistant" + }, + "response_sha256": "d672e483243ab1440245ba95fd03f26b36a1ad7ec51371407043e0f2407d4371", + "status_code": 200 + } + ] + } + ], + "rulepack": "art50-eu-2026.07" +} diff --git a/tests/golden/document-marked/expected_report.json b/tests/golden/document-marked/expected_report.json new file mode 100644 index 0000000..dde4359 --- /dev/null +++ b/tests/golden/document-marked/expected_report.json @@ -0,0 +1,45 @@ +{ + "findings": [ + { + "article": "Art. 50(2)", + "detail": { + "assets": [ + "document-fetch-document" + ], + "checked": 1, + "outcome": "verified" + }, + "evidence_sha256": [ + "8cc23ef8d1e33877c433382a4793455df5609c8615951526079a4f8e5fbea960" + ], + "guideline_ref": "Guidelines C(2026) 5054, \u00a74; Code of Practice, Section 1, Commitment 1", + "message": "1 asset carries a valid, correctly marked manifest", + "obligation": "synthetic-text-marking", + "probe_id": "page", + "result": "PASS", + "rule_id": "MPF-M-002", + "title": "A delivered document carries a manifest binding it as AI-generated" + } + ], + "rulepack": { + "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", + "id": "art50-eu-2026.07", + "license": "CC-BY-4.0", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", + "version": "1.0.0" + }, + "run": { + "markproof_version": "0.0.0-golden", + "platform": "golden", + "python_version": "0.0.0", + "timestamp": "2026-08-31T12:00:00+00:00" + }, + "schema_version": 1, + "summary": { + "failed": 0, + "passed": 1, + "skipped": 0, + "warned": 0 + }, + "target": "golden" +} diff --git a/tests/golden/document-rewritten/evidence.json b/tests/golden/document-rewritten/evidence.json new file mode 100644 index 0000000..f97d777 --- /dev/null +++ b/tests/golden/document-rewritten/evidence.json @@ -0,0 +1,36 @@ +{ + "description": "The same manifest, four characters edited afterwards. The page still renders perfectly and the provenance claim no longer holds — the silent delivery-chain regression this project exists to catch.", + "evidences": [ + { + "lang": "de", + "probe_id": "page", + "probe_kind": "document", + "target_name": "golden", + "turns": [ + { + "artifacts": [ + { + "_fixture": "documents/tampered.html", + "_sidecar": "documents/signed-valid.html.c2pa", + "id": "document-fetch-document", + "media_type": "text/html", + "sha256": "166c7e32179b19ee0a4f41168fa297a6f3673c72e9b1d12173bba98d6e1a11c3", + "sidecar_source": "link-header", + "size_bytes": 257, + "source_url": "https://pages.example.invalid/index.html" + } + ], + "prompt_id": "document-fetch", + "request": [], + "response": { + "content": "257 byte(s) of text/html", + "role": "assistant" + }, + "response_sha256": "d672e483243ab1440245ba95fd03f26b36a1ad7ec51371407043e0f2407d4371", + "status_code": 200 + } + ] + } + ], + "rulepack": "art50-eu-2026.07" +} diff --git a/tests/golden/document-rewritten/expected_report.json b/tests/golden/document-rewritten/expected_report.json new file mode 100644 index 0000000..56fd878 --- /dev/null +++ b/tests/golden/document-rewritten/expected_report.json @@ -0,0 +1,48 @@ +{ + "findings": [ + { + "article": "Art. 50(2)", + "detail": { + "assets": [ + "document-fetch-document" + ], + "checked": 1, + "failed_assets": [ + "document-fetch-document" + ], + "outcome": "invalid" + }, + "evidence_sha256": [ + "166c7e32179b19ee0a4f41168fa297a6f3673c72e9b1d12173bba98d6e1a11c3" + ], + "guideline_ref": "Guidelines C(2026) 5054, \u00a74; Code of Practice, Section 1, Commitment 1", + "message": "1 of 1 asset(s) failed: manifest present but validation failed \u2014 the asset was altered after signing (assertion.dataHash.mismatch)", + "obligation": "synthetic-text-marking", + "probe_id": "page", + "result": "FAIL", + "rule_id": "MPF-M-002", + "title": "A delivered document carries a manifest binding it as AI-generated" + } + ], + "rulepack": { + "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", + "id": "art50-eu-2026.07", + "license": "CC-BY-4.0", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", + "version": "1.0.0" + }, + "run": { + "markproof_version": "0.0.0-golden", + "platform": "golden", + "python_version": "0.0.0", + "timestamp": "2026-08-31T12:00:00+00:00" + }, + "schema_version": 1, + "summary": { + "failed": 1, + "passed": 0, + "skipped": 0, + "warned": 0 + }, + "target": "golden" +} diff --git a/tests/golden/generate.py b/tests/golden/generate.py index 4d36340..9274488 100644 --- a/tests/golden/generate.py +++ b/tests/golden/generate.py @@ -60,6 +60,33 @@ def _turn(prompt_id: str, response: str, *, user_said: str | None = None) -> dic } +def _document_turn(prompt_id: str, document: str, manifest: str | None) -> dict[str, Any]: + """A fetched document plus the manifest bound to it, if there is one.""" + import hashlib + + data = (_FIXTURES / document).read_bytes() + summary = f"{len(data)} byte(s) of text/html" + artifact: dict[str, Any] = { + "id": f"{prompt_id}-document", + "media_type": "text/html", + "size_bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + "source_url": "https://pages.example.invalid/index.html", + "sidecar_source": "link-header" if manifest else None, + "_fixture": document, + } + if manifest: + artifact["_sidecar"] = manifest + return { + "prompt_id": prompt_id, + "request": [], + "response": {"role": "assistant", "content": summary}, + "response_sha256": hashlib.sha256(summary.encode()).hexdigest(), + "status_code": 200, + "artifacts": [artifact], + } + + def _media_turn(prompt_id: str, fixture: str, media_type: str) -> dict[str, Any]: import hashlib @@ -190,6 +217,51 @@ def _media_turn(prompt_id: str, fixture: str, media_type: str) -> dict[str, Any] } ], }, + "document-marked": { + "description": ( + "An HTML page bound to an external C2PA manifest. The format cannot " + "embed one, so the binding is a hash over the delivered bytes." + ), + "rulepack": "art50-eu-2026.07", + "evidences": [ + { + "probe_id": "page", + "probe_kind": "document", + "target_name": "golden", + "lang": "de", + "turns": [ + _document_turn( + "document-fetch", + "documents/signed-valid.html", + "documents/signed-valid.html.c2pa", + ) + ], + } + ], + }, + "document-rewritten": { + "description": ( + "The same manifest, four characters edited afterwards. The page still " + "renders perfectly and the provenance claim no longer holds — the " + "silent delivery-chain regression this project exists to catch." + ), + "rulepack": "art50-eu-2026.07", + "evidences": [ + { + "probe_id": "page", + "probe_kind": "document", + "target_name": "golden", + "lang": "de", + "turns": [ + _document_turn( + "document-fetch", + "documents/tampered.html", + "documents/signed-valid.html.c2pa", + ) + ], + } + ], + }, "probe-unreachable": { "description": ( "The endpoint could not be reached. FAIL, never a silent pass — and the " diff --git a/tests/golden/media-marked/expected_report.json b/tests/golden/media-marked/expected_report.json index c5e6813..77e89e5 100644 --- a/tests/golden/media-marked/expected_report.json +++ b/tests/golden/media-marked/expected_report.json @@ -41,7 +41,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/media-tampered/expected_report.json b/tests/golden/media-tampered/expected_report.json index 06c15a6..d924495 100644 --- a/tests/golden/media-tampered/expected_report.json +++ b/tests/golden/media-tampered/expected_report.json @@ -44,7 +44,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/media-wrong-source-type/expected_report.json b/tests/golden/media-wrong-source-type/expected_report.json index c4353bd..9483645 100644 --- a/tests/golden/media-wrong-source-type/expected_report.json +++ b/tests/golden/media-wrong-source-type/expected_report.json @@ -47,7 +47,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/multi-probe/expected_report.json b/tests/golden/multi-probe/expected_report.json index d858c0f..98437d1 100644 --- a/tests/golden/multi-probe/expected_report.json +++ b/tests/golden/multi-probe/expected_report.json @@ -162,7 +162,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/probe-unreachable/expected_report.json b/tests/golden/probe-unreachable/expected_report.json index 223da39..493fd9d 100644 --- a/tests/golden/probe-unreachable/expected_report.json +++ b/tests/golden/probe-unreachable/expected_report.json @@ -17,7 +17,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/golden/scope-declared-out/expected_report.json b/tests/golden/scope-declared-out/expected_report.json index 0306eb4..c52250e 100644 --- a/tests/golden/scope-declared-out/expected_report.json +++ b/tests/golden/scope-declared-out/expected_report.json @@ -54,7 +54,7 @@ "attribution": "Derived from: European Commission, \"Guidelines on the implementation of the transparency obligations for certain AI systems under Article 50 of Regulation (EU) 2024/1689 (the AI Act)\", C(2026) 5054 final, ANNEX, 20.07.2026, and the \"Code of Practice on Transparency of AI-generated Content\", 10.06.2026 \u2014 both published by the European Commission under CC BY 4.0 (Commission Decision 2011/833/EU). Rules are paraphrased and cite the paragraph numbers of the source; refer to the cited paragraphs for the authoritative text. Neither the European Commission nor the AI Board endorses this rulepack or the reading of their material expressed in it.", "id": "art50-eu-2026.07", "license": "CC-BY-4.0", - "sha256": "d8c9be8eab221fbd654d987c71d12b24d61ed4587e0537b1f799b0701d8c9ccb", + "sha256": "e644cbac5b75202f2308ea6fafd3c6dbc72cedee01d67c6338b654a5fd734994", "version": "1.0.0" }, "run": { diff --git a/tests/test_determinism.py b/tests/test_determinism.py index c8442e8..878192e 100644 --- a/tests/test_determinism.py +++ b/tests/test_determinism.py @@ -68,8 +68,13 @@ def _evidence_with_bytes(raw: dict[str, Any]) -> Evidence: for art in turn.get("artifacts", []): art = dict(art) fixture = art.pop("_fixture", None) + sidecar = art.pop("_sidecar", None) artifacts.append( - Artifact(**art, data=(_FIXTURES / fixture).read_bytes() if fixture else None) + Artifact( + **art, + data=(_FIXTURES / fixture).read_bytes() if fixture else None, + sidecar_manifest=(_FIXTURES / sidecar).read_bytes() if sidecar else None, + ) ) turns.append({**turn, "artifacts": tuple(artifacts)}) return Evidence.model_validate({**raw, "turns": tuple(turns)}) diff --git a/tests/test_document_probe.py b/tests/test_document_probe.py new file mode 100644 index 0000000..8eb8014 --- /dev/null +++ b/tests/test_document_probe.py @@ -0,0 +1,236 @@ +# SPDX-FileCopyrightText: 2026 Lukas Friedrich / Tippel +# SPDX-License-Identifier: Apache-2.0 +"""The document probe and MPF-M-002 — provenance for what cannot embed it. + +C2PA binds a manifest to a document by hashing it. A format that cannot carry the +manifest inside — HTML above all — points at an external one through an RFC 8288 +``Link:`` response header or a ```` element +(C2PA 2.4 §A.7, §15.5.3.2). + +The binding is a hash over the bytes the server sent, which is exactly why this is +worth checking rather than assuming: a minifier, an HTML-rewriting CDN or a +template change turns a valid provenance claim into an invalid one while the page +still renders perfectly. Nothing errors and no log line appears. +""" + +from __future__ import annotations + +from pathlib import Path + +import httpx +import pytest +import respx + +from markproof.checks.c2pa_verify import C2paOutcome, C2paResult, verify_media +from markproof.config import DocumentProbeConfig +from markproof.probes.base import ProbeError +from markproof.probes.document import ( + DocumentProbe, + manifest_link_from_header, + manifest_link_from_html, +) +from markproof.rules.schema import C2paVerifyCheck + +_FIXTURES = Path(__file__).resolve().parent / "fixtures" / "documents" +_URL = "https://pages.example.invalid/index.html" +_MANIFEST_URL = "https://pages.example.invalid/index.html.c2pa" + + +def _fixture(name: str) -> bytes: + return (_FIXTURES / name).read_bytes() + + +def _probe(**overrides: object) -> DocumentProbe: + return DocumentProbe( + DocumentProbeConfig.model_validate( + {"id": "page", "type": "document", "url": _URL, **overrides} + ) + ) + + +class TestFindingTheManifest: + """Two ways a document can point at its manifest, and they are not equivalent.""" + + def test_a_link_header(self) -> None: + assert ( + manifest_link_from_header('