diff --git a/data/fixtures/README.md b/data/fixtures/README.md index ba55f98..4ff10cd 100644 --- a/data/fixtures/README.md +++ b/data/fixtures/README.md @@ -226,6 +226,27 @@ software agent `Runway Image Generation` 4.0.0 or `Runway Video Generation` - License: repository license; generated and explicitly cleared by the maintainer +## Amazon Bedrock Nova Canvas image and Nova Reel video + +Third-party sample outputs published in +[`aws-samples/amazon-nova-samples`](https://github.com/aws-samples/amazon-nova-samples) +at commit `ad2fe2074cbc260b9f1dbcec94a4ec3561c1159a`, copied unmodified. + +| File | Upstream path | Size | SHA-256 | +| --- | --- | --- | --- | +| `amazon-bedrock-nova-canvas.png` | `multimodal-embeddings/getting-started/sample_assets/images/dog.png` | 1280 x 720 PNG | `b2d38079222ca8499f2cbe3adca3f5521add6b383c99bf28d61021503bb3b7c1` | +| `amazon-bedrock-nova-reel.mp4` | `multimodal-understanding/workshop/1-getting-started-with-nova-2/video/cell-phone.mp4` | 1280 x 720 H.264, 24 fps, 6 s | `36d2d68f9be53d9f123bd16127c6fbcbd838faa57cfdb28af44a515458a3cf64` | + +Both carry a valid C2PA manifest signed by `Amazon Web Services, Inc.` with claim +generator `Amazon Bedrock`, recording one `c2pa.created` action typed +`trainedAlgorithmicMedia` with software agent `Nova Canvas` or `Nova Reel 1.1`. +The image was reported as Canva before the Amazon vendor row existed: the byte +fallback found the `Canva` token inside `Nova Canvas`. + +- Expected signal: C2PA AI-generated, platform `Amazon Bedrock (Nova)` +- License: MIT No Attribution, see + [`../licenses/aws-samples-amazon-nova-MIT-0.txt`](../licenses/aws-samples-amazon-nova-MIT-0.txt) + ## Higgsfield and Dreamina downloads Original downloads generated by the maintainer on paid Higgsfield and Dreamina diff --git a/data/fixtures/provenance/amazon-bedrock-nova-canvas.png b/data/fixtures/provenance/amazon-bedrock-nova-canvas.png new file mode 100644 index 0000000..a12e0e6 Binary files /dev/null and b/data/fixtures/provenance/amazon-bedrock-nova-canvas.png differ diff --git a/data/fixtures/provenance/amazon-bedrock-nova-reel.mp4 b/data/fixtures/provenance/amazon-bedrock-nova-reel.mp4 new file mode 100644 index 0000000..b5d0870 Binary files /dev/null and b/data/fixtures/provenance/amazon-bedrock-nova-reel.mp4 differ diff --git a/data/licenses/aws-samples-amazon-nova-MIT-0.txt b/data/licenses/aws-samples-amazon-nova-MIT-0.txt new file mode 100644 index 0000000..09951d9 --- /dev/null +++ b/data/licenses/aws-samples-amazon-nova-MIT-0.txt @@ -0,0 +1,17 @@ +MIT No Attribution + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/docs/module-internals.md b/docs/module-internals.md index 431b2a6..45b6c4a 100644 --- a/docs/module-internals.md +++ b/docs/module-internals.md @@ -663,6 +663,10 @@ metadata extraction from verdict logic: compressed data one turns up by chance, and the entry it hits may assert AI. The trim happens only when the container parses -- a malformed or unknown one is left whole, because dropping real evidence to avoid a chance match is the wrong trade. + When the reader could not open the manifest, the issuer and signer scan narrows + further to the manifest store the container locates (`_c2pa_store_bytes`: JPEG + APP11, PNG `caBX`): an XMP packet names Adobe in its toolkit string, and a vivo + X300 capture was labeled "C2PA signer: Adobe" from it (2026-09-26). - `identify_from_evidence` evaluates that evidence without reopening the source. Rules that decide a verdict live here, not in extraction: extraction has two implementations, and a rule in only one of them is a rule the other lacks. The diff --git a/docs/research-sweep-2026-09.md b/docs/research-sweep-2026-09.md index 1208abb..920a13f 100644 --- a/docs/research-sweep-2026-09.md +++ b/docs/research-sweep-2026-09.md @@ -42,7 +42,11 @@ This page is an index. Each finding lives in its canonical page. records SynthID in its manifest. - **Vidu and Wan marks on more samples.** Both are registered from one real export each (`vidu` video, `wan` image); their gates are provisional until a - wider cohort is measured, and a Wan video label is not yet captured. + wider cohort is measured, and a Wan video label is not yet captured. Public + Vidu feed videos (2026-09-26) carry the TC260 label naming ShengShu, and one + names Tongyi Yunqi as producer with Vidu as propagator, but none shows the + visible `Vidu` mark; Wan's public demo videos show no mark either. Both marks + appear only on account downloads. - **TC260 practice guides.** The audio placements from TC260-PG-202510A are now read (the guide PDFs load in a browser from `tc260.org.cn/tc260/sjzn/list.shtml`, not to scripted requests). The text-file @@ -70,6 +74,14 @@ This page is an index. Each finding lives in its canonical page. real sample per vendor is still needed to confirm the manifest shape before adding a row. Runway is done: its own models sign as `RUNWAY AI, INC.` (measured 2026-09-24) and the row covers the conformance-list spelling too. + Amazon is done: Nova Canvas and Nova Reel outputs published in + `aws-samples/amazon-nova-samples` sign as `Amazon Web Services, Inc.` with + claim generator `Amazon Bedrock` (2026-09-26), and had been reported as Canva + because the byte fallback matched `Canva` inside `Nova Canvas`. A search on + 2026-09-26 found no public Getty, iStock, Jasper, RefaceAI or vivo Albums file + carrying C2PA: their marketing and model-page images are unsigned. A vivo + X300 camera capture carries a vivo device manifest (`digitalCapture`) that + the bundled reader cannot open (`unknown algorithm`). - **Chinese phone galleries.** Huawei's help pages say the Xiaoyi photo-edit AI watermark can be switched off; Honor, OPPO and Xiaomi label behavior is not documented in any primary source found. vivo's Album setting is already diff --git a/docs/supported-signals.md b/docs/supported-signals.md index f40e9ef..8372619 100644 --- a/docs/supported-signals.md +++ b/docs/supported-signals.md @@ -286,6 +286,7 @@ not a universal clean verdict. | Kling AI | Kling AI image and video marks | No registered pixel decoder | TC260 AIGC | | Vidu (ShengShu) | Vidu video mark | No registered pixel decoder | TC260 AIGC in the MP4 `AIGC` key | | Runway | None measured on paid-plan output | No registered pixel decoder | C2PA signed `RUNWAY AI, INC.` on its own models; third-party models keep their vendor's manifest | +| Amazon Bedrock (Nova Canvas, Nova Reel) | None | Invisible watermark readable only through the Bedrock API | C2PA signed `Amazon Web Services, Inc.`, claim generator `Amazon Bedrock` | | Wan (Alibaba) | Wan image mark | No registered pixel decoder | TC260 AIGC in XMP | | Hailuo AI / MiniMax video | Hailuo AI composite video label | No registered pixel decoder | TC260 AIGC where present | | Baidu | Baidu mark | No registered pixel decoder | TC260 AIGC | diff --git a/docs/watermarking-landscape.md b/docs/watermarking-landscape.md index 1b0a09f..dde910d 100644 --- a/docs/watermarking-landscape.md +++ b/docs/watermarking-landscape.md @@ -87,7 +87,7 @@ payloads. Removal remuxes either container through ffmpeg with stream copy. may carry IPTC metadata but no registered C2PA or pixel watermark. The open DWT-DCT decoder only applies when the producing pipeline actually ran its encoder and the carrier remains decodable. -- **Invisible but NOT locally detectable (proprietary, API/oracle only — same wall as SynthID):** Amazon Titan Image Generator + Nova Canvas (Bedrock `DetectGeneratedContent` API; Amazon's Nova Reel service card also claims a watermark on every video), Kakao (new SynthID image adopter, May 2026), NVIDIA Cosmos (SynthID video), ElevenLabs and OpenAI audio (SynthID, 2026), Apple Image Playground and Photos edits (SynthID announced 2026-09-14, not shipped). Microsoft Foundry lists provenance (C2PA and/or invisible watermark; the page does not split them per model) for Azure OpenAI GPT Image, MAI-Image-2.5, Azure-hosted Black Forest Labs Flux (1.1-Pro, Kontext-pro, 2-flex, 2-Pro) and Azure audio models ([Microsoft Learn, 2026-09-17](https://learn.microsoft.com/en-us/azure/foundry/responsible-ai/content-safety/provenance-disclosure)). No local detector possible; treat like SynthID. +- **Invisible but NOT locally detectable (proprietary, API/oracle only — same wall as SynthID):** Amazon Titan Image Generator + Nova Canvas (Bedrock `DetectGeneratedContent` API; Amazon's Nova Reel service card also claims a watermark on every video; both also sign C2PA as `Amazon Web Services, Inc.`, attributed as Amazon Bedrock), Kakao (new SynthID image adopter, May 2026), NVIDIA Cosmos (SynthID video), ElevenLabs and OpenAI audio (SynthID, 2026), Apple Image Playground and Photos edits (SynthID announced 2026-09-14, not shipped). Microsoft Foundry lists provenance (C2PA and/or invisible watermark; the page does not split them per model) for Azure OpenAI GPT Image, MAI-Image-2.5, Azure-hosted Black Forest Labs Flux (1.1-Pro, Kontext-pro, 2-flex, 2-Pro) and Azure audio models ([Microsoft Learn, 2026-09-17](https://learn.microsoft.com/en-us/azure/foundry/responsible-ai/content-safety/provenance-disclosure)). No local detector possible; treat like SynthID. - **C2PA 2.4 "Durable Content Credentials" (April 2026; verified against the spec) raise the bar for metadata stripping.** 2.4 defines soft bindings (an invisible watermark or a content fingerprint) plus a server-side manifest repository and a new `c2pa.repository-receipt` assertion. Per the spec: "if a C2PA manifest is removed from an asset, but a copy of that manifest remains in a provenance store elsewhere, the manifest and asset may be matched using available soft bindings." So our local `metadata --remove` deletes the *embedded* manifest, but a fingerprint/watermark soft binding can still re-link the image to its manifest in a repository server-side. Stripping the file is becoming necessary-but-not-sufficient against durable provenance. (Correction 2026-09-23: 2.3 and 2.4 did change what a parser must read. 2.3 added OGG Vorbis and AVIX embedding; 2.4 added the `c2pa.ai-disclosure` assertion (section 18.28: `modelType`, `modelName`, `modelIdentifier`, `contentProfile.humanOversightLevel`), `digitalSourceType` on ingredient assertions, IFD0 embedding for single-IFD TIFF, and HTML and text embedding. The reader now handles `c2pa.ai-disclosure` and ingredient `digitalSourceType`; HTML and text embedding are out of scope for an image and video tool.) Spec: https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html We READ the soft-binding `alg`, preserve the structured assertion's signed `value`, and generate the display/type registry from the official C2PA JSON with `scripts/sync_c2pa_soft_bindings.py`. The packaged snapshot keeps runtime inspection offline and pins the exact upstream revision. Registration is name-only support: Adobe TrustMark remains the one locally decoded scheme (`trustmark_detector`) unless another compatible decoder is independently verified. Fingerprint entries remain re-linkability signals and are not mislabeled as pixel watermarks. - **Microsoft Paint and Photos InvisMark (reverse-engineered 2026-08-20):** Paint receives a per-generation GUID from remote prompt moderation, embeds it into locally generated pixels, and records the same value in `c2pa.soft-binding` under `com.microsoft.invismark.1`. The C2PA soft-binding registry independently identifies that algorithm as Microsoft Responsible AI InvisMark for image and video. Paint's 144-bit writer framing does not match the public repository's 100-bit pretrained checkpoint interface, so compatibility is not assumed. The parser reports the signed identifier; there is no validated local pixel decoder. Microsoft's external Content Provenance Detection API is the removal oracle because it reports `Watermark` separately from `C2PA`; a metadata-stripped, pixel-identical control must remain watermark-positive before an output-negative result is attributed to pixel removal. The no-account web page for the same check is ; its clean outcome is rendered as `Inconclusive`, and that public provider oracle is the surface used for the measured removal certification. Sources: https://xusheng.dev/posts/reversing/mspaint_invisible_watermark/main/, https://github.com/c2pa-org/softbinding-algorithm-list/blob/main/softbinding-algorithm-list.json, and https://learn.microsoft.com/en-us/azure/ai-services/content-safety/how-to/how-to-provenance-detection - **Built in the dated batch:** soft-binding vendor detection, IPTC Photo diff --git a/src/remove_ai_watermarks/_internal/constants.py b/src/remove_ai_watermarks/_internal/constants.py index b4cb35d..9556251 100644 --- a/src/remove_ai_watermarks/_internal/constants.py +++ b/src/remove_ai_watermarks/_internal/constants.py @@ -106,6 +106,11 @@ C2PA_AI_VENDORS: tuple[C2paAiVendor, ...] = ( "Dreamina", asserts_ai=True, ), + # Amazon Bedrock signs Nova Canvas images and Nova Reel video as "Amazon Web + # Services, Inc." with claim generator "Amazon Bedrock" (aws-samples outputs, + # checked 2026-09-26). Listed before Canva: the Nova Canvas software agent + # contains the Canva token, so a byte scan finds both. + _vendor(b"Amazon Web Services", "Amazon", "Amazon Bedrock (Nova)", "Amazon"), _vendor(b"Canva", "Canva", "Canva (Magic Media)", "Canva"), _vendor(b"Eleven Labs", "ElevenLabs", "ElevenLabs", "ElevenLabs"), _vendor(b"fal-ai", "fal.ai", "fal.ai", "fal.ai", asserts_ai=True), diff --git a/src/remove_ai_watermarks/identify.py b/src/remove_ai_watermarks/identify.py index 96cd343..957ab4e 100644 --- a/src/remove_ai_watermarks/identify.py +++ b/src/remove_ai_watermarks/identify.py @@ -41,6 +41,7 @@ from remove_ai_watermarks._internal.c2pa import ( from remove_ai_watermarks._internal.constants import ( C2PA_AI_TOOLS, C2PA_AI_VENDORS, + C2PA_CHUNK_TYPE, C2PA_CLAIM_GENERATOR_PLATFORMS, C2PA_IDENTITY_AI_ORGS, C2PA_ISSUERS, @@ -731,6 +732,40 @@ def _metadata_region(head: bytes) -> bytes: return head +def _c2pa_store_bytes(head: bytes, fallback: bytes) -> bytes: + """The C2PA manifest-store bytes (JPEG APP11, PNG ``caBX``) of ``head``, else ``fallback``. + + The issuer registry is matched by substring, and a metadata region also holds + XMP, whose boilerplate names Adobe (``Adobe XMP Core``, ``ns.adobe.com``): a + vivo X300 camera capture whose manifest the reader could not open was labeled + "C2PA signer: Adobe" from its XMP packet. Signer identity lives in the store, so + the fallback scan reads only the store where the container locates it. ``head`` + must be the raw buffer: the trimmed metadata region has lost the PNG framing. + """ + size = min(len(head), _SCAN_BYTES) + out = bytearray() + if head.startswith(b"\xff\xd8"): + index = 2 + while index + 4 <= size and head[index] == 0xFF: + marker = head[index + 1] + if marker in (0xDA, 0xD9): + break + length = int.from_bytes(head[index + 2 : index + 4], "big") + if length < 2: + break + if marker == 0xEB: + out += head[index + 4 : min(index + 2 + length, size)] + index += 2 + length + elif head.startswith(b"\x89PNG\r\n\x1a\n"): + position = 8 + while position + 8 <= size: + (length,) = struct.unpack(">I", head[position : position + 4]) + if head[position + 4 : position + 8] == C2PA_CHUNK_TYPE: + out += head[position + 8 : min(position + 8 + length, size)] + position += 12 + length + return bytes(out) or fallback + + def _first_token_match(head: bytes, table: tuple[tuple[bytes, str], ...]) -> str | None: """First platform in ``table`` whose token appears in ``head``, else None. @@ -1225,8 +1260,9 @@ def _identify_from_evidence( # The reader already named which failures moved a dimension; re-deriving that here # by substring made the displayed reason a second, looser rule than the verdict. failed_c2pa_codes = [str(code) for code in cast("list[object]", info.get("c2pa_failed_codes", []))] - issuers = [info["issuer"]] if info.get("issuer") else _issuers_in(region) - signer_label = _signer_platform(region, issuers) + store = _c2pa_store_bytes(head, region) + issuers = [info["issuer"]] if info.get("issuer") else _issuers_in(store) + signer_label = _signer_platform(store, issuers) # Full AI generation (trainedAlgorithmicMedia) vs an AI-enhanced real photo # (compositeWithTrainedAlgorithmicMedia). The structured kind is parsed once in # _internal.c2pa._structured_manifest_fields (covers PNG + any container the c2pa-python @@ -1368,7 +1404,7 @@ def _identify_from_evidence( and trained_source and c2pa_marker_in(head) and not soft_binding_blocks_synthid - and (vendors := synthid_evidence_vendors_in(region)) + and (vendors := synthid_evidence_vendors_in(store)) ): synthid = synthid_verdict(", ".join(vendors)) if synthid: diff --git a/tests/test_identify.py b/tests/test_identify.py index 7f959b0..d884ca5 100644 --- a/tests/test_identify.py +++ b/tests/test_identify.py @@ -883,6 +883,43 @@ class TestIdentifyRealSamples: assert any("SynthID" in watermark for watermark in r.watermarks) assert any("Google Photos AI edit" in caveat for caveat in r.caveats) + def test_amazon_bedrock_nova_canvas_is_not_canva(self): + # aws-samples Nova Canvas output: valid C2PA signed "Amazon Web Services, + # Inc.". Before the Amazon row, the byte fallback matched "Canva" inside the + # "Nova Canvas" software agent and reported Canva (Magic Media). + r = identify(SAMPLES_DIR / "amazon-bedrock-nova-canvas.png", check_visible=False, check_invisible=False) + assert r.is_ai_generated is True + assert r.platform == "Amazon Bedrock (Nova)" + assert not any("Canva" in watermark for watermark in r.watermarks) + + def test_xmp_boilerplate_does_not_name_a_c2pa_signer(self): + # A vivo X300 capture whose manifest the reader could not open was labeled + # "C2PA signer: Adobe" from its XMP packet ("Adobe XMP Core"). The fallback + # scan reads the manifest store (APP11), not the whole metadata region. + from remove_ai_watermarks.identify import _c2pa_store_bytes, _issuers_in, _metadata_region + + def segment(marker: int, payload: bytes) -> bytes: + return bytes([0xFF, marker]) + (len(payload) + 2).to_bytes(2, "big") + payload + + xmp = b"http://ns.adobe.com/xap/1.0/\x00" + store = b"JP\x00\x01jumbjumdc2pa vivo C2PA Device CA1 c2pa.claim.v2" + jpeg = b"\xff\xd8" + segment(0xE1, xmp) + segment(0xEB, store) + b"\xff\xda\x00\x02" + b"\x00" * 16 + + region = _metadata_region(jpeg) + assert "Adobe" in _issuers_in(region) + assert _issuers_in(_c2pa_store_bytes(jpeg, region)) == [] + + def test_png_store_is_read_from_the_raw_buffer(self): + # The trimmed region drops the PNG signature and chunk lengths, so the store + # must come from the raw buffer to narrow a PNG at all. + from remove_ai_watermarks.identify import _c2pa_store_bytes, _metadata_region + + head = (SAMPLES_DIR / "amazon-bedrock-nova-canvas.png").read_bytes() + region = _metadata_region(head) + store = _c2pa_store_bytes(head, region) + assert b"Amazon Web Services" in store + assert len(store) < len(region) // 4 + def test_apple_image_playground_attributed(self): # Real Image Playground export (2026-09-23): XMP photoshop:Credit plus the IPTC # digitalSourceType, no C2PA. The credit names the platform; the IPTC diff --git a/tests/test_video.py b/tests/test_video.py index 6d34968..3ee6faf 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -3236,6 +3236,15 @@ class TestYouTubeReencodedSamples: assert contradicts_video_provenance("kling", veo.metadata_markers) assert not contradicts_video_provenance("kling", kling.metadata_markers) + def test_amazon_bedrock_nova_reel_is_attributed(self): + # aws-samples Nova Reel 1.1 output: C2PA from Amazon Web Services with + # claim generator "Amazon Bedrock"; before the vendor row the platform was None. + from remove_ai_watermarks.video import identify_video + + report = identify_video(self._DIR / "amazon-bedrock-nova-reel.mp4", check_visible=False) + assert report.is_ai_generated is True + assert report.platform == "Amazon Bedrock (Nova)" + def test_original_grok_video_is_xai_without_synthid(self): from remove_ai_watermarks.video import identify_video