This commit is contained in:
Matěj Kubíček
2026-09-02 20:38:45 +02:00
parent 0c524ed6af
commit c7948a788a
46 changed files with 10099 additions and 152 deletions
+4 -137
View File
@@ -1,138 +1,5 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
dist/
artifacts/
coverage/
*.log
+16 -13
View File
@@ -1,18 +1,21 @@
MIT License
Copyright (c) 2026 egmont11
Copyright (c) 2026 SlopDetect contributors
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, subject to the
following conditions:
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, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
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.
+73 -2
View File
@@ -1,3 +1,74 @@
# Slop-detect
# SlopDetect
Firefox rozšíření pro detekci AI generovaného obsahu.
SlopDetect is a privacy-first Firefox extension that inspects webpages for machine-readable AI provenance and separately flags clusters of common generated-site patterns.
It currently provides:
- Automatic scanning of ordinary HTTP and HTTPS pages after load
- An in-page result card for flagged pages that collapses into a persistent score chip
- Local C2PA/Content Credentials parsing and validation for accessible images
- Recognition of AI-related C2PA digital source types
- Separate states for valid, detected, invalid, disclosed, inaccessible, and unknown evidence
- Publisher-disclosure discovery in HTML metadata, structured data, and visible labels
- A bounded, on-device heuristic scan for common visual, typography, layout, motion, and copy patterns
- Unsigned generator-metadata clues for images whose C2PA data was removed but whose export metadata survived
- A mixed-provenance page summary and optional in-page image badges
- A stable adapter API for provider-specific image and text watermark detectors
- On-device processing with no telemetry or remote detector calls
SlopDetect does **not** infer that unmarked content is human-made. Its page-pattern score is a suspicion signal, not proof that AI authored a website. Deliberate human designs can match the same patterns, and generated sites can avoid them.
## Development
Requirements: Node.js 22 or newer and Firefox 128 or newer.
```sh
npm install
npm run check
npm test
npm run lint
npm run build
npm start
```
The unpacked extension is written to `dist/`. A distributable XPI-compatible ZIP is produced in `artifacts/` with:
```sh
npm run package
```
## Architecture
- `src/content/` discovers visible page text, image URLs, disclosures, and bounded rendered-style heuristics, and renders image badges.
- `src/background/` fetches bounded image resources, verifies C2PA manifests, coordinates watermark adapters, caches results, and aggregates page status.
- `src/popup/` presents the current scan and requests optional cross-origin image access.
- `src/options/` manages privacy, badge, and resource-limit settings.
- `src/shared/` contains the message, evidence, and result contracts.
Automatic operation requires persistent access to HTTP and HTTPS pages so the content script can inspect their rendered DOM and the background process can read image bytes. It does not run on Firefox settings pages or other privileged browser documents. Private-network image requests are rejected unless the scanned page has the same origin, redirects are rejected, credentials are omitted, and asset size/concurrency are bounded.
Automatic scans begin shortly after `document_idle`, inspect the largest visible images first, and perform at most one bounded follow-up scan when a page adds substantial dynamic content. The default automatic image limit is 10 with a 10 MB per-image automatic cap; manual toolbar scans can inspect up to 50 using the configured size cap. Pages with no reportable status receive no in-page UI and no toolbar badge. Dismissing an alert keeps it dismissed until the page URL changes.
## Detection semantics
- **Verified AI:** a trusted, valid credential contains an AI-generation/manipulation source type.
- **Detected AI:** a readable credential or compatible watermark indicates AI origin without a trusted signer.
- **Publisher disclosed:** the publisher explicitly labels content as AI-generated.
- **AI-style patterns suspected:** at least two deterministic clues cross the configured suspicion threshold. The page remains origin-unknown.
- **Generator metadata detected:** unsigned image bytes name a known AI image tool or contain a recognizable workflow block. This can be removed or forged.
- **Invalid credential:** provenance data exists but validation reports an integrity or trust failure.
- **No supported evidence:** no compatible evidence was found; origin remains unknown.
Text watermark schemes are currently provider-specific. `src/background/detectors.ts` intentionally ships with an empty registry until a public detector can be integrated and tested. This avoids false claims from generic AI-writing classifiers.
The heuristic catalog currently covers purple/cyan and radial gradients, gradient text, side-tab borders, hairline shadows, extreme radii, icon tiles, oversized and italic-serif heroes, hero eyebrows, overused fonts, nested and repeated card grids, decorative looping motion, marquees, generic SaaS vocabulary, em-dash cadence, manufactured contrast, “theater” framing, and stock product promises. It is inspired by the deterministic patterns documented in the [Impeccable slop catalog](https://impeccable.style/slop/), but implemented independently and conservatively.
## C2PA runtime
The build packages `@contentauth/c2pa-web`, its worker, and its WebAssembly binary locally. Snapshots of the official C2PA signing and timestamp trust lists are also bundled. Remote manifests and OCSP fetching are disabled, so inspecting an asset does not cause the C2PA SDK to contact locations named by that asset. No executable code is loaded from a CDN. The extension CSP permits WebAssembly while continuing to restrict script and worker sources to the extension itself.
`@contentauth/c2pa-web` 0.14.4 applies an HTTPS-only check to explicit worker URLs. Firefox identifies packaged extension resources with `moz-extension:` instead, so `src/background/c2pa.ts` contains a narrow compatibility adapter: the SDK's scheme check sees its required HTTPS shape, while the browser receives and loads the original same-extension worker URL. The adapter rejects non-extension worker locations.
## License
Source code in this repository is available under the MIT License. Third-party packages retain their respective licenses.
+5
View File
@@ -0,0 +1,5 @@
# Security policy
Please report security issues privately to the repository owner rather than opening a public issue containing exploit details.
SlopDetect treats webpage content, resource URLs, metadata, and provenance manifests as untrusted input. The implementation bounds asset size and concurrency, rejects unsafe URL schemes and cross-origin private-network targets, omits credentials from media requests, disallows redirects, validates extension messages, and renders page annotations through an isolated Shadow DOM without injecting untrusted HTML.
+7
View File
@@ -0,0 +1,7 @@
# Third-party notices
SlopDetect uses `@contentauth/c2pa-web` and related Content Authenticity Initiative packages under their published licenses. See the package metadata and the [c2pa-js repository](https://github.com/contentauth/c2pa-js) for the applicable terms.
The bundled `C2PA-TRUST-LIST.pem` and `C2PA-TSA-TRUST-LIST.pem` files are snapshots from the [C2PA Conformance Program public repository](https://github.com/c2pa-org/conformance-public), published under CC BY 4.0. The Coalition for Content Provenance and Authenticity maintains those lists; inclusion does not imply endorsement of SlopDetect.
Other runtime and development dependencies are listed in `package.json` and retain their respective licenses.
+33
View File
@@ -0,0 +1,33 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist/**', 'artifacts/**', 'node_modules/**'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['scripts/**/*.mjs'],
languageOptions: {
globals: { URL: 'readonly' }
}
},
{
files: ['src/**/*.{ts,tsx}', 'tests/**/*.ts'],
languageOptions: {
globals: {
Blob: 'readonly',
browser: 'readonly',
crypto: 'readonly',
document: 'readonly',
fetch: 'readonly',
globalThis: 'readonly',
HTMLElement: 'readonly',
HTMLImageElement: 'readonly',
MutationObserver: 'readonly',
Node: 'readonly',
URL: 'readonly',
window: 'readonly'
}
}
}
);
+6440
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "slopdetect",
"version": "0.3.2",
"private": true,
"description": "A privacy-first Firefox extension for AI provenance and generated-site pattern detection.",
"type": "module",
"scripts": {
"clean": "node scripts/clean.mjs",
"build": "node scripts/build.mjs",
"check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint .",
"package": "npm run build && web-ext build --source-dir dist --artifacts-dir artifacts --overwrite-dest",
"start": "npm run build && web-ext run --source-dir dist"
},
"dependencies": {
"@contentauth/c2pa-web": "0.14.4",
"preact": "10.29.8",
"webextension-polyfill": "0.12.0"
},
"devDependencies": {
"@eslint/js": "10.0.1",
"@types/firefox-webext-browser": "143.0.0",
"@types/webextension-polyfill": "0.12.6",
"esbuild": "0.28.2",
"eslint": "10.9.1",
"typescript": "6.0.3",
"typescript-eslint": "8.69.0",
"vitest": "4.1.11",
"web-ext": "10.6.0"
},
"engines": {
"node": ">=22"
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
<rect width="96" height="96" rx="22" fill="#17152a"/>
<path d="M48 17 76 29v19c0 18-11 29-28 34C31 77 20 66 20 48V29l28-12Z" fill="#7c5cff"/>
<path d="m33 49 10 10 21-23" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

+44
View File
@@ -0,0 +1,44 @@
{
"manifest_version": 3,
"name": "SlopDetect",
"version": "0.3.2",
"description": "Automatically inspect AI provenance and flag common generated-site patterns.",
"browser_specific_settings": {
"gecko": {
"id": "slopdetect@local",
"strict_min_version": "140.0",
"data_collection_permissions": {
"required": ["none"]
}
},
"gecko_android": {
"strict_min_version": "142.0"
}
},
"permissions": ["storage"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"],
"run_at": "document_idle"
}],
"background": {
"scripts": ["background.js"]
},
"action": {
"default_title": "Scan this page with SlopDetect",
"default_popup": "popup.html",
"default_icon": "icons/icon.svg"
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"icons": {
"48": "icons/icon.svg",
"96": "icons/icon.svg"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; object-src 'self'"
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SlopDetect settings</title>
</head>
<body>
<div id="app"></div>
<script src="options.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SlopDetect</title>
</head>
<body>
<div id="app"></div>
<script src="popup.js"></script>
</body>
</html>
+656
View File
@@ -0,0 +1,656 @@
Subject CN=Google C2PA Media Services 1P ICA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIIC3DCCAmOgAwIBAgIUQfqlIUd2IVjaf5ss/439Fgke7j4wCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjI2WhcNMzAwNTA4MjIz
NjI2WjBRMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEtMCsGA1UE
AwwkR29vZ2xlIEMyUEEgTWVkaWEgU2VydmljZXMgMVAgSUNBIEczMHYwEAYHKoZI
zj0CAQYFK4EEACIDYgAEuCPlUxSiltqnB2lx2ES7FK+TVZWmAxRzzDjTzKZ8umoq
yvCqSLOkZBrOieaLqrp+rnzt0EADWWH3X62NqzEXRewW6rb/lS7VXkVCM02gC0Zg
JW7+PCsZgLoUBUQ+nkN5o4IBCDCCAQQwFwYDVR0gBBAwDjAMBgorBgEEAYPoXgEB
MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSUEGDAWBggrBgEFBQcDBAYKKwYBBAGD6F4C
ATASBgNVHRMBAf8ECDAGAQH/AgEAMGQGCCsGAQUFBwEBBFgwVjAsBggrBgEFBQcw
AoYgaHR0cDovL3BraS5nb29nL2MycGEvcm9vdC1nMy5jcnQwJgYIKwYBBQUHMAGG
Gmh0dHA6Ly9jMnBhLW9jc3AucGtpLmdvb2cvMB8GA1UdIwQYMBaAFJxc2IlTQ+da
1YHbA94ZfwQqKi2qMB0GA1UdDgQWBBTae+G9tCyKheAQ1muax0rx+t/2NzAKBggq
hkjOPQQDAwNnADBkAjACxtEE3NW13bwN1u/51ericNF6rkEhYVESDO6Jqb5cX37H
wg0X9S2rH+vXaoFZIHsCMC03wCKKomDHgqV47UtyyHpZlo5IZACW72Xdc4gipdWM
EmhvPk88dvxbYtn+LVd9zA==
-----END CERTIFICATE-----
Subject CN=Google C2PA Mobile A 1P ICA G3 L1, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIIC2TCCAmCgAwIBAgIUdEQo46dHfO396b1NFkYHqblfVzAwCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjI3WhcNMzAwNTA4MjIz
NjI3WjBOMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEqMCgGA1UE
AwwhR29vZ2xlIEMyUEEgTW9iaWxlIEEgMVAgSUNBIEczIEwxMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEiso7FbPFqaTmLWL6vJq0Q0FTLVILWVg1nyG1kR2JkEc5E9WW
k62YSJreXbX6axJqDGecSk5kvqcb9EVJ+wymonRaqy7Gk9c6fi9Hr1mK2mfJZRlX
CIXeMXeneVG8NMJ8o4IBCDCCAQQwFwYDVR0gBBAwDjAMBgorBgEEAYPoXgEBMA4G
A1UdDwEB/wQEAwIBBjAfBgNVHSUEGDAWBggrBgEFBQcDBAYKKwYBBAGD6F4CATAS
BgNVHRMBAf8ECDAGAQH/AgEAMGQGCCsGAQUFBwEBBFgwVjAsBggrBgEFBQcwAoYg
aHR0cDovL3BraS5nb29nL2MycGEvcm9vdC1nMy5jcnQwJgYIKwYBBQUHMAGGGmh0
dHA6Ly9jMnBhLW9jc3AucGtpLmdvb2cvMB8GA1UdIwQYMBaAFJxc2IlTQ+da1YHb
A94ZfwQqKi2qMB0GA1UdDgQWBBSu91pCmLI7HftmLBUUmRZ631vvlDAKBggqhkjO
PQQDAwNnADBkAjA6bs5IFiYOZjmln6Bii/ShnzrqTn4GxKdCZhP79ul9iPz+mQyn
pORTyTY84SsxzicCMCI2tAB6n8FQ5BkN1apEUG5gcJlrrNd1rdqtBNXJZHXqYG1m
7T4gYBB4PHNYYFBGfA==
-----END CERTIFICATE-----
Subject CN=Google C2PA Mobile A 1P ICA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIIC1jCCAl2gAwIBAgIUXKoTHSLJjkXHIxvKY29YmxgOzoowCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjI4WhcNMzAwNTA4MjIz
NjI4WjBLMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEnMCUGA1UE
AwweR29vZ2xlIEMyUEEgTW9iaWxlIEEgMVAgSUNBIEczMHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAEx5s4bjuNRDlYKtdj4Dd14esDWyBAk+fNPHFGlDcWX2lTdxgeeKYF
caw7ZdhD5fWAqUZ++M5wdXXUoGHDFAwfPmTaRKzZupUu7uMFZtolBzAuT2I51meH
EfMHU95kRkTIo4IBCDCCAQQwDgYDVR0PAQH/BAQDAgEGMB8GA1UdJQQYMBYGCCsG
AQUFBwMEBgorBgEEAYPoXgIBMBIGA1UdEwEB/wQIMAYBAf8CAQAwFwYDVR0gBBAw
DjAMBgorBgEEAYPoXgEBMGQGCCsGAQUFBwEBBFgwVjAsBggrBgEFBQcwAoYgaHR0
cDovL3BraS5nb29nL2MycGEvcm9vdC1nMy5jcnQwJgYIKwYBBQUHMAGGGmh0dHA6
Ly9jMnBhLW9jc3AucGtpLmdvb2cvMB8GA1UdIwQYMBaAFJxc2IlTQ+da1YHbA94Z
fwQqKi2qMB0GA1UdDgQWBBQOccStRBCOVzAFalyyAs6XkvF+IDAKBggqhkjOPQQD
AwNnADBkAjBjAj22bX3vKs/3Q4K0qE7jPwY3BljcaGyhTg9Gk6ni0+3TxuluLCq+
zITXauG0qy0CMFfS4bUxKdwCjWOnamLCA9xiaO82my0UN0kvxNMFflLsmeO4PL+N
I/u3EJ347o+4jA==
-----END CERTIFICATE-----
Subject CN=Google C2PA Mobile B 1P ICA G3 L1, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIIC2zCCAmCgAwIBAgIUBpMF/2hh9GWQEhKn4uebRk7j2PcwCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjI3WhcNMzAwNTA4MjIz
NjI3WjBOMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEqMCgGA1UE
AwwhR29vZ2xlIEMyUEEgTW9iaWxlIEIgMVAgSUNBIEczIEwxMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEKGv3Fd3J1vOPfrR744lpYlGc05lZ57UyVDKGHTPj71PuwW19
oHO932WXXf+K4jBkotgXeBChrhrTiUGxBdrmYRB1m6MyAJ/wT3xw06YRcGxoiW0b
IhbMN4YNIAMW76Noo4IBCDCCAQQwFwYDVR0gBBAwDjAMBgorBgEEAYPoXgEBMA4G
A1UdDwEB/wQEAwIBBjAfBgNVHSUEGDAWBggrBgEFBQcDBAYKKwYBBAGD6F4CATAS
BgNVHRMBAf8ECDAGAQH/AgEAMGQGCCsGAQUFBwEBBFgwVjAsBggrBgEFBQcwAoYg
aHR0cDovL3BraS5nb29nL2MycGEvcm9vdC1nMy5jcnQwJgYIKwYBBQUHMAGGGmh0
dHA6Ly9jMnBhLW9jc3AucGtpLmdvb2cvMB8GA1UdIwQYMBaAFJxc2IlTQ+da1YHb
A94ZfwQqKi2qMB0GA1UdDgQWBBRLBf7gDA1hOViJqHtuE8btC3KB8TAKBggqhkjO
PQQDAwNpADBmAjEAhfcU5E3IKMRw/Wxd9YfUdxTpi5HM99JiHG0KmFxcPGd8tDBA
XkoxEV/OYIEeyVl4AjEA1QH3goidp0++w4rQ6P8wbdw3BtlkSJpGsQ4WQc0i5bWz
KyN/WtDOZj/RG3+bqaw+
-----END CERTIFICATE-----
Subject CN=Google C2PA Mobile B 1P ICA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIIC1zCCAl2gAwIBAgIUac3GAHq5vTwO71pE75SNo8wcJVcwCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjI3WhcNMzAwNTA4MjIz
NjI3WjBLMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEnMCUGA1UE
AwweR29vZ2xlIEMyUEEgTW9iaWxlIEIgMVAgSUNBIEczMHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAEbns1saor1bu/tYfe1ozY2yZwBgkcmIcmr552foAPn5v4zmS61OEs
C5VwKHBgi9uTuXZ8SSV1KSPyysqrgm542XLPWsMlkuWRfneQBANeLAk6aEBqDASo
DJ7Aoube5tuao4IBCDCCAQQwDgYDVR0PAQH/BAQDAgEGMB8GA1UdJQQYMBYGCCsG
AQUFBwMEBgorBgEEAYPoXgIBMBIGA1UdEwEB/wQIMAYBAf8CAQAwFwYDVR0gBBAw
DjAMBgorBgEEAYPoXgEBMGQGCCsGAQUFBwEBBFgwVjAsBggrBgEFBQcwAoYgaHR0
cDovL3BraS5nb29nL2MycGEvcm9vdC1nMy5jcnQwJgYIKwYBBQUHMAGGGmh0dHA6
Ly9jMnBhLW9jc3AucGtpLmdvb2cvMB8GA1UdIwQYMBaAFJxc2IlTQ+da1YHbA94Z
fwQqKi2qMB0GA1UdDgQWBBT4N2ZAjPoDJR1+UgU4KwafcJT5mDAKBggqhkjOPQQD
AwNoADBlAjEAg2SjBAAmAOvOLd1kKYZQzkiD6KSXe4+3zALTs5SQCK3xmmkxsFzJ
bBCIiuTbxsrIAjAhQm+LpSxSwkIZiwWI7rGiJMv7BCj38HhiQpmR5lr+anbBOQty
UsWhYsCGlDmiWzA=
-----END CERTIFICATE-----
Subject CN=Google C2PA Root CA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIICLjCCAbOgAwIBAgIUUZK4AROFKiXQZ1UG7FG6qPGc1g8wCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwIBcNMjUwNTA4MjIzMjIxWhgPMjA1MDA1MDgy
MjMyMjFaMEMxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApHb29nbGUgTExDMR8wHQYD
VQQDDBZHb29nbGUgQzJQQSBSb290IENBIEczMHYwEAYHKoZIzj0CAQYFK4EEACID
YgAEhv9f/juKcPpe3Fm7eAISMuSyS+tBxn0aYHC83J+qAsFWREGN9p6PN/OBoouP
zpOFRxvrlWoWmAI3p1lXyPg4E3eg7SNChgopUIpihGu6qlhP8rLXf3p8bhI5FTQ2
MaF2o2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAfBgNV
HSMEGDAWgBScXNiJU0PnWtWB2wPeGX8EKiotqjAdBgNVHQ4EFgQUnFzYiVND51rV
gdsD3hl/BCoqLaowCgYIKoZIzj0EAwMDaQAwZgIxAIyVEe5bdUMkk6BthEWy9QSE
Mb74BOyK8/8pgMX0NPwLlo1ikLNY78ov+k21vZrEZQIxANQ91muDXgPjAMAkzAlK
i32Z9VBB37ynTveKVC7ofTW0ZFfIIYYpWUR1+C4m2yRkOQ==
-----END CERTIFICATE-----
Subject CN=SSL.com C2PA RSA Root CA 2025, O=SSL Corporation, C=US
-----BEGIN CERTIFICATE-----
MIIFlDCCA3ygAwIBAgIUExeshkq/ESresWEq3YWcEUTmxvowDQYJKoZIhvcNAQEL
BQAwTzEmMCQGA1UEAwwdU1NMLmNvbSBDMlBBIFJTQSBSb290IENBIDIwMjUxGDAW
BgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjELMAkGA1UEBhMCVVMwIBcNMjUxMTA3MTYy
NzEzWhgPMjA1MDExMDExNjI3MTNaME8xJjAkBgNVBAMMHVNTTC5jb20gQzJQQSBS
U0EgUm9vdCBDQSAyMDI1MRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xCzAJBgNV
BAYTAlVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA07LXx05AgfZn
F6CPMzpPwA+vGxfqi2Pw3/gvhAe4UdzmasSutCRK9sK66DZYta4AbYPS0/2IToqo
1n33f9/XrmQ62uM3XAyKXfg8UWUaj2WsyA3dIxY5l80B0S0EX0V1HpP6ygw1IDfJ
/KKkknn/0o14zmNkRzLpqQDOMoL4s+kli8NtkEOG8M8NrH5QJCNRL7lN0DG8TvRa
10Zfwyxnd4yw9DT2HWJInjC+Hww9QZua/u6bTy0pFHV0vVCgKQuPtbX8b60998t4
b56trGesWapUefIEu6GH1CkQu7nhsk1Rf59+uPv1NQLOm7wbyghHtQQ00baR/687
r7/qXxRLV1CFNH3nAgFfHkD8N07GWS5C1OOmkUPrc2f9tdKWOJ6USDJCOX1DkTee
o9RgwSJfLueDtAOp19dlBo2UW79A2MGN6VI119DoW04VUjxmPHLIWtt08ZRTHzvN
2I2DA3ic5Wt4pgSLXDZ6ztlFX4Y6nXwVFdWae9zwLpm9ttsF4fepLyPAUTuT9m0j
/oeQt3KmyxIukYD8I1PiSQ6YDLIiWFZcYC8LMyiYmQEX6spsE+IF37lx2nZ1OF+D
AOSCYYamZx6HYShgN4zN1QYTMJdxbz5JgRTIIioVdJTqeF/5GsSGP6f9Eym8ND9j
LQzm8MBEMfvlAgw0q1ieN2Q9INSN3qMCAwEAAaNmMGQwEgYDVR0TAQH/BAgwBgEB
/wIBAjAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPwqSnU6gPqZY5Pwc1fsvpOw
fcN7MB8GA1UdIwQYMBaAFPwqSnU6gPqZY5Pwc1fsvpOwfcN7MA0GCSqGSIb3DQEB
CwUAA4ICAQBAqkR9j5wKs0T7e9gHTUWlLAnkWLyZI7AuEoXimyJ7Ocy1EwZyu3TD
Z2jCecLNXENnUwjuKaEm/8sCwzrVqpxzIgkS1jVLuVURlrj2+apT/7wBw8LCKMZ6
cDhQJV3A1+9hyCc5ozkE1LgcJfWELrBWeBuj8rWLAf0DDgtwaED7nS9c9P8UgrxH
YTMe2AHWEfczXBirx5ubpNaokR0W01aSdXa97ohoS+ZmK0bxvmWCugI4tfsypsVl
lV2iD9v/BPLCVHP+7ot520mE21WdQKD69M54V8KAqfmNoG6I3TB2EcKJ0NpKI0zW
niSX05NDrv70INu+vAI8Sso/mRMHpuo03/0iTm3z8J4ACjwP071U0R3wCjTxN8z2
95EwOf1/JQH5h0njkarxFsWsnHJtWwE3kGEibO3ivB2U3FyS1osN0bEA4X5CSeMF
GDJFCXVB0l4/KeHHNRk3DEVjr5dzcwt/C2DLj9VuaeWEWuVbA3/PDR/MnZzDtPj1
TVLswGkWKwG+AyJaHkLysZD0IKwL4SZGGCRvyKw+G5Go0vvHVyUMPHqVTBApxY9l
pt6absyMPdRnf+w0tdFlhCd2rauRA0HX7JZ5xqnYjkfk0m7MpoauCsQR9udJvv5H
pMv7wvJRchf62/pPnjLKhN/dx9LR/EVqBPbPMhqdzO6ObQdqUJzXPQ==
-----END CERTIFICATE-----
Subject CN=SSL.com C2PA ECC Root CA 2025, O=SSL Corporation, C=US
-----BEGIN CERTIFICATE-----
MIICRTCCAcugAwIBAgIUHTAeXakkTyAFDkZfyu8YyCO9ubgwCgYIKoZIzj0EAwIw
TzEmMCQGA1UEAwwdU1NMLmNvbSBDMlBBIEVDQyBSb290IENBIDIwMjUxGDAWBgNV
BAoMD1NTTCBDb3Jwb3JhdGlvbjELMAkGA1UEBhMCVVMwIBcNMjUwNzE4MTU1MTQ5
WhgPMjA1MDA3MTIxNTUxNDlaME8xJjAkBgNVBAMMHVNTTC5jb20gQzJQQSBFQ0Mg
Um9vdCBDQSAyMDI1MRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xCzAJBgNVBAYT
AlVTMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEo3WOe9pIesN2XWe6KQdiUO+cU1mr
+Bs8opia0I+IA5m8oYhBJmJWPxLea7PH6tlW6f5wqkIOaeJkJ7X1pz3IHPjO8qkX
imKjiUwt/B7IoEj6rhoqkAV4AMO2BMxbS1MUo2YwZDASBgNVHRMBAf8ECDAGAQH/
AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUU50WGoV8zfcRPowvdPbBkJ1O
g6swHwYDVR0jBBgwFoAUU50WGoV8zfcRPowvdPbBkJ1Og6swCgYIKoZIzj0EAwID
aAAwZQIxALxN0Q+F+9KXOnYUKcW70UuxgitT8iwjTBGCIXJbJw4XaMtVdVGf4HwY
NITBgr+rWwIwczO4trazVV27OTCl8xNR0IH4UuyFgBVuzMsIj8IPkQw5mVhtvhY7
nNgp3Dyy2LeE
-----END CERTIFICATE-----
Subject emailAddress=ca@trufo.ai, CN=Trufo C2PA Root CA (2025, ECC P384), OU=CA Division, O=Trufo Inc., L=New York, ST=New York, C=US
-----BEGIN CERTIFICATE-----
MIIDOTCCAr6gAwIBAgIUbXqcPd1r9yQm/fznG9RlSWyXiwswCgYIKoZIzj0EAwMw
gagxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3
IFlvcmsxEzARBgNVBAoMClRydWZvIEluYy4xFDASBgNVBAsMC0NBIERpdmlzaW9u
MRowGAYJKoZIhvcNAQkBFgtjYUB0cnVmby5haTEsMCoGA1UEAwwjVHJ1Zm8gQzJQ
QSBSb290IENBICgyMDI1LCBFQ0MgUDM4NCkwHhcNMjUxMjMwMTkwNTAzWhcNNDUx
MjI1MTkwNTAzWjCBqDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREw
DwYDVQQHDAhOZXcgWW9yazETMBEGA1UECgwKVHJ1Zm8gSW5jLjEUMBIGA1UECwwL
Q0EgRGl2aXNpb24xGjAYBgkqhkiG9w0BCQEWC2NhQHRydWZvLmFpMSwwKgYDVQQD
DCNUcnVmbyBDMlBBIFJvb3QgQ0EgKDIwMjUsIEVDQyBQMzg0KTB2MBAGByqGSM49
AgEGBSuBBAAiA2IABAp0qnhIwMtN6LeGdBVtHLPn85ecetr/lqcXFk8ypK9ukJzU
8LLv55Kh/MYTgEnuIKEOPhDxLDRdahc0mAjRnql4kLk395abw9WZjrBPek3qjv0q
ITR8VPYFABuZ5FRKx6OBpjCBozAdBgNVHQ4EFgQUA9Vfr36D5QQdWYAnSjT/Rf3r
SXgwHwYDVR0jBBgwFoAUA9Vfr36D5QQdWYAnSjT/Rf3rSXgwEgYDVR0TAQH/BAgw
BgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgorBgEEAYPoPAEB
MCQwIgYIKwYBBQUHAgEWFmh0dHBzOi8vdHJ1Zm8uYWkvY3BjcHMwCgYIKoZIzj0E
AwMDaQAwZgIxAMUeYWZyxS2maiVkNETL29RAuLn/gHYTkt97l6evXwHLN46v28mI
39BIf6slyWnrCwIxAPRs/FJ+DoA0d/PCkrF946S+pG7vRqLnjB9OhMdmrMPvzaqx
KQYOBVx7SE4Kz48W8A==
-----END CERTIFICATE-----
Subject CN=vivo Content Provenance and Authenticity Root CA, O=vivo Mobile Communication Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIIC0zCCAjSgAwIBAgIJAMIN3t2/xTDAMAoGCCqGSM49BAMEMHYxOTA3BgNVBAMM
MHZpdm8gQ29udGVudCBQcm92ZW5hbmNlIGFuZCBBdXRoZW50aWNpdHkgUm9vdCBD
QTEsMCoGA1UECgwjdml2byBNb2JpbGUgQ29tbXVuaWNhdGlvbiBDby4sIEx0ZC4x
CzAJBgNVBAYTAkNOMCAXDTI1MDkxODA5MDkxM1oYDzIwNTUwOTExMDkwOTEzWjB2
MTkwNwYDVQQDDDB2aXZvIENvbnRlbnQgUHJvdmVuYW5jZSBhbmQgQXV0aGVudGlj
aXR5IFJvb3QgQ0ExLDAqBgNVBAoMI3Zpdm8gTW9iaWxlIENvbW11bmljYXRpb24g
Q28uLCBMdGQuMQswCQYDVQQGEwJDTjCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE
ANQigNuQ6RnYFSK2BWkaksR3fRFFytX4nzX3E4hLV/N/7m/XttNHxNDase0cAXwR
9pO23p6fb/9NbifILv2wzPn2ASMr306Y2frv/UjJ5J6WgHet2OFywRaPjFjJWRuk
4JCy0qoSxyyx230g8GYdacZzIqSzVTW7/Xz4mjnWDTOB/Db+o2YwZDASBgNVHRMB
Af8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUgB0dwwUcpK4u
7az42vKC2y+nnY8wHwYDVR0jBBgwFoAUgB0dwwUcpK4u7az42vKC2y+nnY8wCgYI
KoZIzj0EAwQDgYwAMIGIAkIB9cjlRbw8I0+0VAIOENCEzKOfp2risHuo3KVqyaSV
K/T6KzMNvpq3ihA6ZjnuH9i5y5XWV//awWbFJDSuT0Bq+pwCQgHuthBjGmDl/nyc
r3MxJErDVYZBh4b140LQBU60MtiPERVYwme03ukGImPW/OKbEWYmnhHyMAf1ym/3
UK/lOv3G2w==
-----END CERTIFICATE-----
Subject CN=Xiaomi Root CA(EC-P384), O=Xiaomi Inc., L=Beijing, ST=Beijing, C=CN
-----BEGIN CERTIFICATE-----
MIICeDCCAf6gAwIBAgITAJQBmyrWaBV3JOroB8uDgC0MxDAKBggqhkjOPQQDAzBp
MQswCQYDVQQGEwJDTjEQMA4GA1UECBMHQmVpamluZzEQMA4GA1UEBxMHQmVpamlu
ZzEUMBIGA1UEChMLWGlhb21pIEluYy4xIDAeBgNVBAMTF1hpYW9taSBSb290IENB
KEVDLVAzODQpMCAXDTI1MDkyNTAxMjI0MFoYDzIwNTAwOTE5MDEyMjQwWjBpMQsw
CQYDVQQGEwJDTjEQMA4GA1UECBMHQmVpamluZzEQMA4GA1UEBxMHQmVpamluZzEU
MBIGA1UEChMLWGlhb21pIEluYy4xIDAeBgNVBAMTF1hpYW9taSBSb290IENBKEVD
LVAzODQpMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDOfbCvQLZmlcAW7uOnEiCJ3K
pVxu+qYxrwBDbSIRsDc2xg9o94+8pX+mveme5SsSsKM/0ouu5+ECwhkxRof/1BDQ
21cK+n4HBY0G7P3RltI6moeDv4bdVvC4ni+yJpFvo2YwZDAdBgNVHQ4EFgQUm8DQ
DCIUeaz1o+Cc286YCgebcc4wHwYDVR0jBBgwFoAUm8DQDCIUeaz1o+Cc286YCgeb
cc4wEgYDVR0TAQH/BAgwBgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0E
AwMDaAAwZQIwUfj3heoA8iZfAMqHrZwarUM71v3Y1gqlltdgOzv9mix5O/EDxHcQ
m26a9NrrMTXmAjEAgwjR42dkym44QlV+QXR05vuMf1o/bdAXmJKvhjDgPTK9VYRa
gFLaTqFtjuZfDg3L
-----END CERTIFICATE-----
Subject CN=DigiCert RSA4096 Root for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgIQL2uv995UIpTpN6WRLwYnHDANBgkqhkiG9w0BAQwFADBS
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKjAoBgNVBAMT
IURpZ2lDZXJ0IFJTQTQwOTYgUm9vdCBmb3IgQzJQQSBHMTAgFw0yNTA4MjcwMDAw
MDBaGA8yMDUwMDgyNjIzNTk1OVowUjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
Z2lDZXJ0LCBJbmMuMSowKAYDVQQDEyFEaWdpQ2VydCBSU0E0MDk2IFJvb3QgZm9y
IEMyUEEgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDTX44Mw12/
in3jeV80FcoutZhxtvhZQHAdiFtiYd9Y+quavnHRqtcexRZQ0n1XtGXeiusE72M6
G4dwdTsHXoXVNXjwrO9a/fA/46u+/T5ZU6FyRQdahwQNIXL5PVfqrjwX2Ta2Gby2
z4tbhfzxtgJSosJGJsCGq5mGiBXIqBkbua0YOvuX1WifAuDQun84bw+qwCCWxSty
QuUcZMm6xl24ye5RZnz8xkctz/p68kgNa9IhJlHrZ3CAe1zEu3PrNS7Oq/uzN06t
Ji6rkpXCV6GlhN0RlEPmUb3pg8kYqQ9uca7u4UTyl/F9QqG2xMDgAtNDrfNbWpyO
fnoYGNQvQyD5M7slsz9j9sUcKwspaz8zmuu31UXPqmvME+UalDvFeknQZP+ft9Uv
8eICovCQxvy1Iwg3sjc2hO0lbYYn/JqGARg+5jP/SlkY4dSypdYBtBaAfVxTd+iQ
m4k1oAEIP7ujyQ+1U0F9gygQPwQrMOcyYINgkcsp36rb1znuxlIF6LIcfO1bkVSx
vHqd0CVP8vwrYXKJOr4dES1s4k12kxv+csvZWiuSqY0uhFUSMSE5id5zWW7H6ZCX
3J3Ati2MjyRjyyejDbTQdTS3wlF4f4bLaWidMWSZ3JCyZABXpj46RtdbhZgV7I7B
ZTXCI6FIBuUaiZzW9RHhVzR+qrfBOx40mwIDAQABo2YwZDASBgNVHRMBAf8ECDAG
AQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVusnOJo+SNZygIWbesMH
UVoE8+UwHwYDVR0jBBgwFoAUVusnOJo+SNZygIWbesMHUVoE8+UwDQYJKoZIhvcN
AQEMBQADggIBAIjcpKt4FeVibq6zP1m2RUagL6PEW+8o5CC5kv0mDMcCgqNTsyNJ
lr/1yYzzpEC82QaCZ34g4eMXnOwq+5jF5xQ5b7RV7X9nFuPXh7r2CittMkBLPKp/
jSApyfEI2qgNtPWc5voKRgPrkSuHu+wB5gTg7AkN9CCZeaauMesIIn7/8lWOSho9
a5DVCJekYh+WcfnSP6l0ilRw1WhnAtPwadhcQ9zJxKe6LSdsH3yNJhViIQNrBhlA
O3BNZF1q2JfOM6TCiY/I+M8AfzuvxF/xyxeDlcRNo6QbfAvUybDOsleg9NxO+EK3
VXTInzaWidoX9TRdEx0iiERj9OB7ehZBwTUllBH6UtxFy3X7YRSnO8ZdjtEERJmk
BNZztgd4KWBnaFMERI0ObiqN1az0uqe8sRULEJ1Ay79sTsJtiemNChMxmYb83XU1
1wSX/W54tD6vPQPCwn84AeLgYSfA10QwRyAo99pwmjZzrCx5uBH7jCVYBkP7iQ6h
o3yHror7znE8LJrrWytOuumEFzxQ0NSaXjrDRKCLKTKnr8niIj+lB/SJsBua52a6
HHRckrpDokgPSPOYI4A+ZGC//Ron9i3S8ce2dQHAWNCqB2ADtnu4Yf36OfmHdrCL
hzIxlPKKDPXwgxv7NTRFs1d7KcIpjYuZp71eZA74ZqxDgVa7CMkhdgML
-----END CERTIFICATE-----
Subject CN=DigiCert ECC P384 Root for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIICSTCCAc+gAwIBAgIQCZ2yarc/T5eBQYVHCJTdyjAKBggqhkjOPQQDAzBTMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKzApBgNVBAMTIkRp
Z2lDZXJ0IEVDQyBQMzg0IFJvb3QgZm9yIEMyUEEgRzEwIBcNMjUwODI3MDAwMDAw
WhgPMjA1MDA4MjYyMzU5NTlaMFMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp
Q2VydCwgSW5jLjErMCkGA1UEAxMiRGlnaUNlcnQgRUNDIFAzODQgUm9vdCBmb3Ig
QzJQQSBHMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNHiy4U9/SpPSnQ3ogU+q3mn
M/f9zmLuqF8krS0LNUC0T81Nu57WCGSb6149CVpkRRC6BYlfGcBWVYXar4aX10oX
ggSiFPi93ovkAfUN9wikKqVu6amI7tRQPKXJ95UsFaNmMGQwEgYDVR0TAQH/BAgw
BgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPQYEa4JgolO/Ju0M/b0
370CTpQ2MB8GA1UdIwQYMBaAFPQYEa4JgolO/Ju0M/b0370CTpQ2MAoGCCqGSM49
BAMDA2gAMGUCMQCg31cyC7If2VPMUvqEr08WB6QhRVpAa+GNRlKUOJlSMq/y2tm+
80RTxpV2RsYC9UsCMGkKQ0Yx4FIs1RhYtXVIgTyHrSxGne4/457DlIS+IMIJN5Yd
gMNj6G6tzM47QDGl2w==
-----END CERTIFICATE-----
Subject CN=DigiCert RSA4096 L1 Claim Signing ICA for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIIGxDCCBKygAwIBAgIQJ4BVBbBmDXnyAtI0JLlo5TANBgkqhkiG9w0BAQwFADBS
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKjAoBgNVBAMT
IURpZ2lDZXJ0IFJTQTQwOTYgUm9vdCBmb3IgQzJQQSBHMTAeFw0yNTA4MjcwMDAw
MDBaFw0zMDA4MjYyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp
Q2VydCwgSW5jLjE6MDgGA1UEAxMxRGlnaUNlcnQgUlNBNDA5NiBMMSBDbGFpbSBT
aWduaW5nIElDQSBmb3IgQzJQQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBAKiOlc644cuaOoCElG+yGHBmAtfXRw3fSBfRTCf/9yaMQiyEGCxP8HKT
fxtdqD8zmXqePLCLzGM2ccGJx3w5RzpFdOhopwGIKsTm+C1wHmZc5Lws5wst7rFQ
stF8nIN9TBP5H6elZcoNaUhqCJ0bxmeXp6J0iiRKiuLF2j0Grfv4cBL8+XlLRE3g
Of1yqEsEZktfK9qeCUuugltxXwL1WP8IKgtkMeLo0QZQSAg4R+gyX6C1j64nlAtV
RewqSrug+Rkquz94doEscaq3w+ypp8pJ9ovpn9Zdx6PArh4uK90YB/ThB92Voi2x
K8T9D8xnzpONwBfKPiUS502160mZuF09Xgm6zBNRPFKmAdhN73QV4i5Hn2rv5104
AR4QGvz5Egh/bZ5RElIutD/CvVrOS1fykm6ZIVuxqHUx0pxWkAsjRyCE+fuhczhW
QfVy2EUst98kNjUZC/OFF4TxfcWt7xu7CKNZb3rE7nrbMAbPLKPZjAlT7GlmEhaa
dw/Y3ap0sPnO3BtsBP3kOSed3lP3zEfokBLSYsQtwDWzdWanGE3nB2HvBmcviYdi
P48gXP/5uybnVlKUHXb0gBtdYzNTBx3ynoUrWtbNwvMuTKv6mmnuLxhGiEWEK+BY
StlfOiPvUdpyr+TMsaujPpWq9IUX6qmsOcl1gfujK/lJ3kuvSUm3AgMBAAGjggGE
MIIBgDASBgNVHRMBAf8ECDAGAQH/AgEAMBcGA1UdIAQQMA4wDAYKKwYBBAGD6F4B
ATAOBgNVHQ8BAf8EBAMCAQYwgYYGCCsGAQUFBwEBBHoweDAoBggrBgEFBQcwAYYc
aHR0cDovL29jc3Aub25lLmRpZ2ljZXJ0LmNvbTBMBggrBgEFBQcwAoZAaHR0cDov
L2NhY2VydHMub25lLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFJTQTQwOTZSb290Zm9y
QzJQQUcxLmNydDBNBgNVHR8ERjBEMEKgQKA+hjxodHRwOi8vY3JsLm9uZS5kaWdp
Y2VydC5jb20vRGlnaUNlcnRSU0E0MDk2Um9vdGZvckMyUEFHMS5jcmwwKQYDVR0l
BCIwIAYKKwYBBAGD6F4CAQYIKwYBBQUHAwQGCCsGAQUFBwMkMB0GA1UdDgQWBBTC
mxzaIdNwH6lU5HUf8IFpzcgbRTAfBgNVHSMEGDAWgBRW6yc4mj5I1nKAhZt6wwdR
WgTz5TANBgkqhkiG9w0BAQwFAAOCAgEAt8Kn2LQMxbL10BAx5ebm1G/TMTrgQN9y
f18ZE5qNk3ipoFFtijZzRT5zidKi+/956V11nPMgm90W/CMtapHImj3Da4eoShzl
7N6hPjGYuHJUFfCYaJFxgz0r2sdFYc0drpOlfLa+WVUmgXM8QhbWiHcyk37z8Ega
6p1fjFVH6QqKWwXhSuR5aH0QflBHJKdWRkRZSTNBU2Hj1FOqhZBPPM+jvLGWgkQg
hpQTrO9pTILZ0nb3rTkvClimBewUwTkjoB8M4UHifHcXJ7/RnFzj+KHR5kWdzVTz
uCLcIlTNeACYQZaAj/eT9vzI1n6IUoU04R/9STijYHr05VbH1jM94rDJzjE8Hjf+
f8YV9X/38Rc3gcH1hLzAKqwIyF/XieaquJLXY90O1Ads/WTrNRlf5A4109FKnELd
w8R9vOneDvslbrHxu/MmoanzJlqxW1Sw6tlU0G2kMfyIH9y0RLmTiJndW9UXyzMU
tGID1N2lhdO0BmwBuzCbZWRHCD0NcP9l5Kc8fCL95OgdpRbivEFeGynZZiCjvrJ9
tT+uGWQiBbTlEgR57e1KsEPFKxb0M81ewNEo+vhxaSoWvXv84ZM/Zc9a0ZqPMEPr
3073hQvLguizQTWJQstDjzlmOWF7K6hnI70rTNLhZBEFC3XJlBYxbUMlGdH4ebLc
9IUUXRE00ws=
-----END CERTIFICATE-----
Subject CN=DigiCert ECC P384 L1 Claim Signing ICA for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIIDdzCCAv2gAwIBAgIQUhYZxg4djFjAWbZwab2qqTAKBggqhkjOPQQDAzBTMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKzApBgNVBAMTIkRp
Z2lDZXJ0IEVDQyBQMzg0IFJvb3QgZm9yIEMyUEEgRzEwHhcNMjUwODI3MDAwMDAw
WhcNMzAwODI2MjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNl
cnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IEVDQyBQMzg0IEwxIENsYWltIFNp
Z25pbmcgSUNBIGZvciBDMlBBIEcxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ2hi
wBWq/Dldnu/FxGwfHDAmRxezmmKQtIWSd/HYfiJW2T9wNJmSPywB5Kl7WBJXyaxy
Uwhn8tpDqBypcT0hlN8Mi8Cql9yj9t2MJXN3KshCyBiQWjcKnZoOFhW83E1oo4IB
hDCCAYAwEgYDVR0TAQH/BAgwBgEB/wIBADAXBgNVHSAEEDAOMAwGCisGAQQBg+he
AQEwTQYDVR0fBEYwRDBCoECgPoY8aHR0cDovL2NybC5vbmUuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0RUNDUDM4NFJvb3Rmb3JDMlBBRzEuY3JsMA4GA1UdDwEB/wQEAwIB
BjCBhgYIKwYBBQUHAQEEejB4MCgGCCsGAQUFBzABhhxodHRwOi8vb2NzcC5vbmUu
ZGlnaWNlcnQuY29tMEwGCCsGAQUFBzAChkBodHRwOi8vY2FjZXJ0cy5vbmUuZGln
aWNlcnQuY29tL0RpZ2lDZXJ0RUNDUDM4NFJvb3Rmb3JDMlBBRzEuY3J0MCkGA1Ud
JQQiMCAGCisGAQQBg+heAgEGCCsGAQUFBwMEBggrBgEFBQcDJDAdBgNVHQ4EFgQU
JqGs/hW9GFhOR/R0fNP8EaPxlocwHwYDVR0jBBgwFoAU9BgRrgmCiU78m7Qz9vTf
vQJOlDYwCgYIKoZIzj0EAwMDaAAwZQIwa6ohDos9g13+HZlgKR45C5K3qug4QUi3
tHu79IL601lgQSDgpnclOmJOuWxZFxfGAjEA3kAU5JV6no1hQpZdtb11g+B5wCHW
bUVPYZLXZ1Ie2RaND1QmLeYYKbuqsC58ZtSw
-----END CERTIFICATE-----
Subject CN=Adobe Product Issuing CA vault-a-or2.adobe.net cai, O=Adobe Inc, L=San Jose, ST=California, C=US
-----BEGIN CERTIFICATE-----
MIIDhDCCAwugAwIBAgIUTNz+Jl3kNFEJK5md8Tp/Cwsm/B4wCgYIKoZIzj0EAwMw
bzELMAkGA1UEBhMCVVMxETAPBgNVBAcTCFNhbiBKb3NlMRMwEQYDVQQKEwpBZG9i
ZSBJbmMuMRAwDgYDVQQLEwdQcm9kdWN0MSYwJAYDVQQDEx1BZG9iZSBQcm9kdWN0
IEludGVybWVkaWF0ZSBDQTAeFw0yNTExMTkxODI5NDRaFw0zMDExMjAwMDMwMTRa
MIGGMTswOQYDVQQDEzJBZG9iZSBQcm9kdWN0IElzc3VpbmcgQ0EgdmF1bHQtYS1v
cjIuYWRvYmUubmV0IGNhaTESMBAGA1UEChMJQWRvYmUgSW5jMQswCQYDVQQGEwJV
UzERMA8GA1UEBxMIU2FuIEpvc2UxEzARBgNVBAgTCkNhbGlmb3JuaWEwdjAQBgcq
hkjOPQIBBgUrgQQAIgNiAASArJB/ZRkabuyoSoGy/6JS124LqMOeKRTruZ4GIxZ3
Rg2UKXeWDINVsgXVewJalg98T0HgeXyXM8Ia0y0dLQUhxT++kVaascgJBuevPs8z
5Z+n+grN03x6ttUORdJ4O9qjggFOMIIBSjASBgNVHRMBAf8ECDAGAQH/AgEAMB8G
A1UdIwQYMBaAFBldp+77+M3A85KlrD7gVNgW7D8UMFsGCCsGAQUFBwEBBE8wTTBL
BggrBgEFBQcwAoY/aHR0cDovL3BraS1jZG4uYWRvYmUubmV0L2NhL2Fkb2JlX2lu
dGVybmFsX2ludGVybWVkaWF0ZV9wcm9kdWN0ME0GA1UdHwRGMEQwQqBAoD6GPGh0
dHA6Ly9wa2ktY2RuLmFkb2JlLm5ldC9hZG9iZV9pbnRlcm5hbF9pbnRlcm1lZGlh
dGVfcHJvZHVjdDAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFC76762PJu/wKMyE
92MRuG8aw1sIMBcGA1UdIAQQMA4wDAYKKwYBBAGD6F4BATAfBgNVHSUEGDAWBgor
BgEEAYPoXgIBBggrBgEFBQcDBDAKBggqhkjOPQQDAwNnADBkAjAfjQ+ZR+qrZcyW
iOVr07JrjClkluOhKPreYGCIQjRXvZPiFPDorO1df9tEIPmJ2Q4CMAIfpEWAmTsA
ggP/MIJIBoTabnBjhwmUcaqRdjK/LuT3KvwkZzZ6R7iPDkQs0d+dQA==
-----END CERTIFICATE-----
Subject CN=Irdeto C2PA Root CA G1, O=Irdeto BV, C=NL
-----BEGIN CERTIFICATE-----
MIICLDCCAbGgAwIBAgIUL8JDbqsQllkL9gozEdKhKc9GFAcwCgYIKoZIzj0EAwMw
QjELMAkGA1UEBhMCTkwxEjAQBgNVBAoMCUlyZGV0byBCVjEfMB0GA1UEAwwWSXJk
ZXRvIEMyUEEgUm9vdCBDQSBHMTAgFw0yNTEyMDUxMjQ0NDBaGA8yMDUwMTIwNTEy
NDQ0MFowQjELMAkGA1UEBhMCTkwxEjAQBgNVBAoMCUlyZGV0byBCVjEfMB0GA1UE
AwwWSXJkZXRvIEMyUEEgUm9vdCBDQSBHMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA
BEOtgkWt4t4r3Fvn2vDshY1KcZ63/VTaZN7K2uiAcCeztiH+ZSFf4hA5cSCABRJ+
VkWZYLvnhNLsCVbtEij6mDN5hf4VIUdUNgUonGZ6tkL1Djs3CUIkjD+nH/HXbCSj
pqNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O
BBYEFGs1MrTyCBABc00WKVK0njUakyplMB8GA1UdIwQYMBaAFGs1MrTyCBABc00W
KVK0njUakyplMAoGCCqGSM49BAMDA2kAMGYCMQCB1h+esIhAsddoGkZI1aXwwCFM
wL/cHskaM8JZsvUfcecGQmG08ZVesfUeCzei0SECMQCLjWbR4LY1mgfF+PzlieDZ
9DTcd5qOpJaSf90/8AnQA/UWGtRGqJsrmERInDoXEOA=
-----END CERTIFICATE-----
Subject emailAddress=ca@tauth.io, CN=Tauth Root CA, OU=CA Division, O=Tauth Labs Inc., L=New York, ST=New York, C=US
-----BEGIN CERTIFICATE-----
MIIDjzCCAxWgAwIBAgIUW6zwDse+LBk8Yvy/u3p7lSjnaA4wCgYIKoZIzj0EAwMw
gZcxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3
IFlvcmsxGDAWBgNVBAoMD1RhdXRoIExhYnMgSW5jLjEUMBIGA1UECwwLQ0EgRGl2
aXNpb24xFjAUBgNVBAMMDVRhdXRoIFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC2Nh
QHRhdXRoLmlvMB4XDTI2MDEwNTE2MDgzMVoXDTQ1MTIzMTE2MDgzMVowgZcxCzAJ
BgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsx
GDAWBgNVBAoMD1RhdXRoIExhYnMgSW5jLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24x
FjAUBgNVBAMMDVRhdXRoIFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC2NhQHRhdXRo
LmlvMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETgpV7Q9tsMzXFPduTyVt71IRRfU0
yeZGVuXmVO6zyUXxqGCg07jafnMyv24pRwJ9Bk+ROhslNWFS0NhXjIINDuk4h5Ji
KcEEGBJIlGl7jOB/MPLF/HU7CfURT2ayXKXmo4IBHjCCARowHQYDVR0OBBYEFB3R
AQfd1ulUQKBf+J34TnS1CxI+MIHXBgNVHSMEgc8wgcyAFB3RAQfd1ulUQKBf+J34
TnS1CxI+oYGdpIGaMIGXMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsx
ETAPBgNVBAcMCE5ldyBZb3JrMRgwFgYDVQQKDA9UYXV0aCBMYWJzIEluYy4xFDAS
BgNVBAsMC0NBIERpdmlzaW9uMRYwFAYDVQQDDA1UYXV0aCBSb290IENBMRowGAYJ
KoZIhvcNAQkBFgtjYUB0YXV0aC5pb4IUW6zwDse+LBk8Yvy/u3p7lSjnaA4wDwYD
VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIx
AJzhLaivtrjOMyEAuoHQRIhN2u7kw2iWZZyAT6FJH4+GBYudax0dfewXeIvC0MH5
SwIwAKojdigAO86Xvk6++B875ZpTjQIysPX0bGumbmAwcAmsqum9JLLIRCDz+7Kt
WrRP
-----END CERTIFICATE-----
Subject CN=HUAWEI C2PA ECC384 Root CA E346, O=Huawei Technologies Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIICoDCCAiagAwIBAgIIKsblXSNLwF8wCgYIKoZIzj0EAwMwXzELMAkGA1UEBhMC
Q04xJjAkBgNVBAoMHUh1YXdlaSBUZWNobm9sb2dpZXMgQ28uLCBMdGQuMSgwJgYD
VQQDDB9IVUFXRUkgQzJQQSBFQ0MzODQgUm9vdCBDQSBFMzQ2MB4XDTI2MDQxMzAx
NTQxMFoXDTQ2MDQwODAxNTQxMFowXzELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUh1
YXdlaSBUZWNobm9sb2dpZXMgQ28uLCBMdGQuMSgwJgYDVQQDDB9IVUFXRUkgQzJQ
QSBFQ0MzODQgUm9vdCBDQSBFMzQ2MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOrBo
SivfM2LRlG3B4r52hE0vDjQR2iWv9sKTZXzkscQIasoH+uYBDDie9VcosH4Mfq/U
xN8ojhW7BZW38LT38SmYpJPZirtzKTyCD4ha4dU8tLcHoJsOn3p+kBw09Bvoo4Gu
MIGrMB8GA1UdIwQYMBaAFM/OyEcEDscO78w/okatAmz35ypMMB0GA1UdDgQWBBTP
zshHBA7HDu/MP6JGrQJs9+cqTDASBgNVHRMBAf8ECDAGAQH/AgECMEUGCCsGAQUF
BwELBDkwNzA1BggrBgEFBQcwBYYpaHR0cDovL2NhLmh1YXdlaWNsb3VkLmNvbS9y
ZXBvc2l0b3J5Lmh0bWwwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUC
MDCsqpRvJIPw6OAQi2Rj0CWnwJgK75YK5gj+K9xomIDu8ai/2ExrifL8JgROQOeZ
egIxAL3tT3Dp84wP18ohYU83gkRcA8j9B3T4BRpxY1Ew9IJMYJ2zqz0weFjAEGNC
9Z36XQ==
-----END CERTIFICATE-----
Subject CN=Huanyu Trust C2PA EC-384 Root CA, O=Huanyu Trust Ltd, C=CN
-----BEGIN CERTIFICATE-----
MIICTTCCAdOgAwIBAgIUCqsLOXYNHdz0EBDsWKcrsiBcF10wCgYIKoZIzj0EAwMw
UzELMAkGA1UEBhMCQ04xGTAXBgNVBAoTEEh1YW55dSBUcnVzdCBMdGQxKTAnBgNV
BAMTIEh1YW55dSBUcnVzdCBDMlBBIEVDLTM4NCBSb290IENBMCAXDTI2MDQxMzA2
MDUwOVoYDzIwNTEwNDEzMDYwNTA5WjBTMQswCQYDVQQGEwJDTjEZMBcGA1UEChMQ
SHVhbnl1IFRydXN0IEx0ZDEpMCcGA1UEAxMgSHVhbnl1IFRydXN0IEMyUEEgRUMt
Mzg0IFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARDnAlELrIETK81oB6E
/ZfbkGSH9YPhGdFpXTzDCHD3IsKPQdPPv/mF319XjdrZMECxpH9rbmWGuWrhq6K8
zIGO27H2MUE658XNu6aO5xcZV/YdAMkG/YWPYo9CmCT4/SujZjBkMBIGA1UdEwEB
/wQIMAYBAf8CAQIwHwYDVR0jBBgwFoAUYc0ZfGc1yTPouBQYj81mEEdaefswHQYD
VR0OBBYEFGHNGXxnNckz6LgUGI/NZhBHWnn7MA4GA1UdDwEB/wQEAwIBBjAKBggq
hkjOPQQDAwNoADBlAjEAqfQxNSnmGxtt1QBKJbaY8AyUiaJxzX5MnH0l/ielnc6D
qBhIcALvKKc3a8MizR01AjB85bwUFBgs6WlIWZ6sFDDbCQzrWpPU11YQnj/mGom7
yXjL/2axHPWPY2Do/ldP5mc=
-----END CERTIFICATE-----
Subject CN=Verimago Root CA, O=Verimago LLC, ST=Washington, C=US
-----BEGIN CERTIFICATE-----
MIICTjCCAdSgAwIBAgIVAOFSVdVKiaIUGZlbNzPTi5bS/Gy+MAoGCCqGSM49BAMD
MFQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRUwEwYDVQQKEwxW
ZXJpbWFnbyBMTEMxGTAXBgNVBAMTEFZlcmltYWdvIFJvb3QgQ0EwHhcNMjYwNTE1
MDE0NjUwWhcNNDYwNTE1MDE0NjUwWjBUMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
V2FzaGluZ3RvbjEVMBMGA1UEChMMVmVyaW1hZ28gTExDMRkwFwYDVQQDExBWZXJp
bWFnbyBSb290IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAExn74thYXg89OFcXC
obav4kqQAt0YDK4h28ovPP5RJWs77BBRwUhbdb0C6SMVFJ+MuyUK/cBoua+cSIOh
N6prHNqeHbZMi3nzlbVfEbDDWm9hy+OxXtOLlJQYL5zjUgkco2YwZDASBgNVHRMB
Af8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUAkmvpVkpKvS1
2ZHgaoysoshW7XowHwYDVR0jBBgwFoAUAkmvpVkpKvS12ZHgaoysoshW7XowCgYI
KoZIzj0EAwMDaAAwZQIwDqzc2oVzZezm4GeER3jPUDWNj4zsqdXciOoGO0JkAHmC
vyUBt8FH9vZK9d6bZ9DEAjEA5eFk9oFV8cm8pyJ2+hKo5ji/iip8A0T5U2GwHQyN
W4GXdTaTwkzPD7X6whoLKKB/
-----END CERTIFICATE-----
Subject CN=Verimago Claim Signing Issuing CA, O=Verimago LLC, ST=Washington, C=US
-----BEGIN CERTIFICATE-----
MIIDTDCCAtOgAwIBAgIUNcPH/7deevUcZvKM0EQGUJN99PMwCgYIKoZIzj0EAwMw
VDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xFTATBgNVBAoTDFZl
cmltYWdvIExMQzEZMBcGA1UEAxMQVmVyaW1hZ28gUm9vdCBDQTAeFw0yNjA1MTUw
MTQ2NTBaFw0zMTA1MTUwNzQ2NTBaMGUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
YXNoaW5ndG9uMRUwEwYDVQQKEwxWZXJpbWFnbyBMTEMxKjAoBgNVBAMTIVZlcmlt
YWdvIENsYWltIFNpZ25pbmcgSXNzdWluZyBDQTB2MBAGByqGSM49AgEGBSuBBAAi
A2IABPB0fzGODSoLVNkTQWekSBkL6rD2h2//WP+4f1pdl9K8K0Z8Bt0SrlOXNtrp
I/ytOybRwV7nH3TDAWkZV833jc8Gjgqs7P0Q4ZZc4+4WIey3eD9084B8rLXs3/K/
q7NyHaOCAVMwggFPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG
MCkGA1UdJQQiMCAGCisGAQQBg+heAgEGCCsGAQUFBwMEBggrBgEFBQcDJDAXBgNV
HSAEEDAOMAwGCisGAQQBg+heAQEwHQYDVR0OBBYEFCM0wx5MWMCrBF5eYt8J9b41
TMwcMB8GA1UdIwQYMBaAFAJJr6VZKSr0tdmR4GqMrKLIVu16MDsGA1UdHwQ0MDIw
MKAuoCyGKmh0dHA6Ly9jYS52ZXJpbWFnby5pby9jcmwvaW50ZXJtZWRpYXRlLmNy
bDBoBggrBgEFBQcBAQRcMFowJgYIKwYBBQUHMAGGGmh0dHA6Ly9jYS52ZXJpbWFn
by5pby9vY3NwMDAGCCsGAQUFBzAChiRodHRwOi8vY2EudmVyaW1hZ28uaW8vY2Vy
dHMvcm9vdC5wZW0wCgYIKoZIzj0EAwMDZwAwZAIwDq2qaR9as+lyVFcwsUTlMIm/
OGxgW4dqVg7jlsBST+3xvS0nIpZUS7lcF44FVcFdAjA5abgOYS/ie7NXxp3smYtu
FoKzc/V7TAaL0VS/HGB3x7sTa+D6HIY3/ls33Dy/4Gk=
-----END CERTIFICATE-----
Subject CN=Snowball ECC P384 Root CA for C2PA G1, O=Snowball Technology Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIICiTCCAg6gAwIBAgIUVlsRGj1G9vJ4ws2jMYyFlFcKUokwCgYIKoZIzj0EAwMw
ZTELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28u
LCBMdGQuMS4wLAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBSb290IENBIGZvciBD
MlBBIEcxMB4XDTI2MDYxODA5MzkxOVoXDTQ2MDYxMzA5MzkxOVowZTELMAkGA1UE
BhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28uLCBMdGQuMS4w
LAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBSb290IENBIGZvciBDMlBBIEcxMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAELJUrcxeVmmYrb8Kl/YVAYYFiyuiqiIa6z/1n
FoCpB2JV/Qkwjdns7rOvLheWBi1s880NcwqD/KD9t9/SOgWTfD+WOAhtPioAql2Y
YUl0toOUzoXaAOB3+HIQRiKsL2RIo38wfTASBgNVHRMBAf8ECDAGAQH/AgECMA4G
A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU6R4F1vboIaKQly8jCAQX2lQhWPQwHwYD
VR0jBBgwFoAU6R4F1vboIaKQly8jCAQX2lQhWPQwFwYDVR0gBBAwDjAMBgorBgEE
AYPoXgEBMAoGCCqGSM49BAMDA2kAMGYCMQD6/xszsnZoYt+gYIsgYKTMFARxRpB5
N16s2u2hgkuFs4zyLco1z7gOk0mv6EbBkm0CMQCcsqIQnr45MVjDUdn9L//VdGDO
6H+P67vBdmvb3LiLTB+Ey/JYYfnHHIKzJrRG4ls=
-----END CERTIFICATE-----
Subject CN=Snowball ECC P384 Claim Signing ICA for C2PA G1, O=Snowball Technology Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIIDrjCCAzSgAwIBAgIUMPJ1KkDp+fvHaZOBsdKSnPm/hdowCgYIKoZIzj0EAwMw
ZTELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28u
LCBMdGQuMS4wLAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBSb290IENBIGZvciBD
MlBBIEcxMB4XDTI2MDYxODA5NDQ1OVoXDTMxMDYxOTA5NDQ1OVowbzELMAkGA1UE
BhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28uLCBMdGQuMTgw
NgYDVQQDDC9Tbm93YmFsbCBFQ0MgUDM4NCBDbGFpbSBTaWduaW5nIElDQSBmb3Ig
QzJQQSBHMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJvqMpOfFPJuwAV3s2YGMbe7
mRj48rsUZsJrMSOWU3SnssFtPEWTFh8QyM88zaFuprKC8nm53vnEMkGG+YtmZdBf
zsmayZqs9Rq6EQchDSF2vVNvE9wxAqhKJIawhYSTe6OCAZkwggGVMBIGA1UdEwEB
/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCkGA1UdJQQiMCAGCisGAQQBg+he
AgEGCCsGAQUFBwMEBggrBgEFBQcDJDAdBgNVHQ4EFgQUt0FFkasBa70GGSB6Ntn5
2UCzqwYwHwYDVR0jBBgwFoAU6R4F1vboIaKQly8jCAQX2lQhWPQwFwYDVR0gBBAw
DjAMBgorBgEEAYPoXgEBMIGUBggrBgEFBQcBAQSBhzCBhDAtBggrBgEFBQcwAYYh
aHR0cDovL29jc3AuYzJwYS5zbm93YmFsbHRlY2guY29tMFMGCCsGAQUFBzAChkdo
dHRwOi8vY2FjZXJ0cy5jMnBhLnNub3diYWxsdGVjaC5jb20vU25vd2JhbGxFQ0NQ
Mzg0Um9vdENBZm9yQzJQQUcxLmNydDBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
Y3JsLmMycGEuc25vd2JhbGx0ZWNoLmNvbS9Tbm93YmFsbEVDQ1AzODRSb290Q0Fm
b3JDMlBBRzEuY3JsMAoGCCqGSM49BAMDA2gAMGUCMQCp0U69fhqepzep6uvRFmGg
e+mgVJX1a2JsC3m+W6GAA62UruIAskoTmrEC0iZXMwYCMAcKDRNtDSwk9balyQ/8
RUPGGwGS2AvOsUC0H84jMBNCL+DlPg7dJz1gc32QCv9sWA==
-----END CERTIFICATE-----
Subject CN=Encypher C2PA Root CA 2026, OU=CA Division, O=Encypher Corp., L=San Francisco, ST=California, C=US
-----BEGIN CERTIFICATE-----
MIIDFDCCApqgAwIBAgIUS2zOIYRKaa59snU3NcNFadfTtoAwCgYIKoZIzj0EAwMw
gY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T
YW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwL
Q0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2
MB4XDTI2MDUyMjEzNDk1NloXDTQ2MDUxNzEzNDk1NlowgY4xCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcw
FQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xIzAh
BgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2MHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAE27XmRBc4mE0dYfUhLiuPiAnY3tRDMke9evylB+VgyMspT1svR9h3
KXxr8tml+1oM82US+pw2jJenfUxoUJGVDY/lvwl+4Q+7vawXFd26yPv/CQSnhI+w
05+yiSiwshIOo4G2MIGzMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQD
AgEGMB0GA1UdDgQWBBS3gC7ps3/HLQbb3uuqL4U8gDnKEDAfBgNVHSMEGDAWgBS3
gC7ps3/HLQbb3uuqL4U8gDnKEDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAy
BggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9j
cHMwCgYIKoZIzj0EAwMDaAAwZQIwYeb83YEb/Oh9uELuvO9jpb85orRWWQiceqf0
sCwxtJw07/giyy/hI1BFvbQZcfkLAjEAj8jfwuJSJ9LjeeYL/VXStQWDHewxbIou
IKdYpS9ZBidpsi+01tpQY1j7apLAXMHO
-----END CERTIFICATE-----
Subject CN=Encypher C2PA Issuing CA 2026, OU=CA Division, O=Encypher Corp., L=San Francisco, ST=California, C=US
-----BEGIN CERTIFICATE-----
MIID6zCCA3GgAwIBAgIUXzMIOXOrlyOjfKthGAPSSBM5n3UwCgYIKoZIzj0EAwMw
gY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T
YW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwL
Q0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2
MB4XDTI2MDUyMjEzNDk1NloXDTMxMDUyMzEzNDk1NlowgZExCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcw
FQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xJjAk
BgNVBAMMHUVuY3lwaGVyIEMyUEEgSXNzdWluZyBDQSAyMDI2MHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEINEuEOHZL9o8vKynpXV9O+04syCIj+WeydlgTMJBMhiZ8MhE
yig+4R0kmGpX/RZjgSSQh/aQ4NYpv9SHx1r1t3agJEpfYv/jZB4AfeGC47OiEREe
wKHBWD1cVg6qrFtVo4IBiTCCAYUwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B
Af8EBAMCAQYwIAYDVR0lBBkwFwYKKwYBBAGD6F4CAQYJKoZIhvcvAQEFMB0GA1Ud
DgQWBBT5K7GC8GHRLCg4CpI6Owg8ubeZ8DAfBgNVHSMEGDAWgBS3gC7ps3/HLQbb
3uuqL4U8gDnKEDA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY2EuZW5jeXBoZXIu
Y29tL2NybC9yb290LWNhLmNybDB1BggrBgEFBQcBAQRpMGcwJwYIKwYBBQUHMAGG
G2h0dHA6Ly9jYS5lbmN5cGhlci5jb20vb2NzcDA8BggrBgEFBQcwAoYwaHR0cDov
L2NhLmVuY3lwaGVyLmNvbS9yZXBvc2l0b3J5L2lzc3VpbmctY2EuY3J0ME0GA1Ud
IARGMEQwQgYKKwYBBAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVu
Y3lwaGVyLmNvbS9yZXBvc2l0b3J5L2NwczAKBggqhkjOPQQDAwNoADBlAjABzhlt
QpFyat0q5DZPJh+XvWnRAUej/4yrnPkQgtZZufnTVg9csupYAtdD8EVvkJ8CMQDK
aReb1d/xaGyypi9HwEqVRaEClhopAogb+8VtoUbBD/3mKv0rGlYOUa9heE2DRkw=
-----END CERTIFICATE-----
Subject CN=TrustAsia C2PA RSA Root CA, O=TrustAsia Technologies, Inc., C=CN
-----BEGIN CERTIFICATE-----
MIIFqDCCA5CgAwIBAgIUNqYa1cdsY+cp091HGRPKXzrE4kwwDQYJKoZIhvcNAQEM
BQAwWTELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp
ZXMsIEluYy4xIzAhBgNVBAMMGlRydXN0QXNpYSBDMlBBIFJTQSBSb290IENBMCAX
DTI2MDYwNTAzNTMwMFoYDzIwNTEwNjA1MDM1MjU5WjBZMQswCQYDVQQGEwJDTjEl
MCMGA1UECgwcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEjMCEGA1UEAwwa
VHJ1c3RBc2lhIEMyUEEgUlNBIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQCvgJqlqXZ+SYbtl9dLK4SQ5Gt9KrPbOmWEeMYnh8UdB3+KzkrG
6PSZ1/s6KDIiEPNH1Sno7UOnTWzZ50Ce3Qq4PKFJBb9iJ0cbtw4qGOjHDhgXhozv
nPQhXy6uiEODKgsKRaZwxT7xer3ZOAbX/30tQDwyAavoEtb+F+o2faYxHwjojUnK
zO0Aaz68h7U5zMHgdMUvkqc0B5NIpoTixCj7Om2X0XROWAFx7ELrVxMmyL4PCWOj
caRXoP5QlDHiIc5Ck9/HD6VYr2jJG/8BFZqk9l+mgNlxjaIbp0D/B3D7WvxcGOFR
qiRJRni3LAsdVAMGPs/msUVBsYnTIEWb1o8qvkKpPOE0OtseEhWIQX4EDTA3Kim1
xVqcS3+9irfDf7Jda2vOX1mamZa+Y89xCZcG7ce4u0fpE/NOs46dymoG/zU0uUvd
KOtzvddNuDOWymROa6SVG6uETSdVFRsQfR/Dp8MQegVLB6ITAfnXHc3Pn67B1ncs
L359DcwlZkqYGegsz//8ToRAlSSA/9C9vzlXfbGIg2HHtToHcwa6BjDkRpfdm6nk
egfCvoaWT4k/9vDQNXvg8Wee1vpUDKhB7y1SSaSkm+7wr8fV4ovFGSrbygpr0BuG
m+ShU3Vh+8UXAICPntscPAgNXidB9A8Zgib5rJjiVi0sxizPYueAchPQDwIDAQAB
o2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E
FgQUCdViRLQz3hH0IboHIKjSrMMHRYAwHwYDVR0jBBgwFoAUCdViRLQz3hH0IboH
IKjSrMMHRYAwDQYJKoZIhvcNAQEMBQADggIBAHInmN938SAQOkERvIrZDi14uVmZ
P5GgqQ6ZH4BF77RFcQcDl7Id+3WBf1WqSl0zyuq6HulYyM6pWikh0W5K/NBBLlt+
zXtEHbW71jhAhCKALdbclT+KuqRRQbrzATXPyYB5Jb/7xlUIofilo70+lqNwahsy
Y5F+6XlOZ4JuA84RdfxGdyn/IJlOJ9mc2RbI6aAxMuFcsot483gOYtkU4Og0g9M7
XsdLT2VXwuVk3hXwc7UWlZUm1Vn6dGmrcSj41wGlw9JGMU87OfzYFseGBDGDc2Xv
njFCY+SBAEWGk4m6ugNGat8OKBVNpnYGp0iNODMtQXJ0aUCvOziFPJY9li08sUDe
HI9ZwLB+yADsaC1gaX4eloRFsIyyl7YRQEY1/EfVw3wnN+XBJYyBw9v1RtpXDh/v
O2ezzXc1sdH/WgXMGGrRdiCAZb5mFkjTEoKXVnkGMn7bnJKuFgDQPWerA24Jck1J
QTuQBXJOAt8FVaYMjL6wPSIv84UjVXIRkOP4ZOydpYgykEl5LfOWcQtA4U6h+obS
35aB8YE1KC9rIeEE8q5onGo6tRYJzh+mB/XSUP35fNbwe4FkI3I6ZpNSS6wt9HY0
DCYCNhAz7KP8RwexR/stlYyeUxbj3+jxK4AOGTZQEKBBWkoqfrPCDpybwpXCxxP5
qtj6+iKCvpx9jmvr
-----END CERTIFICATE-----
Subject CN=TrustAsia C2PA ECC Root CA, O=TrustAsia Technologies, Inc., C=CN
-----BEGIN CERTIFICATE-----
MIICWjCCAd+gAwIBAgIUdUWMu3G69tNb9bwz/0KTupxLm1kwCgYIKoZIzj0EAwMw
WTELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
IEluYy4xIzAhBgNVBAMMGlRydXN0QXNpYSBDMlBBIEVDQyBSb290IENBMCAXDTI2
MDYwNTAzNTAwMFoYDzIwNTEwNjA1MDM0OTU5WjBZMQswCQYDVQQGEwJDTjElMCMG
A1UECgwcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEjMCEGA1UEAwwaVHJ1
c3RBc2lhIEMyUEEgRUNDIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQL
ERY0d7ug9MiCqmXnDjDs2med9zfTqTWOcG3OD5Q6FSEgNUcjT5Qy7xzKn1MfqaY4
Ye3sf+Do7cCDR6AiFW6SqLQobfJ70jMDsSQYlrCCjMLvb+NdNPhED43s8fgehxyj
ZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQIwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW
BBT13DKCd7Jlz2rEe0vBlwo/CoR2mDAfBgNVHSMEGDAWgBT13DKCd7Jlz2rEe0vB
lwo/CoR2mDAKBggqhkjOPQQDAwNpADBmAjEAwp2Jie8k6/haTc/df3QsX+Ttg7Wp
ARtYTwJSP4Z+ggpf39sZ+7QPOE50BqBj9Q6zAjEAjZ+5glED9tav6r3jWuMOmKTB
kobBXzvjqTDLGG570BbSQfNmS3HRC6u5YTIpElpb
-----END CERTIFICATE-----
Subject CN=RealReel C2PA Root CA, O=Whole Earth Labs LLC, C=US
-----BEGIN CERTIFICATE-----
MIICPzCCAcWgAwIBAgIUK7g4QvDKVbdq1t88l76ZMwiegK0wCgYIKoZIzj0EAwMw
TDELMAkGA1UEBhMCVVMxHTAbBgNVBAoMFFdob2xlIEVhcnRoIExhYnMgTExDMR4w
HAYDVQQDDBVSZWFsUmVlbCBDMlBBIFJvb3QgQ0EwIBcNMjYwNzI0MDAyNzAyWhgP
MjA1MTA3MjQwMDI3MDJaMEwxCzAJBgNVBAYTAlVTMR0wGwYDVQQKDBRXaG9sZSBF
YXJ0aCBMYWJzIExMQzEeMBwGA1UEAwwVUmVhbFJlZWwgQzJQQSBSb290IENBMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEJVWLUwyoBcqwmv3cUuX4/fpVanvQdjJKG4Ca
zLx5XO/eu7qgxCurzpLTuRAC3Lr+tY4LA5shXTSFYqNRRlQnxkYugqEfqDJcUhks
1evDAlYZdBB3X134zjk9DHQElntEo2YwZDASBgNVHRMBAf8ECDAGAQH/AgEBMA4G
A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoaV/7ghFZ89nsoENzOrkL+Pmwg0wHwYD
VR0jBBgwFoAUoaV/7ghFZ89nsoENzOrkL+Pmwg0wCgYIKoZIzj0EAwMDaAAwZQIw
F8nY6P9IvWgtd9dKjnelTOZwICUuxVHbd/uHvU9hjwez+B33zyL1iP+8E/HUe9yD
AjEAmUnK720BAi3UXkj8kZHs74wnIy4oh8buxmiecO1akF4I3B+0+18SkclV6ytF
+Zms
-----END CERTIFICATE-----
Subject CN=Castlabs C2PA ECC P-384 Root CA, OU=C2PA Trust Infrastructure, O=Castlabs, L=Berlin, ST=Berlin, C=DE
-----BEGIN CERTIFICATE-----
MIICyDCCAk2gAwIBAgIUN5SkK2Lb2Ts/j1AC3K8Y3eVydFQwCgYIKoZIzj0EAwMw
gZAxKDAmBgNVBAMMH0Nhc3RsYWJzIEMyUEEgRUNDIFAtMzg0IFJvb3QgQ0ExCzAJ
BgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xETAPBgNVBAoMCENhc3RsYWJzMSIw
IAYDVQQLDBlDMlBBIFRydXN0IEluZnJhc3RydWN0dXJlMQ8wDQYDVQQIDAZCZXJs
aW4wHhcNMjYwNzA4MDE0NDMwWhcNMzYwNzA1MDE0NDMwWjCBkDEoMCYGA1UEAwwf
Q2FzdGxhYnMgQzJQQSBFQ0MgUC0zODQgUm9vdCBDQTELMAkGA1UEBhMCREUxDzAN
BgNVBAcMBkJlcmxpbjERMA8GA1UECgwIQ2FzdGxhYnMxIjAgBgNVBAsMGUMyUEEg
VHJ1c3QgSW5mcmFzdHJ1Y3R1cmUxDzANBgNVBAgMBkJlcmxpbjB2MBAGByqGSM49
AgEGBSuBBAAiA2IABPXhd9L+pjQVnUXH6I7+oFSrd6JpO4A+CYDm/AuDZgJvwiuJ
lrM9dC3CAE7UAK8IcQEo85qzuak0TfLdPaJWD26+q3tJ/S7NW4sOKhH09KRN7iFq
XbsOe8FOxaQql/UrMqNmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB
Af8CAQIwHQYDVR0OBBYEFHNKe2PoAANQBj4/25eRv2VY4Fy7MB8GA1UdIwQYMBaA
FHNKe2PoAANQBj4/25eRv2VY4Fy7MAoGCCqGSM49BAMDA2kAMGYCMQDUlNJD49Ux
zwBccYHiz0wgmbIXBDdgpVrLfWTQnbi21BhfPBQ6j0HJ24FY36L5oj8CMQCv5+bK
gBIkJq9USBx2FjjDkukYjQMI/WDD6SH6QGTdotsH3cQQ6bjr9C+0ruxxx08=
-----END CERTIFICATE-----
+496
View File
@@ -0,0 +1,496 @@
Subject CN=Google C2PA Core Time-Stamping ICA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIICzzCCAlagAwIBAgIURQCDbnITAsVkpJ5kM3b6jwm3ZPQwCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjI2WhcNNDAwNTA4MjIz
NjI2WjBSMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEuMCwGA1UE
AwwlR29vZ2xlIEMyUEEgQ29yZSBUaW1lLVN0YW1waW5nIElDQSBHMzB2MBAGByqG
SM49AgEGBSuBBAAiA2IABKN99/G9CCofRVkl4FL5qSDf/tsuj0Uh2E8K1c0Dcd1n
KixZbsCcJDJyInm5ApFfuabKR5+nxTRzE35exSVE6TEijjTVuBb+GsGrM+rGISwj
T/8B5ODBf/A4a8VyrSVLCqOB+zCB+DAXBgNVHSAEEDAOMAwGCisGAQQBg+heAQEw
DgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBIGA1UdEwEB/wQI
MAYBAf8CAQAwZAYIKwYBBQUHAQEEWDBWMCwGCCsGAQUFBzAChiBodHRwOi8vcGtp
Lmdvb2cvYzJwYS9yb290LWczLmNydDAmBggrBgEFBQcwAYYaaHR0cDovL2MycGEt
b2NzcC5wa2kuZ29vZy8wHwYDVR0jBBgwFoAUnFzYiVND51rVgdsD3hl/BCoqLaow
HQYDVR0OBBYEFN5Vl4xgdDsD4mq0RAZll2HK5fiOMAoGCCqGSM49BAMDA2cAMGQC
MEHGBo0dSnwBldblTYF0fGBdzHBCW0oRhGP/pYfclCTYgcyo+UdR5nYuiHZpKFhQ
cQIwcAumLdMem8XpEJsAEedT9O0lo+ksaufwbJ93BVh5HG3h37rxij8nE064uhpS
PiMt
-----END CERTIFICATE-----
Subject CN=Google C2PA Pixel Time-Stamping ICA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIIC0TCCAlegAwIBAgIUP2ie1DhmCwB0kqG0zYX4MkF5VsgwCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwHhcNMjUwNTA4MjIzNjQ1WhcNNDAwNTA4MjIz
NjQ1WjBTMQswCQYDVQQGEwJVUzETMBEGA1UECgwKR29vZ2xlIExMQzEvMC0GA1UE
AwwmR29vZ2xlIEMyUEEgUGl4ZWwgVGltZS1TdGFtcGluZyBJQ0EgRzMwdjAQBgcq
hkjOPQIBBgUrgQQAIgNiAAQ+8JePdivzSowun2WW4rDH0LXX9Iurbgh3xNHoKtIw
ei3+ayp1TC1AKjLbF+ZeNxLXHGaF6lwC20eGLp/gK7moar9ob8ltCnWFZXTZE1gK
BMORiCbTf1iVnRUykBcdr8ajgfswgfgwFwYDVR0gBBAwDjAMBgorBgEEAYPoXgEB
MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDCDASBgNVHRMBAf8E
CDAGAQH/AgEAMGQGCCsGAQUFBwEBBFgwVjAsBggrBgEFBQcwAoYgaHR0cDovL3Br
aS5nb29nL2MycGEvcm9vdC1nMy5jcnQwJgYIKwYBBQUHMAGGGmh0dHA6Ly9jMnBh
LW9jc3AucGtpLmdvb2cvMB8GA1UdIwQYMBaAFJxc2IlTQ+da1YHbA94ZfwQqKi2q
MB0GA1UdDgQWBBSnltnit0LAWi0rd0qr3tBEM58EuzAKBggqhkjOPQQDAwNoADBl
AjEAsVIYGAu0tVemHYqq082P164T0B6NWstqZU2RTv8ZMV21tdBJYwnwDWWSYLmF
8s7kAjA59j5cHDebfKTUPcnxyhMUIJJ+qU6x8wM5E86O0FZyhb2G/kr3HrLkqzmB
Mo9QvzU=
-----END CERTIFICATE-----
Subject CN=Google C2PA Root CA G3, O=Google LLC, C=US
-----BEGIN CERTIFICATE-----
MIICLjCCAbOgAwIBAgIUUZK4AROFKiXQZ1UG7FG6qPGc1g8wCgYIKoZIzj0EAwMw
QzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkdvb2dsZSBMTEMxHzAdBgNVBAMMFkdv
b2dsZSBDMlBBIFJvb3QgQ0EgRzMwIBcNMjUwNTA4MjIzMjIxWhgPMjA1MDA1MDgy
MjMyMjFaMEMxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApHb29nbGUgTExDMR8wHQYD
VQQDDBZHb29nbGUgQzJQQSBSb290IENBIEczMHYwEAYHKoZIzj0CAQYFK4EEACID
YgAEhv9f/juKcPpe3Fm7eAISMuSyS+tBxn0aYHC83J+qAsFWREGN9p6PN/OBoouP
zpOFRxvrlWoWmAI3p1lXyPg4E3eg7SNChgopUIpihGu6qlhP8rLXf3p8bhI5FTQ2
MaF2o2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAfBgNV
HSMEGDAWgBScXNiJU0PnWtWB2wPeGX8EKiotqjAdBgNVHQ4EFgQUnFzYiVND51rV
gdsD3hl/BCoqLaowCgYIKoZIzj0EAwMDaQAwZgIxAIyVEe5bdUMkk6BthEWy9QSE
Mb74BOyK8/8pgMX0NPwLlo1ikLNY78ov+k21vZrEZQIxANQ91muDXgPjAMAkzAlK
i32Z9VBB37ynTveKVC7ofTW0ZFfIIYYpWUR1+C4m2yRkOQ==
-----END CERTIFICATE-----
Subject CN=SSL.com C2PA RSA Root CA 2025, O=SSL Corporation, C=US
-----BEGIN CERTIFICATE-----
MIIFlDCCA3ygAwIBAgIUExeshkq/ESresWEq3YWcEUTmxvowDQYJKoZIhvcNAQEL
BQAwTzEmMCQGA1UEAwwdU1NMLmNvbSBDMlBBIFJTQSBSb290IENBIDIwMjUxGDAW
BgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjELMAkGA1UEBhMCVVMwIBcNMjUxMTA3MTYy
NzEzWhgPMjA1MDExMDExNjI3MTNaME8xJjAkBgNVBAMMHVNTTC5jb20gQzJQQSBS
U0EgUm9vdCBDQSAyMDI1MRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xCzAJBgNV
BAYTAlVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA07LXx05AgfZn
F6CPMzpPwA+vGxfqi2Pw3/gvhAe4UdzmasSutCRK9sK66DZYta4AbYPS0/2IToqo
1n33f9/XrmQ62uM3XAyKXfg8UWUaj2WsyA3dIxY5l80B0S0EX0V1HpP6ygw1IDfJ
/KKkknn/0o14zmNkRzLpqQDOMoL4s+kli8NtkEOG8M8NrH5QJCNRL7lN0DG8TvRa
10Zfwyxnd4yw9DT2HWJInjC+Hww9QZua/u6bTy0pFHV0vVCgKQuPtbX8b60998t4
b56trGesWapUefIEu6GH1CkQu7nhsk1Rf59+uPv1NQLOm7wbyghHtQQ00baR/687
r7/qXxRLV1CFNH3nAgFfHkD8N07GWS5C1OOmkUPrc2f9tdKWOJ6USDJCOX1DkTee
o9RgwSJfLueDtAOp19dlBo2UW79A2MGN6VI119DoW04VUjxmPHLIWtt08ZRTHzvN
2I2DA3ic5Wt4pgSLXDZ6ztlFX4Y6nXwVFdWae9zwLpm9ttsF4fepLyPAUTuT9m0j
/oeQt3KmyxIukYD8I1PiSQ6YDLIiWFZcYC8LMyiYmQEX6spsE+IF37lx2nZ1OF+D
AOSCYYamZx6HYShgN4zN1QYTMJdxbz5JgRTIIioVdJTqeF/5GsSGP6f9Eym8ND9j
LQzm8MBEMfvlAgw0q1ieN2Q9INSN3qMCAwEAAaNmMGQwEgYDVR0TAQH/BAgwBgEB
/wIBAjAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPwqSnU6gPqZY5Pwc1fsvpOw
fcN7MB8GA1UdIwQYMBaAFPwqSnU6gPqZY5Pwc1fsvpOwfcN7MA0GCSqGSIb3DQEB
CwUAA4ICAQBAqkR9j5wKs0T7e9gHTUWlLAnkWLyZI7AuEoXimyJ7Ocy1EwZyu3TD
Z2jCecLNXENnUwjuKaEm/8sCwzrVqpxzIgkS1jVLuVURlrj2+apT/7wBw8LCKMZ6
cDhQJV3A1+9hyCc5ozkE1LgcJfWELrBWeBuj8rWLAf0DDgtwaED7nS9c9P8UgrxH
YTMe2AHWEfczXBirx5ubpNaokR0W01aSdXa97ohoS+ZmK0bxvmWCugI4tfsypsVl
lV2iD9v/BPLCVHP+7ot520mE21WdQKD69M54V8KAqfmNoG6I3TB2EcKJ0NpKI0zW
niSX05NDrv70INu+vAI8Sso/mRMHpuo03/0iTm3z8J4ACjwP071U0R3wCjTxN8z2
95EwOf1/JQH5h0njkarxFsWsnHJtWwE3kGEibO3ivB2U3FyS1osN0bEA4X5CSeMF
GDJFCXVB0l4/KeHHNRk3DEVjr5dzcwt/C2DLj9VuaeWEWuVbA3/PDR/MnZzDtPj1
TVLswGkWKwG+AyJaHkLysZD0IKwL4SZGGCRvyKw+G5Go0vvHVyUMPHqVTBApxY9l
pt6absyMPdRnf+w0tdFlhCd2rauRA0HX7JZ5xqnYjkfk0m7MpoauCsQR9udJvv5H
pMv7wvJRchf62/pPnjLKhN/dx9LR/EVqBPbPMhqdzO6ObQdqUJzXPQ==
-----END CERTIFICATE-----
Subject CN=SSL.com C2PA ECC Root CA 2025, O=SSL Corporation, C=US
-----BEGIN CERTIFICATE-----
MIICRTCCAcugAwIBAgIUHTAeXakkTyAFDkZfyu8YyCO9ubgwCgYIKoZIzj0EAwIw
TzEmMCQGA1UEAwwdU1NMLmNvbSBDMlBBIEVDQyBSb290IENBIDIwMjUxGDAWBgNV
BAoMD1NTTCBDb3Jwb3JhdGlvbjELMAkGA1UEBhMCVVMwIBcNMjUwNzE4MTU1MTQ5
WhgPMjA1MDA3MTIxNTUxNDlaME8xJjAkBgNVBAMMHVNTTC5jb20gQzJQQSBFQ0Mg
Um9vdCBDQSAyMDI1MRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xCzAJBgNVBAYT
AlVTMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEo3WOe9pIesN2XWe6KQdiUO+cU1mr
+Bs8opia0I+IA5m8oYhBJmJWPxLea7PH6tlW6f5wqkIOaeJkJ7X1pz3IHPjO8qkX
imKjiUwt/B7IoEj6rhoqkAV4AMO2BMxbS1MUo2YwZDASBgNVHRMBAf8ECDAGAQH/
AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUU50WGoV8zfcRPowvdPbBkJ1O
g6swHwYDVR0jBBgwFoAUU50WGoV8zfcRPowvdPbBkJ1Og6swCgYIKoZIzj0EAwID
aAAwZQIxALxN0Q+F+9KXOnYUKcW70UuxgitT8iwjTBGCIXJbJw4XaMtVdVGf4HwY
NITBgr+rWwIwczO4trazVV27OTCl8xNR0IH4UuyFgBVuzMsIj8IPkQw5mVhtvhY7
nNgp3Dyy2LeE
-----END CERTIFICATE-----
Subject emailAddress=ca@trufo.ai, CN=Trufo C2PA Root CA (2025, ECC P384), OU=CA Division, O=Trufo Inc., L=New York, ST=New York, C=US
-----BEGIN CERTIFICATE-----
MIIDOTCCAr6gAwIBAgIUbXqcPd1r9yQm/fznG9RlSWyXiwswCgYIKoZIzj0EAwMw
gagxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3
IFlvcmsxEzARBgNVBAoMClRydWZvIEluYy4xFDASBgNVBAsMC0NBIERpdmlzaW9u
MRowGAYJKoZIhvcNAQkBFgtjYUB0cnVmby5haTEsMCoGA1UEAwwjVHJ1Zm8gQzJQ
QSBSb290IENBICgyMDI1LCBFQ0MgUDM4NCkwHhcNMjUxMjMwMTkwNTAzWhcNNDUx
MjI1MTkwNTAzWjCBqDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREw
DwYDVQQHDAhOZXcgWW9yazETMBEGA1UECgwKVHJ1Zm8gSW5jLjEUMBIGA1UECwwL
Q0EgRGl2aXNpb24xGjAYBgkqhkiG9w0BCQEWC2NhQHRydWZvLmFpMSwwKgYDVQQD
DCNUcnVmbyBDMlBBIFJvb3QgQ0EgKDIwMjUsIEVDQyBQMzg0KTB2MBAGByqGSM49
AgEGBSuBBAAiA2IABAp0qnhIwMtN6LeGdBVtHLPn85ecetr/lqcXFk8ypK9ukJzU
8LLv55Kh/MYTgEnuIKEOPhDxLDRdahc0mAjRnql4kLk395abw9WZjrBPek3qjv0q
ITR8VPYFABuZ5FRKx6OBpjCBozAdBgNVHQ4EFgQUA9Vfr36D5QQdWYAnSjT/Rf3r
SXgwHwYDVR0jBBgwFoAUA9Vfr36D5QQdWYAnSjT/Rf3rSXgwEgYDVR0TAQH/BAgw
BgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgorBgEEAYPoPAEB
MCQwIgYIKwYBBQUHAgEWFmh0dHBzOi8vdHJ1Zm8uYWkvY3BjcHMwCgYIKoZIzj0E
AwMDaQAwZgIxAMUeYWZyxS2maiVkNETL29RAuLn/gHYTkt97l6evXwHLN46v28mI
39BIf6slyWnrCwIxAPRs/FJ+DoA0d/PCkrF946S+pG7vRqLnjB9OhMdmrMPvzaqx
KQYOBVx7SE4Kz48W8A==
-----END CERTIFICATE-----
Subject CN=DigiCert RSA4096 TSA ICA for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIIGoTCCBImgAwIBAgIQHlk+WwlryKtFqaffcO7jpjANBgkqhkiG9w0BAQwFADBS
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKjAoBgNVBAMT
IURpZ2lDZXJ0IFJTQTQwOTYgUm9vdCBmb3IgQzJQQSBHMTAeFw0yNTA4MjcwMDAw
MDBaFw00MDA4MjYyMzU5NTlaMFUxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp
Q2VydCwgSW5jLjEtMCsGA1UEAxMkRGlnaUNlcnQgUlNBNDA5NiBUU0EgSUNBIGZv
ciBDMlBBIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyK6dRasG
6TUERcSoXIhQ1/Vk0FXmVELjxjqeQMAFCNJkUm8MEdV0g5va49TO/n7N1stKakEZ
Bu2wtNElU7Iz2ht048r5f9mCksUHQzpyKxytCsaE/sWmth4StqtRqjlF2sXrMRVc
5t/bHMOXxclMpuGMWggwKHPxSqQKd2/AuTIzfWshQPUrU/JLxv0BbRCXGm1tQ9CQ
BNrl1xQFxOZ0D+j4eDRYqZNo/jlKzAzY1OMN92rErcbTyZ6Senr7MYwsKKHjeRCO
tfcSmQCp7qw2mlF9QkqFSLPNirv4rSYfrkrWsrue6cYWSKGB+sEEyTOHTGNxi4e4
NvX+1n8I3jPmbPQEgb3O+UTOxKa+mULBhwHaXeV1wfczgEBRdRXfn+Y7btJjLr8S
zroU0lTZTyarII/WKYKbHJwWF/Tni1Eu4/EnRxMQJz+gXBadQAM3RhfeYqiz53rd
aQgPosKqls1+0hJ4KVVcstiTeytSuG6bhDHL14fgSSl7aGndFU+6wBz+KIBVxs9j
38cQSiqJWgm30erGj2DTCEPDpFvBvSaDHm59kviaFLq/RgHqM1wDwXtyHrimMKmd
cCW+PWlhFqdpUiQ90a5JVIAJyWiOZ7Ko7GsG4y1nohJ9Wvm0V0AoQw+TFOPHRbVO
nhY/UIV9OZWZE9jTsWq2SD216kVvF4eCWi8CAwEAAaOCAW4wggFqMBIGA1UdEwEB
/wQIMAYBAf8CAQAwFwYDVR0gBBAwDjAMBgorBgEEAYPoXgEBMBMGA1UdJQQMMAoG
CCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIBBjCBhgYIKwYBBQUHAQEEejB4MCgGCCsG
AQUFBzABhhxodHRwOi8vb2NzcC5vbmUuZGlnaWNlcnQuY29tMEwGCCsGAQUFBzAC
hkBodHRwOi8vY2FjZXJ0cy5vbmUuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0UlNBNDA5
NlJvb3Rmb3JDMlBBRzEuY3J0ME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9jcmwu
b25lLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFJTQTQwOTZSb290Zm9yQzJQQUcxLmNy
bDAdBgNVHQ4EFgQURnZBGfLaiWXQIbuFjJH9X1GWG7wwHwYDVR0jBBgwFoAUVusn
OJo+SNZygIWbesMHUVoE8+UwDQYJKoZIhvcNAQEMBQADggIBAFB06NmfYUkxUr55
hxDUOKmwrB7w5KKSnNkF90JJw3pXILQjutAs5rD6KuExSPwa/bMe1fMx0Eh+ma/U
4tPwJMyzyjNppXHbOhE7elQbqnXUw6eeLoxwKxTGQfKGwNA6DqNGHnNcT8325pxl
b0YmQ4IAoy93b9tt51q8suRH/3JhEbEFNHKlpU4lJKGWrirqL3D8g75ghKwA7E/2
q9sWUmxOm5oHN8AoiGD3NXUCdGKWGhCSVxJB0c0mH6hJ9Pa+HO9aKRIdGb2EdciP
mIICOtrXMHIZd9VZJoVORQ0SI/9d92W9KAkZPFsa4xySVh/AnmsL5JVSa88JudFd
0yNRjdII4mqGvUu+FpwxAq0RUhnzAP9pUoEvq0lWd+LTxfkvvKg4hcL8nvHgxNqK
JEIdGXcouuFGp7ZPjw5OX6b4lycjtZrw+dvsihQ3KHpHqcDHSmMUHHk6mrj+0iP6
xgrUpk02Q9kMEvN5Hf9M9DR6IgEv5b9B20x/ZaeEY7/PNlG7dAiBi+TN1s1/p0/W
GqP622vsQp/xmykypJd1H59AbdjW4kq16XkxQav3vDg4XzG09/1ztUICatcdVvgG
hCKOlPVNVK4Cp8+ylNKbApOzwZJ8s+wj8PnQBPtqSymVufdkGkyQiR0+aDJ9VXld
gTGEoa59zjtlgSyXKpTfifENxWpk
-----END CERTIFICATE-----
Subject CN=DigiCert ECC P384 TSA ICA for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIIDVTCCAtqgAwIBAgIQa/vORh9Mst4bJJF5DoeAazAKBggqhkjOPQQDAzBTMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKzApBgNVBAMTIkRp
Z2lDZXJ0IEVDQyBQMzg0IFJvb3QgZm9yIEMyUEEgRzEwHhcNMjUwODI3MDAwMDAw
WhcNNDAwODI2MjM1OTU5WjBWMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNl
cnQsIEluYy4xLjAsBgNVBAMTJURpZ2lDZXJ0IEVDQyBQMzg0IFRTQSBJQ0EgZm9y
IEMyUEEgRzEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT8G1ndHhFlZDBFZ0YyRmd6
x6AKtDvnSWQLr3tfRq4mDLw4veOCBgfqPJH2ofVoFuNVUpnaWIJQKx5tush8yQgG
NUDqcDtjcWqz+tBVNRPF7N1bXCrmPVHnOVxMlq9ykeOjggFuMIIBajASBgNVHRMB
Af8ECDAGAQH/AgEAMIGGBggrBgEFBQcBAQR6MHgwKAYIKwYBBQUHMAGGHGh0dHA6
Ly9vY3NwLm9uZS5kaWdpY2VydC5jb20wTAYIKwYBBQUHMAKGQGh0dHA6Ly9jYWNl
cnRzLm9uZS5kaWdpY2VydC5jb20vRGlnaUNlcnRFQ0NQMzg0Um9vdGZvckMyUEFH
MS5jcnQwFwYDVR0gBBAwDjAMBgorBgEEAYPoXgEBME0GA1UdHwRGMEQwQqBAoD6G
PGh0dHA6Ly9jcmwub25lLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEVDQ1AzODRSb290
Zm9yQzJQQUcxLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMC
AQYwHQYDVR0OBBYEFCCVvm3C1NqJFBP6pBOCIhPGHwgRMB8GA1UdIwQYMBaAFPQY
Ea4JgolO/Ju0M/b0370CTpQ2MAoGCCqGSM49BAMDA2kAMGYCMQD5QJg0LWan44KK
sMic+nWXydVCGoa7hzlNDYvZfuA/kx/SPUWqssA3TDPPyxwziOkCMQCopKRPtJ87
CaQGUou1I+7hjCQWZvy0FaNEe/XYwK1kN/xc3U5ZqFqE50744xaOwsw=
-----END CERTIFICATE-----
Subject CN=DigiCert ECC P384 Root for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIICSTCCAc+gAwIBAgIQCZ2yarc/T5eBQYVHCJTdyjAKBggqhkjOPQQDAzBTMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKzApBgNVBAMTIkRp
Z2lDZXJ0IEVDQyBQMzg0IFJvb3QgZm9yIEMyUEEgRzEwIBcNMjUwODI3MDAwMDAw
WhgPMjA1MDA4MjYyMzU5NTlaMFMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp
Q2VydCwgSW5jLjErMCkGA1UEAxMiRGlnaUNlcnQgRUNDIFAzODQgUm9vdCBmb3Ig
QzJQQSBHMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNHiy4U9/SpPSnQ3ogU+q3mn
M/f9zmLuqF8krS0LNUC0T81Nu57WCGSb6149CVpkRRC6BYlfGcBWVYXar4aX10oX
ggSiFPi93ovkAfUN9wikKqVu6amI7tRQPKXJ95UsFaNmMGQwEgYDVR0TAQH/BAgw
BgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPQYEa4JgolO/Ju0M/b0
370CTpQ2MB8GA1UdIwQYMBaAFPQYEa4JgolO/Ju0M/b0370CTpQ2MAoGCCqGSM49
BAMDA2gAMGUCMQCg31cyC7If2VPMUvqEr08WB6QhRVpAa+GNRlKUOJlSMq/y2tm+
80RTxpV2RsYC9UsCMGkKQ0Yx4FIs1RhYtXVIgTyHrSxGne4/457DlIS+IMIJN5Yd
gMNj6G6tzM47QDGl2w==
-----END CERTIFICATE-----
Subject CN=DigiCert RSA4096 Root for C2PA G1, O=DigiCert, Inc., C=US
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgIQL2uv995UIpTpN6WRLwYnHDANBgkqhkiG9w0BAQwFADBS
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKjAoBgNVBAMT
IURpZ2lDZXJ0IFJTQTQwOTYgUm9vdCBmb3IgQzJQQSBHMTAgFw0yNTA4MjcwMDAw
MDBaGA8yMDUwMDgyNjIzNTk1OVowUjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
Z2lDZXJ0LCBJbmMuMSowKAYDVQQDEyFEaWdpQ2VydCBSU0E0MDk2IFJvb3QgZm9y
IEMyUEEgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDTX44Mw12/
in3jeV80FcoutZhxtvhZQHAdiFtiYd9Y+quavnHRqtcexRZQ0n1XtGXeiusE72M6
G4dwdTsHXoXVNXjwrO9a/fA/46u+/T5ZU6FyRQdahwQNIXL5PVfqrjwX2Ta2Gby2
z4tbhfzxtgJSosJGJsCGq5mGiBXIqBkbua0YOvuX1WifAuDQun84bw+qwCCWxSty
QuUcZMm6xl24ye5RZnz8xkctz/p68kgNa9IhJlHrZ3CAe1zEu3PrNS7Oq/uzN06t
Ji6rkpXCV6GlhN0RlEPmUb3pg8kYqQ9uca7u4UTyl/F9QqG2xMDgAtNDrfNbWpyO
fnoYGNQvQyD5M7slsz9j9sUcKwspaz8zmuu31UXPqmvME+UalDvFeknQZP+ft9Uv
8eICovCQxvy1Iwg3sjc2hO0lbYYn/JqGARg+5jP/SlkY4dSypdYBtBaAfVxTd+iQ
m4k1oAEIP7ujyQ+1U0F9gygQPwQrMOcyYINgkcsp36rb1znuxlIF6LIcfO1bkVSx
vHqd0CVP8vwrYXKJOr4dES1s4k12kxv+csvZWiuSqY0uhFUSMSE5id5zWW7H6ZCX
3J3Ati2MjyRjyyejDbTQdTS3wlF4f4bLaWidMWSZ3JCyZABXpj46RtdbhZgV7I7B
ZTXCI6FIBuUaiZzW9RHhVzR+qrfBOx40mwIDAQABo2YwZDASBgNVHRMBAf8ECDAG
AQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVusnOJo+SNZygIWbesMH
UVoE8+UwHwYDVR0jBBgwFoAUVusnOJo+SNZygIWbesMHUVoE8+UwDQYJKoZIhvcN
AQEMBQADggIBAIjcpKt4FeVibq6zP1m2RUagL6PEW+8o5CC5kv0mDMcCgqNTsyNJ
lr/1yYzzpEC82QaCZ34g4eMXnOwq+5jF5xQ5b7RV7X9nFuPXh7r2CittMkBLPKp/
jSApyfEI2qgNtPWc5voKRgPrkSuHu+wB5gTg7AkN9CCZeaauMesIIn7/8lWOSho9
a5DVCJekYh+WcfnSP6l0ilRw1WhnAtPwadhcQ9zJxKe6LSdsH3yNJhViIQNrBhlA
O3BNZF1q2JfOM6TCiY/I+M8AfzuvxF/xyxeDlcRNo6QbfAvUybDOsleg9NxO+EK3
VXTInzaWidoX9TRdEx0iiERj9OB7ehZBwTUllBH6UtxFy3X7YRSnO8ZdjtEERJmk
BNZztgd4KWBnaFMERI0ObiqN1az0uqe8sRULEJ1Ay79sTsJtiemNChMxmYb83XU1
1wSX/W54tD6vPQPCwn84AeLgYSfA10QwRyAo99pwmjZzrCx5uBH7jCVYBkP7iQ6h
o3yHror7znE8LJrrWytOuumEFzxQ0NSaXjrDRKCLKTKnr8niIj+lB/SJsBua52a6
HHRckrpDokgPSPOYI4A+ZGC//Ron9i3S8ce2dQHAWNCqB2ADtnu4Yf36OfmHdrCL
hzIxlPKKDPXwgxv7NTRFs1d7KcIpjYuZp71eZA74ZqxDgVa7CMkhdgML
-----END CERTIFICATE-----
Subject CN=Irdeto C2PA Root CA G1, O=Irdeto BV, C=NL
-----BEGIN CERTIFICATE-----
MIICLDCCAbGgAwIBAgIUL8JDbqsQllkL9gozEdKhKc9GFAcwCgYIKoZIzj0EAwMw
QjELMAkGA1UEBhMCTkwxEjAQBgNVBAoMCUlyZGV0byBCVjEfMB0GA1UEAwwWSXJk
ZXRvIEMyUEEgUm9vdCBDQSBHMTAgFw0yNTEyMDUxMjQ0NDBaGA8yMDUwMTIwNTEy
NDQ0MFowQjELMAkGA1UEBhMCTkwxEjAQBgNVBAoMCUlyZGV0byBCVjEfMB0GA1UE
AwwWSXJkZXRvIEMyUEEgUm9vdCBDQSBHMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA
BEOtgkWt4t4r3Fvn2vDshY1KcZ63/VTaZN7K2uiAcCeztiH+ZSFf4hA5cSCABRJ+
VkWZYLvnhNLsCVbtEij6mDN5hf4VIUdUNgUonGZ6tkL1Djs3CUIkjD+nH/HXbCSj
pqNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBAjAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O
BBYEFGs1MrTyCBABc00WKVK0njUakyplMB8GA1UdIwQYMBaAFGs1MrTyCBABc00W
KVK0njUakyplMAoGCCqGSM49BAMDA2kAMGYCMQCB1h+esIhAsddoGkZI1aXwwCFM
wL/cHskaM8JZsvUfcecGQmG08ZVesfUeCzei0SECMQCLjWbR4LY1mgfF+PzlieDZ
9DTcd5qOpJaSf90/8AnQA/UWGtRGqJsrmERInDoXEOA=
-----END CERTIFICATE-----
Subject CN=vivo Content Provenance and Authenticity Root CA, O=vivo Mobile Communication Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIIC0zCCAjSgAwIBAgIJAMIN3t2/xTDAMAoGCCqGSM49BAMEMHYxOTA3BgNVBAMM
MHZpdm8gQ29udGVudCBQcm92ZW5hbmNlIGFuZCBBdXRoZW50aWNpdHkgUm9vdCBD
QTEsMCoGA1UECgwjdml2byBNb2JpbGUgQ29tbXVuaWNhdGlvbiBDby4sIEx0ZC4x
CzAJBgNVBAYTAkNOMCAXDTI1MDkxODA5MDkxM1oYDzIwNTUwOTExMDkwOTEzWjB2
MTkwNwYDVQQDDDB2aXZvIENvbnRlbnQgUHJvdmVuYW5jZSBhbmQgQXV0aGVudGlj
aXR5IFJvb3QgQ0ExLDAqBgNVBAoMI3Zpdm8gTW9iaWxlIENvbW11bmljYXRpb24g
Q28uLCBMdGQuMQswCQYDVQQGEwJDTjCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE
ANQigNuQ6RnYFSK2BWkaksR3fRFFytX4nzX3E4hLV/N/7m/XttNHxNDase0cAXwR
9pO23p6fb/9NbifILv2wzPn2ASMr306Y2frv/UjJ5J6WgHet2OFywRaPjFjJWRuk
4JCy0qoSxyyx230g8GYdacZzIqSzVTW7/Xz4mjnWDTOB/Db+o2YwZDASBgNVHRMB
Af8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUgB0dwwUcpK4u
7az42vKC2y+nnY8wHwYDVR0jBBgwFoAUgB0dwwUcpK4u7az42vKC2y+nnY8wCgYI
KoZIzj0EAwQDgYwAMIGIAkIB9cjlRbw8I0+0VAIOENCEzKOfp2risHuo3KVqyaSV
K/T6KzMNvpq3ihA6ZjnuH9i5y5XWV//awWbFJDSuT0Bq+pwCQgHuthBjGmDl/nyc
r3MxJErDVYZBh4b140LQBU60MtiPERVYwme03ukGImPW/OKbEWYmnhHyMAf1ym/3
UK/lOv3G2w==
-----END CERTIFICATE-----
Subject emailAddress=ca@tauth.io, CN=Tauth Root CA, OU=CA Division, O=Tauth Labs Inc., L=New York, ST=New York, C=US
-----BEGIN CERTIFICATE-----
MIIDjzCCAxWgAwIBAgIUW6zwDse+LBk8Yvy/u3p7lSjnaA4wCgYIKoZIzj0EAwMw
gZcxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3
IFlvcmsxGDAWBgNVBAoMD1RhdXRoIExhYnMgSW5jLjEUMBIGA1UECwwLQ0EgRGl2
aXNpb24xFjAUBgNVBAMMDVRhdXRoIFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC2Nh
QHRhdXRoLmlvMB4XDTI2MDEwNTE2MDgzMVoXDTQ1MTIzMTE2MDgzMVowgZcxCzAJ
BgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsx
GDAWBgNVBAoMD1RhdXRoIExhYnMgSW5jLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24x
FjAUBgNVBAMMDVRhdXRoIFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC2NhQHRhdXRo
LmlvMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETgpV7Q9tsMzXFPduTyVt71IRRfU0
yeZGVuXmVO6zyUXxqGCg07jafnMyv24pRwJ9Bk+ROhslNWFS0NhXjIINDuk4h5Ji
KcEEGBJIlGl7jOB/MPLF/HU7CfURT2ayXKXmo4IBHjCCARowHQYDVR0OBBYEFB3R
AQfd1ulUQKBf+J34TnS1CxI+MIHXBgNVHSMEgc8wgcyAFB3RAQfd1ulUQKBf+J34
TnS1CxI+oYGdpIGaMIGXMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsx
ETAPBgNVBAcMCE5ldyBZb3JrMRgwFgYDVQQKDA9UYXV0aCBMYWJzIEluYy4xFDAS
BgNVBAsMC0NBIERpdmlzaW9uMRYwFAYDVQQDDA1UYXV0aCBSb290IENBMRowGAYJ
KoZIhvcNAQkBFgtjYUB0YXV0aC5pb4IUW6zwDse+LBk8Yvy/u3p7lSjnaA4wDwYD
VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIx
AJzhLaivtrjOMyEAuoHQRIhN2u7kw2iWZZyAT6FJH4+GBYudax0dfewXeIvC0MH5
SwIwAKojdigAO86Xvk6++B875ZpTjQIysPX0bGumbmAwcAmsqum9JLLIRCDz+7Kt
WrRP
-----END CERTIFICATE-----
Subject CN=HUAWEI C2PA ECC384 Root CA E346, O=Huawei Technologies Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIICoDCCAiagAwIBAgIIKsblXSNLwF8wCgYIKoZIzj0EAwMwXzELMAkGA1UEBhMC
Q04xJjAkBgNVBAoMHUh1YXdlaSBUZWNobm9sb2dpZXMgQ28uLCBMdGQuMSgwJgYD
VQQDDB9IVUFXRUkgQzJQQSBFQ0MzODQgUm9vdCBDQSBFMzQ2MB4XDTI2MDQxMzAx
NTQxMFoXDTQ2MDQwODAxNTQxMFowXzELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUh1
YXdlaSBUZWNobm9sb2dpZXMgQ28uLCBMdGQuMSgwJgYDVQQDDB9IVUFXRUkgQzJQ
QSBFQ0MzODQgUm9vdCBDQSBFMzQ2MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOrBo
SivfM2LRlG3B4r52hE0vDjQR2iWv9sKTZXzkscQIasoH+uYBDDie9VcosH4Mfq/U
xN8ojhW7BZW38LT38SmYpJPZirtzKTyCD4ha4dU8tLcHoJsOn3p+kBw09Bvoo4Gu
MIGrMB8GA1UdIwQYMBaAFM/OyEcEDscO78w/okatAmz35ypMMB0GA1UdDgQWBBTP
zshHBA7HDu/MP6JGrQJs9+cqTDASBgNVHRMBAf8ECDAGAQH/AgECMEUGCCsGAQUF
BwELBDkwNzA1BggrBgEFBQcwBYYpaHR0cDovL2NhLmh1YXdlaWNsb3VkLmNvbS9y
ZXBvc2l0b3J5Lmh0bWwwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUC
MDCsqpRvJIPw6OAQi2Rj0CWnwJgK75YK5gj+K9xomIDu8ai/2ExrifL8JgROQOeZ
egIxAL3tT3Dp84wP18ohYU83gkRcA8j9B3T4BRpxY1Ew9IJMYJ2zqz0weFjAEGNC
9Z36XQ==
-----END CERTIFICATE-----
Subject CN=Huanyu Trust C2PA EC-384 Root CA, O=Huanyu Trust Ltd, C=CN
-----BEGIN CERTIFICATE-----
MIICTTCCAdOgAwIBAgIUCqsLOXYNHdz0EBDsWKcrsiBcF10wCgYIKoZIzj0EAwMw
UzELMAkGA1UEBhMCQ04xGTAXBgNVBAoTEEh1YW55dSBUcnVzdCBMdGQxKTAnBgNV
BAMTIEh1YW55dSBUcnVzdCBDMlBBIEVDLTM4NCBSb290IENBMCAXDTI2MDQxMzA2
MDUwOVoYDzIwNTEwNDEzMDYwNTA5WjBTMQswCQYDVQQGEwJDTjEZMBcGA1UEChMQ
SHVhbnl1IFRydXN0IEx0ZDEpMCcGA1UEAxMgSHVhbnl1IFRydXN0IEMyUEEgRUMt
Mzg0IFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARDnAlELrIETK81oB6E
/ZfbkGSH9YPhGdFpXTzDCHD3IsKPQdPPv/mF319XjdrZMECxpH9rbmWGuWrhq6K8
zIGO27H2MUE658XNu6aO5xcZV/YdAMkG/YWPYo9CmCT4/SujZjBkMBIGA1UdEwEB
/wQIMAYBAf8CAQIwHwYDVR0jBBgwFoAUYc0ZfGc1yTPouBQYj81mEEdaefswHQYD
VR0OBBYEFGHNGXxnNckz6LgUGI/NZhBHWnn7MA4GA1UdDwEB/wQEAwIBBjAKBggq
hkjOPQQDAwNoADBlAjEAqfQxNSnmGxtt1QBKJbaY8AyUiaJxzX5MnH0l/ielnc6D
qBhIcALvKKc3a8MizR01AjB85bwUFBgs6WlIWZ6sFDDbCQzrWpPU11YQnj/mGom7
yXjL/2axHPWPY2Do/ldP5mc=
-----END CERTIFICATE-----
Subject CN=Snowball ECC P384 Root CA for C2PA G1, O=Snowball Technology Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIICiTCCAg6gAwIBAgIUVlsRGj1G9vJ4ws2jMYyFlFcKUokwCgYIKoZIzj0EAwMw
ZTELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28u
LCBMdGQuMS4wLAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBSb290IENBIGZvciBD
MlBBIEcxMB4XDTI2MDYxODA5MzkxOVoXDTQ2MDYxMzA5MzkxOVowZTELMAkGA1UE
BhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28uLCBMdGQuMS4w
LAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBSb290IENBIGZvciBDMlBBIEcxMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAELJUrcxeVmmYrb8Kl/YVAYYFiyuiqiIa6z/1n
FoCpB2JV/Qkwjdns7rOvLheWBi1s880NcwqD/KD9t9/SOgWTfD+WOAhtPioAql2Y
YUl0toOUzoXaAOB3+HIQRiKsL2RIo38wfTASBgNVHRMBAf8ECDAGAQH/AgECMA4G
A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU6R4F1vboIaKQly8jCAQX2lQhWPQwHwYD
VR0jBBgwFoAU6R4F1vboIaKQly8jCAQX2lQhWPQwFwYDVR0gBBAwDjAMBgorBgEE
AYPoXgEBMAoGCCqGSM49BAMDA2kAMGYCMQD6/xszsnZoYt+gYIsgYKTMFARxRpB5
N16s2u2hgkuFs4zyLco1z7gOk0mv6EbBkm0CMQCcsqIQnr45MVjDUdn9L//VdGDO
6H+P67vBdmvb3LiLTB+Ey/JYYfnHHIKzJrRG4ls=
-----END CERTIFICATE-----
Subject CN=Snowball ECC P384 TSA ICA for C2PA G1, O=Snowball Technology Co., Ltd., C=CN
-----BEGIN CERTIFICATE-----
MIIDjjCCAxSgAwIBAgIUVFsaHdwprWWlk08S4C67gq5M6qwwCgYIKoZIzj0EAwMw
ZTELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28u
LCBMdGQuMS4wLAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBSb290IENBIGZvciBD
MlBBIEcxMB4XDTI2MDYxODA5NDIxNFoXDTQxMDYxODA5NDIxNFowZTELMAkGA1UE
BhMCQ04xJjAkBgNVBAoMHVNub3diYWxsIFRlY2hub2xvZ3kgQ28uLCBMdGQuMS4w
LAYDVQQDDCVTbm93YmFsbCBFQ0MgUDM4NCBUU0EgSUNBIGZvciBDMlBBIEcxMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEsI3OjbQCcr6lFS0Y1T8vaS8KcApdpQUAAee7
EGSXvyuTfXcK9AUSYHxVZRiJjGliHgyVQL5C2tYEL4JOxw3ttxlvKVkuQI2MHd05
7yOyrgn1YC/tsw6dRJhrt+u+7xFso4IBgzCCAX8wEgYDVR0TAQH/BAgwBgEB/wIB
ADAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYE
FNBjlXQUhXxUL3xl/vB43xqIyE7NMB8GA1UdIwQYMBaAFOkeBdb26CGikJcvIwgE
F9pUIVj0MBcGA1UdIAQQMA4wDAYKKwYBBAGD6F4BATCBlAYIKwYBBQUHAQEEgYcw
gYQwLQYIKwYBBQUHMAGGIWh0dHA6Ly9vY3NwLmMycGEuc25vd2JhbGx0ZWNoLmNv
bTBTBggrBgEFBQcwAoZHaHR0cDovL2NhY2VydHMuYzJwYS5zbm93YmFsbHRlY2gu
Y29tL1Nub3diYWxsRUNDUDM4NFJvb3RDQWZvckMyUEFHMS5jcnQwVAYDVR0fBE0w
SzBJoEegRYZDaHR0cDovL2NybC5jMnBhLnNub3diYWxsdGVjaC5jb20vU25vd2Jh
bGxFQ0NQMzg0Um9vdENBZm9yQzJQQUcxLmNybDAKBggqhkjOPQQDAwNoADBlAjB0
7SyNJ2s9Mdz78HJwm5aU3J5NdZF8XbNhPcKB76QqrbNjtb2ehkqceoYx3wkotCsC
MQDSJY/v5rMUu9DeqNTNnHSvBVTXEzqZj+k47QlVnv9Auu29JHFXrY+Gj+b3Cx6G
ZcE=
-----END CERTIFICATE-----
Subject CN=Encypher C2PA Root CA 2026, OU=CA Division, O=Encypher Corp., L=San Francisco, ST=California, C=US
-----BEGIN CERTIFICATE-----
MIIDFDCCApqgAwIBAgIUS2zOIYRKaa59snU3NcNFadfTtoAwCgYIKoZIzj0EAwMw
gY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T
YW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwL
Q0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2
MB4XDTI2MDUyMjEzNDk1NloXDTQ2MDUxNzEzNDk1NlowgY4xCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcw
FQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xIzAh
BgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2MHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAE27XmRBc4mE0dYfUhLiuPiAnY3tRDMke9evylB+VgyMspT1svR9h3
KXxr8tml+1oM82US+pw2jJenfUxoUJGVDY/lvwl+4Q+7vawXFd26yPv/CQSnhI+w
05+yiSiwshIOo4G2MIGzMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQD
AgEGMB0GA1UdDgQWBBS3gC7ps3/HLQbb3uuqL4U8gDnKEDAfBgNVHSMEGDAWgBS3
gC7ps3/HLQbb3uuqL4U8gDnKEDBNBgNVHSAERjBEMEIGCisGAQQBg+heAQEwNDAy
BggrBgEFBQcCARYmaHR0cHM6Ly9jYS5lbmN5cGhlci5jb20vcmVwb3NpdG9yeS9j
cHMwCgYIKoZIzj0EAwMDaAAwZQIwYeb83YEb/Oh9uELuvO9jpb85orRWWQiceqf0
sCwxtJw07/giyy/hI1BFvbQZcfkLAjEAj8jfwuJSJ9LjeeYL/VXStQWDHewxbIou
IKdYpS9ZBidpsi+01tpQY1j7apLAXMHO
-----END CERTIFICATE-----
Subject CN=Encypher C2PA TSA Issuing CA 2026, OU=CA Division, O=Encypher Corp., L=San Francisco, ST=California, C=US
-----BEGIN CERTIFICATE-----
MIID3zCCA2WgAwIBAgIUfkelmX2xvvmLxUBI5ayBZXdGOz4wCgYIKoZIzj0EAwMw
gY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T
YW4gRnJhbmNpc2NvMRcwFQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwL
Q0EgRGl2aXNpb24xIzAhBgNVBAMMGkVuY3lwaGVyIEMyUEEgUm9vdCBDQSAyMDI2
MB4XDTI2MDUyNDE1NTUxOVoXDTMxMDUyNTE1NTUxOVowgZUxCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRcw
FQYDVQQKDA5FbmN5cGhlciBDb3JwLjEUMBIGA1UECwwLQ0EgRGl2aXNpb24xKjAo
BgNVBAMMIUVuY3lwaGVyIEMyUEEgVFNBIElzc3VpbmcgQ0EgMjAyNjB2MBAGByqG
SM49AgEGBSuBBAAiA2IABAPWSlEq6MdUceWdJ5nsZSwXTBq+enJovg6OTABtcAdS
uEDTcD1t9eajKVlPvK7xqbG18InxR0Ave8UA6jNTFD+4f/fjqR/HbJAtX2l7lcv5
lM9PiYJER64WF7Diyibcm6OCAXkwggF1MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD
VR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMB0GA1UdDgQWBBRACKf4
2ACoV/5pGlYPM5hBkBcWlTAfBgNVHSMEGDAWgBS3gC7ps3/HLQbb3uuqL4U8gDnK
EDA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY2EuZW5jeXBoZXIuY29tL2NybC9y
b290LWNhLmNybDByBggrBgEFBQcBAQRmMGQwJwYIKwYBBQUHMAGGG2h0dHA6Ly9j
YS5lbmN5cGhlci5jb20vb2NzcDA5BggrBgEFBQcwAoYtaHR0cDovL2NhLmVuY3lw
aGVyLmNvbS9yZXBvc2l0b3J5L3Jvb3QtY2EuY3J0ME0GA1UdIARGMEQwQgYKKwYB
BAGD6F4BATA0MDIGCCsGAQUFBwIBFiZodHRwczovL2NhLmVuY3lwaGVyLmNvbS9y
ZXBvc2l0b3J5L2NwczAKBggqhkjOPQQDAwNoADBlAjBXL0geKILQqoIKi9wD74Hr
id0nXiRZhyMrI39yGXDjq5/rqsQhCdOR+Gs4XIX9lhUCMQCb7yodaeskIdoHYjvV
y59amQPG8tMRAXIaybH/Df/Dwb2YD/D+hbgMn0MufkrB4/w=
-----END CERTIFICATE-----
Subject CN=TrustAsia C2PA RSA Root CA, O=TrustAsia Technologies, Inc., C=CN
-----BEGIN CERTIFICATE-----
MIIFqDCCA5CgAwIBAgIUNqYa1cdsY+cp091HGRPKXzrE4kwwDQYJKoZIhvcNAQEM
BQAwWTELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp
ZXMsIEluYy4xIzAhBgNVBAMMGlRydXN0QXNpYSBDMlBBIFJTQSBSb290IENBMCAX
DTI2MDYwNTAzNTMwMFoYDzIwNTEwNjA1MDM1MjU5WjBZMQswCQYDVQQGEwJDTjEl
MCMGA1UECgwcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEjMCEGA1UEAwwa
VHJ1c3RBc2lhIEMyUEEgUlNBIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQCvgJqlqXZ+SYbtl9dLK4SQ5Gt9KrPbOmWEeMYnh8UdB3+KzkrG
6PSZ1/s6KDIiEPNH1Sno7UOnTWzZ50Ce3Qq4PKFJBb9iJ0cbtw4qGOjHDhgXhozv
nPQhXy6uiEODKgsKRaZwxT7xer3ZOAbX/30tQDwyAavoEtb+F+o2faYxHwjojUnK
zO0Aaz68h7U5zMHgdMUvkqc0B5NIpoTixCj7Om2X0XROWAFx7ELrVxMmyL4PCWOj
caRXoP5QlDHiIc5Ck9/HD6VYr2jJG/8BFZqk9l+mgNlxjaIbp0D/B3D7WvxcGOFR
qiRJRni3LAsdVAMGPs/msUVBsYnTIEWb1o8qvkKpPOE0OtseEhWIQX4EDTA3Kim1
xVqcS3+9irfDf7Jda2vOX1mamZa+Y89xCZcG7ce4u0fpE/NOs46dymoG/zU0uUvd
KOtzvddNuDOWymROa6SVG6uETSdVFRsQfR/Dp8MQegVLB6ITAfnXHc3Pn67B1ncs
L359DcwlZkqYGegsz//8ToRAlSSA/9C9vzlXfbGIg2HHtToHcwa6BjDkRpfdm6nk
egfCvoaWT4k/9vDQNXvg8Wee1vpUDKhB7y1SSaSkm+7wr8fV4ovFGSrbygpr0BuG
m+ShU3Vh+8UXAICPntscPAgNXidB9A8Zgib5rJjiVi0sxizPYueAchPQDwIDAQAB
o2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E
FgQUCdViRLQz3hH0IboHIKjSrMMHRYAwHwYDVR0jBBgwFoAUCdViRLQz3hH0IboH
IKjSrMMHRYAwDQYJKoZIhvcNAQEMBQADggIBAHInmN938SAQOkERvIrZDi14uVmZ
P5GgqQ6ZH4BF77RFcQcDl7Id+3WBf1WqSl0zyuq6HulYyM6pWikh0W5K/NBBLlt+
zXtEHbW71jhAhCKALdbclT+KuqRRQbrzATXPyYB5Jb/7xlUIofilo70+lqNwahsy
Y5F+6XlOZ4JuA84RdfxGdyn/IJlOJ9mc2RbI6aAxMuFcsot483gOYtkU4Og0g9M7
XsdLT2VXwuVk3hXwc7UWlZUm1Vn6dGmrcSj41wGlw9JGMU87OfzYFseGBDGDc2Xv
njFCY+SBAEWGk4m6ugNGat8OKBVNpnYGp0iNODMtQXJ0aUCvOziFPJY9li08sUDe
HI9ZwLB+yADsaC1gaX4eloRFsIyyl7YRQEY1/EfVw3wnN+XBJYyBw9v1RtpXDh/v
O2ezzXc1sdH/WgXMGGrRdiCAZb5mFkjTEoKXVnkGMn7bnJKuFgDQPWerA24Jck1J
QTuQBXJOAt8FVaYMjL6wPSIv84UjVXIRkOP4ZOydpYgykEl5LfOWcQtA4U6h+obS
35aB8YE1KC9rIeEE8q5onGo6tRYJzh+mB/XSUP35fNbwe4FkI3I6ZpNSS6wt9HY0
DCYCNhAz7KP8RwexR/stlYyeUxbj3+jxK4AOGTZQEKBBWkoqfrPCDpybwpXCxxP5
qtj6+iKCvpx9jmvr
-----END CERTIFICATE-----
Subject CN=TrustAsia C2PA ECC Root CA, O=TrustAsia Technologies, Inc., C=CN
-----BEGIN CERTIFICATE-----
MIICWjCCAd+gAwIBAgIUdUWMu3G69tNb9bwz/0KTupxLm1kwCgYIKoZIzj0EAwMw
WTELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
IEluYy4xIzAhBgNVBAMMGlRydXN0QXNpYSBDMlBBIEVDQyBSb290IENBMCAXDTI2
MDYwNTAzNTAwMFoYDzIwNTEwNjA1MDM0OTU5WjBZMQswCQYDVQQGEwJDTjElMCMG
A1UECgwcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEjMCEGA1UEAwwaVHJ1
c3RBc2lhIEMyUEEgRUNDIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQL
ERY0d7ug9MiCqmXnDjDs2med9zfTqTWOcG3OD5Q6FSEgNUcjT5Qy7xzKn1MfqaY4
Ye3sf+Do7cCDR6AiFW6SqLQobfJ70jMDsSQYlrCCjMLvb+NdNPhED43s8fgehxyj
ZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQIwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW
BBT13DKCd7Jlz2rEe0vBlwo/CoR2mDAfBgNVHSMEGDAWgBT13DKCd7Jlz2rEe0vB
lwo/CoR2mDAKBggqhkjOPQQDAwNpADBmAjEAwp2Jie8k6/haTc/df3QsX+Ttg7Wp
ARtYTwJSP4Z+ggpf39sZ+7QPOE50BqBj9Q6zAjEAjZ+5glED9tav6r3jWuMOmKTB
kobBXzvjqTDLGG570BbSQfNmS3HRC6u5YTIpElpb
-----END CERTIFICATE-----
Subject CN=Castlabs C2PA ECC P-384 Root CA, OU=C2PA Trust Infrastructure, O=Castlabs, L=Berlin, ST=Berlin, C=DE
-----BEGIN CERTIFICATE-----
MIICyDCCAk2gAwIBAgIUN5SkK2Lb2Ts/j1AC3K8Y3eVydFQwCgYIKoZIzj0EAwMw
gZAxKDAmBgNVBAMMH0Nhc3RsYWJzIEMyUEEgRUNDIFAtMzg0IFJvb3QgQ0ExCzAJ
BgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xETAPBgNVBAoMCENhc3RsYWJzMSIw
IAYDVQQLDBlDMlBBIFRydXN0IEluZnJhc3RydWN0dXJlMQ8wDQYDVQQIDAZCZXJs
aW4wHhcNMjYwNzA4MDE0NDMwWhcNMzYwNzA1MDE0NDMwWjCBkDEoMCYGA1UEAwwf
Q2FzdGxhYnMgQzJQQSBFQ0MgUC0zODQgUm9vdCBDQTELMAkGA1UEBhMCREUxDzAN
BgNVBAcMBkJlcmxpbjERMA8GA1UECgwIQ2FzdGxhYnMxIjAgBgNVBAsMGUMyUEEg
VHJ1c3QgSW5mcmFzdHJ1Y3R1cmUxDzANBgNVBAgMBkJlcmxpbjB2MBAGByqGSM49
AgEGBSuBBAAiA2IABPXhd9L+pjQVnUXH6I7+oFSrd6JpO4A+CYDm/AuDZgJvwiuJ
lrM9dC3CAE7UAK8IcQEo85qzuak0TfLdPaJWD26+q3tJ/S7NW4sOKhH09KRN7iFq
XbsOe8FOxaQql/UrMqNmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB
Af8CAQIwHQYDVR0OBBYEFHNKe2PoAANQBj4/25eRv2VY4Fy7MB8GA1UdIwQYMBaA
FHNKe2PoAANQBj4/25eRv2VY4Fy7MAoGCCqGSM49BAMDA2kAMGYCMQDUlNJD49Ux
zwBccYHiz0wgmbIXBDdgpVrLfWTQnbi21BhfPBQ6j0HJ24FY36L5oj8CMQCv5+bK
gBIkJq9USBx2FjjDkukYjQMI/WDD6SH6QGTdotsH3cQQ6bjr9C+0ruxxx08=
-----END CERTIFICATE-----
+5
View File
@@ -0,0 +1,5 @@
The PEM files in this directory are snapshots of the official C2PA Conformance
Program trust lists from https://github.com/c2pa-org/conformance-public.
They are bundled so credential trust validation remains local and reproducible.
Refresh both files before a release and record the update in the release notes.
+40
View File
@@ -0,0 +1,40 @@
import { cp, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
const root = new URL('../', import.meta.url);
const outdir = new URL('../dist/', import.meta.url);
const rootPath = fileURLToPath(root);
const outdirPath = fileURLToPath(outdir);
await mkdir(outdir, { recursive: true });
await cp(new URL('../public/', import.meta.url), outdir, { recursive: true });
await cp(
new URL('../node_modules/@contentauth/c2pa-web/dist/resources/c2pa_bg.wasm', import.meta.url),
new URL('../dist/c2pa_bg.wasm', import.meta.url)
);
await cp(
new URL('../node_modules/@contentauth/c2pa-web/dist/c2pa_worker.js', import.meta.url),
new URL('../dist/c2pa_worker.js', import.meta.url)
);
await build({
absWorkingDir: rootPath,
entryPoints: {
background: 'src/background/index.ts',
content: 'src/content/index.ts',
popup: 'src/popup/index.tsx',
options: 'src/options/index.tsx'
},
outdir: outdirPath,
bundle: true,
format: 'iife',
platform: 'browser',
target: ['firefox128'],
jsx: 'automatic',
jsxImportSource: 'preact',
sourcemap: true,
minify: true,
legalComments: 'eof',
logLevel: 'info'
});
+6
View File
@@ -0,0 +1,6 @@
import { rm } from 'node:fs/promises';
await Promise.all([
rm(new URL('../dist', import.meta.url), { recursive: true, force: true }),
rm(new URL('../artifacts', import.meta.url), { recursive: true, force: true })
]);
+219
View File
@@ -0,0 +1,219 @@
import { createC2pa } from '@contentauth/c2pa-web';
import type { Evidence } from '../shared/types';
const AI_SOURCE_TYPES = [
'trainedalgorithmicmedia',
'compositewithtrainedalgorithmicmedia',
'algorithmicallyenhanced',
'algorithmicmedia'
];
interface RecordLike {
[key: string]: unknown;
}
let c2paPromise: ReturnType<typeof createC2pa> | undefined;
/**
* c2pa-web 0.14.4 rejects every explicit worker URL whose protocol is not
* `https:`, including Firefox's same-extension `moz-extension:` URLs. The
* package only reads `protocol` and `toString()` before constructing Worker,
* so this adapter lets its HTTPS-only guard validate while Worker still
* receives the real, CSP-approved packaged URL. Keep this isolated until the
* upstream SDK accepts browser-extension schemes directly.
*/
export class FirefoxExtensionWorkerUrl extends URL {
readonly #packagedUrl: string;
constructor(packagedUrl: string) {
super('https://slopdetect.invalid/c2pa_worker.js');
if (!packagedUrl.startsWith('moz-extension:')) {
throw new Error('C2PA worker must be a packaged Firefox extension resource.');
}
this.#packagedUrl = packagedUrl;
}
override toString(): string {
return this.#packagedUrl;
}
}
async function initialiseC2pa(): ReturnType<typeof createC2pa> {
const [trustList, timestampTrustList] = await Promise.all([
fetch(browser.runtime.getURL('trust/C2PA-TRUST-LIST.pem')).then((response) => response.text()),
fetch(browser.runtime.getURL('trust/C2PA-TSA-TRUST-LIST.pem')).then((response) => response.text())
]);
return createC2pa({
wasmSrc: browser.runtime.getURL('c2pa_bg.wasm'),
workerSrc: new FirefoxExtensionWorkerUrl(browser.runtime.getURL('c2pa_worker.js')),
settings: {
trust: { trustAnchors: `${trustList}\n${timestampTrustList}` },
verify: {
verifyTrust: true,
verifyAfterReading: true,
verifyTimestampTrust: true,
ocspFetch: false,
remoteManifestFetch: false
}
}
});
}
function getC2pa(): ReturnType<typeof createC2pa> {
c2paPromise ??= initialiseC2pa();
return c2paPromise;
}
function isRecord(value: unknown): value is RecordLike {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function walk(value: unknown, visit: (key: string, value: unknown) => void): void {
if (Array.isArray(value)) {
value.forEach((item) => walk(item, visit));
return;
}
if (!isRecord(value)) return;
for (const [key, child] of Object.entries(value)) {
visit(key, child);
walk(child, visit);
}
}
function collectStrings(value: unknown): string[] {
const strings: string[] = [];
walk(value, (_key, child) => {
if (typeof child === 'string') strings.push(child);
});
return strings;
}
function collectValidationCodes(value: unknown): string[] {
const codes = new Set<string>();
walk(value, (key, child) => {
if (/^(code|status|validation_status|validationStatus)$/i.test(key) && typeof child === 'string') {
codes.add(child);
}
if (/validation_status|validationStatus/i.test(key) && Array.isArray(child)) {
for (const entry of child) {
if (typeof entry === 'string') codes.add(entry);
else if (isRecord(entry) && typeof entry.code === 'string') codes.add(entry.code);
}
}
});
return [...codes];
}
function findFirstString(value: unknown, matchingKeys: RegExp): string | undefined {
let found: string | undefined;
walk(value, (key, child) => {
if (!found && matchingKeys.test(key) && typeof child === 'string') found = child;
});
return found;
}
function hasTrustedCredential(value: unknown): boolean {
let trusted = false;
walk(value, (key, child) => {
if (/trusted/i.test(key) && child === true) trusted = true;
if (/validation_state|validationState/i.test(key) && child === 'Trusted') trusted = true;
});
return trusted;
}
function hasInvalidValidationState(value: unknown): boolean {
let invalid = false;
walk(value, (key, child) => {
if (/validation_state|validationState/i.test(key) && child === 'Invalid') invalid = true;
});
return invalid;
}
function containsManifest(value: unknown): boolean {
if (!isRecord(value)) return false;
return Boolean(value.active_manifest || value.activeManifest || value.manifests || value.claims);
}
function isInvalidStatus(code: string): boolean {
return /invalid|mismatch|malformed|error|failure|revoked|expired/i.test(code);
}
function serialisable(value: unknown): unknown {
try {
return JSON.parse(JSON.stringify(value, (_key, item) => (typeof item === 'bigint' ? item.toString() : item)));
} catch {
return undefined;
}
}
function isMissingManifestError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /manifest.*(?:not found|absent)|jumbf.*not found|no claim|unsupported.*(?:mime|format)/i.test(message);
}
export async function inspectC2pa(blob: Blob, includeRaw: boolean): Promise<Evidence[]> {
let reader: Awaited<ReturnType<Awaited<ReturnType<typeof createC2pa>>['reader']['fromBlob']>> | undefined;
try {
const c2pa = await getC2pa();
reader = await c2pa.reader.fromBlob(blob.type || 'application/octet-stream', blob);
if (!reader) return [];
const store: unknown = await reader.manifestStore();
return evidenceFromManifestStore(store, includeRaw);
} catch (error) {
if (isMissingManifestError(error)) return [];
const message = error instanceof Error ? error.message : String(error);
return [{
id: crypto.randomUUID(),
kind: 'limitation',
strength: 'none',
verdict: 'unknown',
title: 'C2PA inspection failed',
detail: message.slice(0, 300),
detector: '@contentauth/c2pa-web'
}];
} finally {
await reader?.free();
}
}
export function evidenceFromManifestStore(store: unknown, includeRaw: boolean): Evidence[] {
if (!containsManifest(store)) return [];
const strings = collectStrings(store);
const aiGenerated = strings.some((value) => {
const normalised = value.toLowerCase().replace(/[^a-z]/g, '');
return AI_SOURCE_TYPES.some((type) => normalised.includes(type));
});
const statuses = collectValidationCodes(store);
const invalid = hasInvalidValidationState(store) || statuses.some(isInvalidStatus);
const trusted = hasTrustedCredential(store);
const issuer = findFirstString(store, /issuer|common_name|organization|signer/i);
if (invalid) {
return [{
id: crypto.randomUUID(),
kind: 'c2pa',
strength: 'invalid',
verdict: 'invalid',
title: 'Invalid Content Credential',
detail: `C2PA provenance was present but validation reported: ${statuses.join(', ') || 'an integrity error'}.`,
detector: '@contentauth/c2pa-web',
issuer,
raw: includeRaw ? serialisable(store) : undefined
}];
}
return [{
id: crypto.randomUUID(),
kind: 'c2pa',
strength: trusted ? 'verified' : 'detected',
verdict: aiGenerated ? 'ai' : 'not-ai-asserted',
title: aiGenerated ? 'AI provenance credential' : 'Content Credential',
detail: aiGenerated
? `A ${trusted ? 'trusted and valid' : 'readable but not trusted'} C2PA claim identifies AI generation or manipulation.`
: `A ${trusted ? 'trusted and valid' : 'readable but not trusted'} C2PA claim was found without an AI-generation assertion.`,
detector: '@contentauth/c2pa-web',
issuer,
raw: includeRaw ? serialisable(store) : undefined
}];
}
+31
View File
@@ -0,0 +1,31 @@
interface CacheEntry<T> {
value: T;
touchedAt: number;
}
export class ResultCache<T> {
readonly #entries = new Map<string, CacheEntry<T>>();
#clock = 0;
public constructor(private readonly maximumEntries = 200) {}
public get(key: string): T | undefined {
const entry = this.#entries.get(key);
if (!entry) return undefined;
entry.touchedAt = ++this.#clock;
return entry.value;
}
public set(key: string, value: T): void {
this.#entries.set(key, { value, touchedAt: ++this.#clock });
if (this.#entries.size <= this.maximumEntries) return;
const oldest = [...this.#entries.entries()].sort((left, right) => left[1].touchedAt - right[1].touchedAt)[0];
if (oldest) this.#entries.delete(oldest[0]);
}
}
export async function hashBlob(blob: Blob): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', await blob.arrayBuffer());
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
}
+38
View File
@@ -0,0 +1,38 @@
import type { Evidence, ImageCandidate, TextCandidate } from '../shared/types';
export interface ImageWatermarkDetector {
readonly id: string;
readonly name: string;
supports(mimeType: string): boolean;
detect(blob: Blob, candidate: ImageCandidate): Promise<Evidence[]>;
}
export interface TextWatermarkDetector {
readonly id: string;
readonly name: string;
readonly minimumTokens: number;
supports(language?: string): boolean;
detect(candidate: TextCandidate): Promise<Evidence[]>;
}
// Watermark schemes currently require a provider-specific public SDK or API.
// Adapters are deliberately empty until a detector can be tested end-to-end.
export const imageWatermarkDetectors: readonly ImageWatermarkDetector[] = [];
export const textWatermarkDetectors: readonly TextWatermarkDetector[] = [];
export async function runImageWatermarkDetectors(
blob: Blob,
candidate: ImageCandidate
): Promise<Evidence[]> {
const compatible = imageWatermarkDetectors.filter((detector) => detector.supports(blob.type));
const results = await Promise.allSettled(compatible.map((detector) => detector.detect(blob, candidate)));
return results.flatMap((result) => (result.status === 'fulfilled' ? result.value : []));
}
export async function runTextWatermarkDetectors(candidate: TextCandidate): Promise<Evidence[]> {
const compatible = textWatermarkDetectors.filter(
(detector) => candidate.tokenEstimate >= detector.minimumTokens && detector.supports()
);
const results = await Promise.allSettled(compatible.map((detector) => detector.detect(candidate)));
return results.flatMap((result) => (result.status === 'fulfilled' ? result.value : []));
}
+48
View File
@@ -0,0 +1,48 @@
import type { Evidence, PageDisclosureCandidate } from '../shared/types';
const AI_DISCLOSURE = /\b(ai[- ]generated|generated (?:or manipulated )?by ai|artificially generated|created with (?:generative )?ai|synthetic media|trainedalgorithmicmedia)\b/i;
const AI_GENERATOR = /\b(chatgpt|openai|claude|anthropic|gemini|midjourney|dall[ -]?e|stable diffusion|firefly|copilot|framer ai|wix ai)\b/i;
function evidenceId(prefix: string, value: string): string {
let hash = 0;
for (const character of value) hash = (hash * 31 + character.charCodeAt(0)) | 0;
return `${prefix}-${Math.abs(hash)}`;
}
export function analyseDisclosures(candidates: PageDisclosureCandidate[]): Evidence[] {
const evidence: Evidence[] = [];
const seen = new Set<string>();
for (const candidate of candidates) {
const value = candidate.value.trim().slice(0, 500);
if (!value) continue;
const identity = `${candidate.source}:${value}`.toLowerCase();
if (seen.has(identity)) continue;
seen.add(identity);
if (AI_DISCLOSURE.test(value) || /^(true|yes|1)$/i.test(value) && /ai/i.test(candidate.source)) {
evidence.push({
id: evidenceId('disclosure', identity),
kind: 'disclosure',
strength: 'disclosed',
verdict: 'ai',
title: 'Publisher AI disclosure',
detail: `${candidate.source}: ${value}`
});
continue;
}
if (candidate.source.toLowerCase().includes('generator') && AI_GENERATOR.test(value)) {
evidence.push({
id: evidenceId('generator', identity),
kind: 'generator',
strength: 'clue',
verdict: 'unknown',
title: 'AI-related generator metadata',
detail: `${candidate.source}: ${value}. This is a tooling clue, not proof that the page content was AI-generated.`
});
}
}
return evidence;
}
+23
View File
@@ -0,0 +1,23 @@
import type { Evidence, HeuristicFinding } from '../shared/types';
export function evidenceFromHeuristics(findings: HeuristicFinding[]): Evidence[] {
return findings.map((finding) => ({
id: `heuristic:${finding.ruleId}`,
kind: 'heuristic',
strength: 'clue',
verdict: 'unknown',
title: finding.title,
detail: `${finding.detail}${finding.sample ? ` Example: ${finding.sample}` : ''}`,
detector: 'SlopDetect deterministic page heuristics',
category: finding.category,
score: finding.score,
occurrences: finding.occurrences
}));
}
export function heuristicScore(evidence: Evidence[]): number {
const heuristicEvidence = evidence.filter((item) => item.kind === 'heuristic');
const base = heuristicEvidence.reduce((sum, item) => sum + (item.score || 0), 0);
const categories = new Set(heuristicEvidence.map((item) => item.category).filter(Boolean));
return base + Math.max(0, categories.size - 1);
}
+42
View File
@@ -0,0 +1,42 @@
import type { Evidence } from '../shared/types';
const MAX_METADATA_SCAN_BYTES = 2 * 1024 * 1024;
interface GeneratorSignature {
name: string;
pattern: RegExp;
}
const SIGNATURES: GeneratorSignature[] = [
{ name: 'OpenAI / DALL·E', pattern: /\b(?:openai|dall[ -]?e)\b/i },
{ name: 'Midjourney', pattern: /\bmidjourney\b/i },
{ name: 'Stable Diffusion', pattern: /\bstable[ _-]?diffusion\b/i },
{ name: 'AUTOMATIC1111', pattern: /\bautomatic1111\b/i },
{ name: 'ComfyUI', pattern: /\bcomfyui\b|"(?:ckpt_name|sampler_name|positive)"\s*:/i },
{ name: 'InvokeAI', pattern: /\binvokeai\b/i },
{ name: 'Fooocus', pattern: /\bfooocus\b/i },
{ name: 'NovelAI', pattern: /\bnovelai\b/i },
{ name: 'Stable Diffusion parameters', pattern: /\bsteps:\s*\d+[^\0]{0,300}\bsampler:\s*[^,\0]+[^\0]{0,300}\bcfg scale:\s*[\d.]+/i }
];
function evidenceId(name: string): string {
return `image-generator:${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
}
export async function inspectImageGeneratorMetadata(blob: Blob): Promise<Evidence[]> {
const head = await blob.slice(0, Math.min(blob.size, MAX_METADATA_SCAN_BYTES)).arrayBuffer();
const text = new TextDecoder('latin1').decode(head);
const found = SIGNATURES.filter((signature) => signature.pattern.test(text));
if (found.length === 0) return [];
const names = [...new Set(found.map((signature) => signature.name))];
return [{
id: evidenceId(names.join('-')),
kind: 'generator',
strength: 'detected',
verdict: 'ai',
title: 'AI generator metadata detected',
detail: `The image bytes contain metadata associated with ${names.join(', ')}. This survives some exports, but it is unsigned and can be removed or forged.`,
detector: 'SlopDetect image metadata signatures'
}];
}
+55
View File
@@ -0,0 +1,55 @@
import browser from 'webextension-polyfill';
import { scanPage } from './scanner';
import type { RuntimeRequest, RuntimeResponse, ScanResult } from '../shared/types';
Object.assign(globalThis, { browser });
const tabResults = new Map<number, ScanResult>();
browser.runtime.onMessage.addListener((message: unknown, sender: browser.Runtime.MessageSender) => {
const request = message as RuntimeRequest;
if (!request || typeof request !== 'object' || typeof request.type !== 'string') return undefined;
if (request.type === 'analyse-page') {
return scanPage(request.page, request.mode)
.then((result): RuntimeResponse<ScanResult> => {
if (sender.tab?.id !== undefined) {
tabResults.set(sender.tab.id, result);
const badge = ['verified-ai', 'provenance-detected', 'watermark-detected', 'publisher-disclosed'].includes(result.summary.status) ? 'AI'
: result.summary.status === 'invalid-credential' ? '!'
: result.summary.status === 'mixed-provenance' ? '≋'
: result.summary.status === 'heuristic-suspected' ? 'S'
: '';
const color = ['verified-ai', 'watermark-detected'].includes(result.summary.status) ? '#6847ee'
: result.summary.status === 'invalid-credential' ? '#bd2535'
: result.summary.status === 'heuristic-suspected' ? '#b05d16'
: result.summary.status === 'mixed-provenance' ? '#4761a8' : '#956100';
void browser.action.setBadgeText({ tabId: sender.tab.id, text: badge });
void browser.action.setBadgeBackgroundColor({ tabId: sender.tab.id, color });
}
return { ok: true, data: result };
})
.catch((error): RuntimeResponse => ({
ok: false,
error: error instanceof Error ? error.message : String(error)
}));
}
if (request.type === 'get-tab-result') {
return Promise.resolve({ ok: true, data: tabResults.get(request.tabId) } satisfies RuntimeResponse<ScanResult | undefined>);
}
if (request.type === 'clear-tab-result') {
tabResults.delete(request.tabId);
return Promise.resolve({ ok: true } satisfies RuntimeResponse);
}
return undefined;
});
browser.tabs.onRemoved.addListener((tabId) => tabResults.delete(tabId));
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status !== 'loading' && changeInfo.url === undefined) return;
tabResults.delete(tabId);
void browser.action.setBadgeText({ tabId, text: '' });
});
+165
View File
@@ -0,0 +1,165 @@
import { inspectC2pa } from './c2pa';
import { ResultCache, hashBlob } from './cache';
import { analyseDisclosures } from './disclosures';
import { evidenceFromHeuristics } from './heuristics';
import { inspectImageGeneratorMetadata } from './image-metadata';
import { runImageWatermarkDetectors, runTextWatermarkDetectors, textWatermarkDetectors } from './detectors';
import { aggregateSummary, createAssetResult } from '../shared/status';
import {
DEFAULT_SETTINGS,
type AssetResult,
type Evidence,
type ExtensionSettings,
type ImageCandidate,
type PageCandidate,
type ScanResult,
type TextCandidate
} from '../shared/types';
const imageCache = new ResultCache<Evidence[]>(200);
const AUTO_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
async function loadSettings(): Promise<ExtensionSettings> {
const saved = await browser.storage.local.get('settings');
return { ...DEFAULT_SETTINGS, ...(saved.settings as Partial<ExtensionSettings> | undefined) };
}
function isPrivateHost(hostname: string): boolean {
const host = hostname.toLowerCase().replace(/^\[|\]$/g, '');
if (host === 'localhost' || host === '::1' || host.endsWith('.local')) return true;
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host)) return true;
const private172 = /^172\.(\d{1,2})\./.exec(host);
return private172 ? Number(private172[1]) >= 16 && Number(private172[1]) <= 31 : false;
}
function safeAssetUrl(value: string, pageUrl: string): URL | undefined {
try {
const url = new URL(value);
if (!['http:', 'https:', 'data:'].includes(url.protocol) || url.username || url.password) return undefined;
if (url.protocol === 'data:') return url;
const page = new URL(pageUrl);
if (isPrivateHost(url.hostname) && url.origin !== page.origin) return undefined;
return url;
} catch {
return undefined;
}
}
async function fetchImage(candidate: ImageCandidate, maximumBytes: number, pageUrl: string): Promise<Blob> {
const url = safeAssetUrl(candidate.url, pageUrl);
if (!url) throw new Error('Unsupported or unsafe image URL scheme.');
const response = await fetch(url.href, { credentials: 'omit', cache: 'force-cache', redirect: 'error' });
if (!response.ok) throw new Error(`Image request returned HTTP ${response.status}.`);
const declaredLength = Number(response.headers.get('content-length') || 0);
if (declaredLength > maximumBytes) throw new Error(`Image exceeds the ${Math.round(maximumBytes / 1048576)} MB scan limit.`);
const blob = await response.blob();
if (blob.size > maximumBytes) throw new Error(`Image exceeds the ${Math.round(maximumBytes / 1048576)} MB scan limit.`);
if (blob.type && !blob.type.startsWith('image/')) throw new Error(`Resource is ${blob.type}, not an image.`);
return blob;
}
async function scanImage(
candidate: ImageCandidate,
settings: ExtensionSettings,
pageUrl: string
): Promise<AssetResult> {
try {
const blob = await fetchImage(candidate, settings.maxImageBytes, pageUrl);
const cacheKey = `${await hashBlob(blob)}:${settings.includeRawEvidence}`;
let evidence = imageCache.get(cacheKey);
if (!evidence) {
evidence = [
...(await inspectC2pa(blob, settings.includeRawEvidence)),
...(await inspectImageGeneratorMetadata(blob)),
...(await runImageWatermarkDetectors(blob, candidate))
];
imageCache.set(cacheKey, evidence);
}
return createAssetResult(candidate.id, 'image', candidate.alt || 'Image', evidence, candidate.url);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
return createAssetResult(candidate.id, 'image', candidate.alt || 'Image', [{
id: crypto.randomUUID(),
kind: 'limitation',
strength: 'none',
verdict: 'unknown',
title: 'Image could not be inspected',
detail
}], candidate.url);
}
}
async function scanText(candidate: TextCandidate): Promise<AssetResult> {
const evidence = await runTextWatermarkDetectors(candidate);
if (evidence.length === 0 && candidate.tokenEstimate >= 200 && textWatermarkDetectors.length === 0) {
evidence.push({
id: crypto.randomUUID(),
kind: 'limitation',
strength: 'none',
verdict: 'unknown',
title: 'No compatible text-watermark detector',
detail: 'The text was collected, but no public interoperable detector is bundled for its watermark scheme.'
});
}
return createAssetResult(candidate.id, 'text', `Text sample (${candidate.tokenEstimate} tokens)`, evidence);
}
async function mapWithConcurrency<T, R>(
items: T[],
concurrency: number,
mapper: (item: T) => Promise<R>
): Promise<R[]> {
const results = new Array<R>(items.length);
let cursor = 0;
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
while (cursor < items.length) {
const index = cursor++;
const item = items[index];
if (item !== undefined) results[index] = await mapper(item);
}
});
await Promise.all(workers);
return results;
}
export async function scanPage(page: PageCandidate, mode: 'auto' | 'manual' = 'manual'): Promise<ScanResult> {
const settings = await loadSettings();
const imageLimit = mode === 'auto'
? Math.min(settings.maxImagesPerPage, settings.maxAutoImagesPerPage)
: settings.maxImagesPerPage;
const imageSettings = mode === 'auto'
? { ...settings, maxImageBytes: Math.min(settings.maxImageBytes, AUTO_IMAGE_MAX_BYTES) }
: settings;
const selectedImages = [...page.images]
.sort((left, right) => right.width * right.height - left.width * left.height)
.slice(0, imageLimit);
const pageEvidence = [
...analyseDisclosures(page.disclosures),
...(settings.enableHeuristicDetection ? evidenceFromHeuristics(page.heuristics || []) : [])
];
const pageResult = createAssetResult('page', 'page', page.title || page.url, pageEvidence, page.url);
const [images, texts] = await Promise.all([
mapWithConcurrency(selectedImages, 3, (candidate) => scanImage(candidate, imageSettings, page.url)),
mapWithConcurrency(page.texts, 2, scanText)
]);
const warnings: string[] = [];
if (page.images.length > selectedImages.length) {
warnings.push(`${page.images.length - selectedImages.length} images were skipped because of the ${mode} scan limit.`);
}
if (textWatermarkDetectors.length === 0 && page.texts.some((text) => text.tokenEstimate >= 200)) {
warnings.push('Text was not classified: no compatible public watermark detector is bundled.');
}
return {
scanId: crypto.randomUUID(),
pageUrl: page.url,
pageTitle: page.title,
scannedAt: new Date().toISOString(),
summary: aggregateSummary(pageResult, images, texts, page.images.length, page.texts.length, settings.heuristicThreshold),
page: pageResult,
images,
texts,
warnings
};
}
+75
View File
@@ -0,0 +1,75 @@
import type { AssetResult, ScanResult } from '../shared/types';
import { IMAGE_ID_ATTRIBUTE } from './discovery';
const HOST_ID = 'slopdetect-badge-layer';
let refreshListener: (() => void) | undefined;
function badgePresentation(result: AssetResult): { text: string; className: string; title: string } | undefined {
if (result.verdict === 'invalid') return { text: '!', className: 'invalid', title: 'Invalid provenance credential' };
if (result.verdict === 'ai' && result.strength === 'verified') {
return { text: 'AI', className: 'ai', title: 'Verified AI provenance' };
}
if (result.verdict === 'ai') return { text: 'AI', className: 'detected', title: 'AI origin detected or disclosed' };
if (result.strength === 'verified' || result.strength === 'detected') {
return { text: 'CC', className: 'credential', title: 'Content Credential found' };
}
return undefined;
}
export function clearBadges(): void {
document.getElementById(HOST_ID)?.remove();
if (refreshListener) {
window.removeEventListener('scroll', refreshListener, true);
window.removeEventListener('resize', refreshListener);
refreshListener = undefined;
}
}
export function renderBadges(result: ScanResult): void {
clearBadges();
const marked = result.images
.map((image) => ({ image, presentation: badgePresentation(image) }))
.filter((entry) => entry.presentation !== undefined);
if (marked.length === 0) return;
const host = document.createElement('div');
host.id = HOST_ID;
host.style.cssText = 'all:initial;position:fixed;inset:0;z-index:2147483647;pointer-events:none;';
const root = host.attachShadow({ mode: 'closed' });
const style = document.createElement('style');
style.textContent = `
.badge { position: fixed; min-width: 24px; height: 24px; padding: 0 6px; box-sizing: border-box;
display: grid; place-items: center; color: white; border: 2px solid white; border-radius: 999px;
font: 700 11px/1 system-ui, sans-serif; box-shadow: 0 2px 8px #0008; }
.ai { background: #6847ee; } .detected { background: #9b6500; }
.credential { background: #18735c; } .invalid { background: #bd2535; }
`;
root.append(style);
const placements: Array<{ badge: HTMLDivElement; target: Element }> = [];
for (const entry of marked) {
const target = document.querySelector(`[${IMAGE_ID_ATTRIBUTE}="${window.CSS.escape(entry.image.id)}"]`);
if (!target || !entry.presentation) continue;
const badge = document.createElement('div');
badge.className = `badge ${entry.presentation.className}`;
badge.textContent = entry.presentation.text;
badge.title = entry.presentation.title;
badge.setAttribute('aria-label', entry.presentation.title);
root.append(badge);
placements.push({ badge, target });
}
const position = (): void => {
for (const placement of placements) {
const rect = placement.target.getBoundingClientRect();
placement.badge.style.left = `${Math.max(2, rect.right - 28)}px`;
placement.badge.style.top = `${Math.max(2, rect.top + 4)}px`;
placement.badge.style.display = rect.bottom < 0 || rect.top > window.innerHeight ? 'none' : 'grid';
}
};
position();
refreshListener = position;
window.addEventListener('scroll', position, true);
window.addEventListener('resize', position);
document.documentElement.append(host);
}
+154
View File
@@ -0,0 +1,154 @@
import type {
ImageCandidate,
PageCandidate,
PageDisclosureCandidate,
TextCandidate
} from '../shared/types';
import { discoverHeuristics } from './heuristics';
const IMAGE_ID_ATTRIBUTE = 'data-slopdetect-image-id';
const MAX_TEXT_BLOCKS = 8;
const MIN_TEXT_CHARACTERS = 300;
function isVisible(element: Element): boolean {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
}
function makeImageCandidate(element: Element, url: string, alt = ''): ImageCandidate | undefined {
if (!url || !isVisible(element)) return undefined;
const rect = element.getBoundingClientRect();
if (rect.width < 16 || rect.height < 16) return undefined;
const existingId = element.getAttribute(IMAGE_ID_ATTRIBUTE);
const id = existingId || crypto.randomUUID();
element.setAttribute(IMAGE_ID_ATTRIBUTE, id);
return {
id,
url: new URL(url, document.baseURI).href,
alt: alt.trim().slice(0, 200),
width: Math.round(rect.width),
height: Math.round(rect.height)
};
}
function discoverImages(): ImageCandidate[] {
const candidates: ImageCandidate[] = [];
const seenUrls = new Set<string>();
for (const image of document.querySelectorAll('img')) {
const candidate = makeImageCandidate(image, image.currentSrc || image.src, image.alt);
if (candidate && !seenUrls.has(candidate.url)) {
candidates.push(candidate);
seenUrls.add(candidate.url);
}
}
for (const image of document.querySelectorAll('svg image')) {
const href = image.getAttribute('href') || image.getAttribute('xlink:href') || '';
const candidate = makeImageCandidate(image, href, image.getAttribute('aria-label') || 'SVG image');
if (candidate && !seenUrls.has(candidate.url)) {
candidates.push(candidate);
seenUrls.add(candidate.url);
}
}
const elements = [...document.querySelectorAll('body *')].slice(0, 2000);
for (const element of elements) {
const background = window.getComputedStyle(element).backgroundImage;
const match = /^url\(["']?(.*?)["']?\)$/.exec(background);
if (!match?.[1]) continue;
const candidate = makeImageCandidate(element, match[1], element.getAttribute('aria-label') || 'Background image');
if (candidate && !seenUrls.has(candidate.url)) {
candidates.push(candidate);
seenUrls.add(candidate.url);
}
}
return candidates;
}
export function estimateTokens(text: string): number {
return text.trim() ? Math.ceil(text.trim().split(/\s+/).length * 1.3) : 0;
}
function discoverText(): TextCandidate[] {
const selectors = 'article p, main p, [role="main"] p, article li, main li, blockquote';
const values = [...document.querySelectorAll<HTMLElement>(selectors)]
.filter(isVisible)
.map((element) => element.innerText.trim())
.filter((text) => text.length >= MIN_TEXT_CHARACTERS);
const unique = [...new Set(values)].sort((left, right) => right.length - left.length).slice(0, MAX_TEXT_BLOCKS);
if (unique.length === 0) {
const root = document.querySelector<HTMLElement>('article, main, [role="main"]') || document.body;
const combined = root?.innerText.trim().slice(0, 24_000) || '';
if (combined.length >= MIN_TEXT_CHARACTERS) unique.push(combined);
}
return unique.map((text) => ({ id: crypto.randomUUID(), text, tokenEstimate: estimateTokens(text) }));
}
function pushDisclosure(
output: PageDisclosureCandidate[],
source: string,
value: string | null | undefined
): void {
const trimmed = value?.trim();
if (trimmed) output.push({ source, value: trimmed.slice(0, 2000) });
}
function discoverDisclosures(): PageDisclosureCandidate[] {
const candidates: PageDisclosureCandidate[] = [];
const interestingMeta = /ai|artificial|synthetic|provenance|credential|generator|content-origin/i;
for (const meta of document.querySelectorAll<HTMLMetaElement>('meta[name], meta[property]')) {
const name = meta.name || meta.getAttribute('property') || '';
if (interestingMeta.test(name)) {
pushDisclosure(candidates, `meta:${name}`, meta.content);
}
}
for (const link of document.querySelectorAll<HTMLLinkElement>('link[rel]')) {
if (/credential|provenance|c2pa|manifest/i.test(link.rel)) {
pushDisclosure(candidates, `link:${link.rel}`, link.href);
}
}
for (const script of document.querySelectorAll<HTMLScriptElement>('script[type="application/ld+json"]')) {
if (/"(?:aiGenerated|isAIGenerated|contentOrigin|digitalSourceType)"\s*:\s*(?:true|"[^"]*(?:trainedAlgorithmicMedia|synthetic|AI)[^"]*")/i.test(script.textContent || '')) {
pushDisclosure(candidates, 'structured-data', script.textContent);
}
}
const labelSelectors = 'figcaption, small, [class*="disclosure" i], [class*="provenance" i], [class*="credit" i], [aria-label*="AI-generated" i]';
for (const element of document.querySelectorAll<HTMLElement>(labelSelectors)) {
const value = (element.innerText || element.getAttribute('aria-label') || '').trim();
if (value.length <= 300 && /\b(?:AI[- ]generated|generated by AI|artificially generated|created with AI|synthetic media)\b/i.test(value)) {
pushDisclosure(candidates, 'visible-label', value);
}
}
for (const element of document.querySelectorAll<HTMLElement>('[data-ai-generated], [data-content-origin]')) {
pushDisclosure(
candidates,
element.hasAttribute('data-ai-generated') ? 'data-ai-generated' : 'data-content-origin',
element.getAttribute('data-ai-generated') || element.getAttribute('data-content-origin')
);
}
return candidates;
}
export function discoverPage(): PageCandidate {
return {
url: location.href,
title: document.title,
images: discoverImages(),
texts: discoverText(),
disclosures: discoverDisclosures(),
heuristics: discoverHeuristics()
};
}
export { IMAGE_ID_ATTRIBUTE };
+247
View File
@@ -0,0 +1,247 @@
import type { HeuristicCategory, HeuristicFinding } from '../shared/types';
const MAX_ELEMENTS = 1500;
const MAX_STYLED_ELEMENTS = 700;
interface MutableFinding extends HeuristicFinding {
samples: Set<string>;
}
function finaliseFinding(finding: MutableFinding): HeuristicFinding {
return {
ruleId: finding.ruleId,
category: finding.category,
title: finding.title,
detail: finding.detail,
score: finding.score,
occurrences: finding.occurrences,
sample: finding.sample
};
}
function visible(element: HTMLElement): boolean {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width >= 12 && rect.height >= 8 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) !== 0;
}
function pixels(value: string): number {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function durationSeconds(value: string): number {
return value.split(',').reduce((maximum, item) => {
const trimmed = item.trim();
const parsed = Number.parseFloat(trimmed);
return Math.max(maximum, trimmed.endsWith('ms') ? parsed / 1000 : parsed);
}, 0);
}
function shortText(element: HTMLElement): string {
return (element.innerText || element.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 100);
}
function isCard(element: HTMLElement, style: CSSStyleDeclaration): boolean {
const rect = element.getBoundingClientRect();
if (rect.width < 120 || rect.height < 55) return false;
const hasEdge = pixels(style.borderTopWidth) > 0 || style.boxShadow !== 'none';
const hasSurface = style.backgroundColor !== 'rgba(0, 0, 0, 0)' || style.backgroundImage !== 'none';
return pixels(style.borderRadius) >= 8 && (hasEdge || hasSurface);
}
function structureSignature(element: HTMLElement): string {
const children = [...element.children].slice(0, 8);
return `${element.tagName}:${children.map((child) => child.tagName).join(',')}:${Boolean(element.querySelector('h2,h3,h4'))}:${Boolean(element.querySelector('p'))}`;
}
function normaliseFont(fontFamily: string): string {
return fontFamily.split(',')[0]?.replace(/["']/g, '').trim().toLowerCase() || '';
}
function addFinding(
findings: Map<string, MutableFinding>,
ruleId: string,
category: HeuristicCategory,
title: string,
detail: string,
score: number,
sample = ''
): void {
const existing = findings.get(ruleId);
if (existing) {
existing.occurrences += 1;
if (sample) existing.samples.add(sample);
return;
}
findings.set(ruleId, {
ruleId,
category,
title,
detail,
score,
occurrences: 1,
sample: sample || undefined,
samples: new Set(sample ? [sample] : [])
});
}
export function analyseCopyPatterns(text: string): HeuristicFinding[] {
const findings = new Map<string, MutableFinding>();
const clean = text.replace(/\s+/g, ' ').trim();
const words = clean ? clean.split(/\s+/).length : 0;
const buzzwords = clean.match(/\b(?:seamlessly|streamline|empower|supercharge|elevate|unlock|revolutioni[sz]e|cutting-edge|next-generation|world-class|enterprise-grade|game-changer|transformative|delve|ever-evolving|digital landscape)\b/gi) || [];
const uniqueBuzzwords = [...new Set(buzzwords.map((word) => word.toLowerCase()))];
if (uniqueBuzzwords.length >= 3) {
addFinding(findings, 'copy-marketing-buzzwords', 'copy', 'Dense generic marketing vocabulary', 'Several stock phrases commonly produced by generated SaaS copy appear together.', 2, uniqueBuzzwords.slice(0, 6).join(', '));
}
const emDashes = (clean.match(/—/g) || []).length;
if (emDashes >= 3 && (words === 0 || emDashes / words >= 0.003)) {
addFinding(findings, 'copy-em-dash-cadence', 'copy', 'Em-dash-heavy cadence', 'The page uses em dashes frequently enough to be a weak generated-copy signal.', 1, `${emDashes} em dashes in about ${words} words`);
}
const aphorisms = clean.match(/\bnot\s+(?:just\s+)?[^.!?]{1,50}[.!?]\s+(?:it(?:'s| is)|we(?:'re| are)|a|the)\s+[^.!?]{1,55}[.!?]/gi) || [];
if (aphorisms.length >= 2) {
addFinding(findings, 'copy-aphoristic-contrast', 'copy', 'Repeated manufactured contrast', 'Multiple “not X, but Y” style fragments create a recurring generated-marketing cadence.', 2, aphorisms[0]);
}
const theater = clean.match(/\b(?:growth|innovation|productivity|marketing|security|compliance|performance) theater\b/gi) || [];
if (theater.length > 0) {
addFinding(findings, 'copy-theater-framing', 'copy', '“Theater” framing', 'Dismissing a practice as “theater” is a recurring generated-copy tic.', 1, theater[0]);
}
const stockPromises = clean.match(/\b(?:build the future|everything you need|ship faster|work smarter|future of work|designed for modern teams|from idea to impact)\b/gi) || [];
if (stockPromises.length >= 2) {
addFinding(findings, 'copy-stock-promises', 'copy', 'Stock AI-product promises', 'Several interchangeable startup promises appear on the same page.', 2, stockPromises.slice(0, 3).join(', '));
}
return [...findings.values()].map(finaliseFinding);
}
export function discoverHeuristics(): HeuristicFinding[] {
const findings = new Map<string, MutableFinding>();
const elements = [...document.querySelectorAll<HTMLElement>('body *')].slice(0, MAX_ELEMENTS);
const styled = elements.filter(visible).slice(0, MAX_STYLED_ELEMENTS);
const cards: HTMLElement[] = [];
const fontCounts = new Map<string, number>();
for (const element of styled) {
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
const text = shortText(element);
const background = style.backgroundImage.toLowerCase();
const radius = pixels(style.borderRadius);
const font = normaliseFont(style.fontFamily);
if (font) fontCounts.set(font, (fontCounts.get(font) || 0) + 1);
if (isCard(element, style)) cards.push(element);
if (/repeating-(?:linear|radial)-gradient/.test(background)) {
addFinding(findings, 'visual-repeating-gradient', 'visual', 'Decorative repeating-gradient texture', 'Repeating gradient stripes or grids are a recurring generated-interface decoration.', 2);
}
if (/radial-gradient/.test(background) && rect.width >= 180 && rect.height >= 100) {
addFinding(findings, 'visual-radial-glow', 'visual', 'Decorative radial glow', 'A large radial-gradient halo is used as surface decoration.', 2);
}
if (/gradient/.test(background) && /(?:rgb\([^)]*(?:128|139|147|168|124|99)[^)]*\)|#(?:7c3aed|8b5cf6|9333ea|6366f1|06b6d4|22d3ee))/i.test(background)) {
addFinding(findings, 'visual-purple-cyan-gradient', 'visual', 'Purple/cyan gradient palette', 'Purple-to-blue or cyan gradients are a common generated landing-page default.', 2);
}
if (/gradient/.test(background) && (style.backgroundClip === 'text' || style.getPropertyValue('-webkit-background-clip') === 'text') && (style.color === 'transparent' || /rgba\([^)]*,\s*0\)/.test(style.color))) {
addFinding(findings, 'visual-gradient-text', 'visual', 'Gradient text', 'A heading or metric uses a decorative gradient fill.', 2, text);
}
const sideWidths = [style.borderTopWidth, style.borderRightWidth, style.borderBottomWidth, style.borderLeftWidth].map(pixels);
const thickSides = sideWidths.filter((width) => width >= 4).length;
if (radius >= 8 && thickSides === 1 && sideWidths.filter((width) => width <= 2).length >= 3) {
addFinding(findings, 'visual-side-accent', 'visual', 'Side-tab accent border', 'A rounded panel has one thick colored edge, a recognizable generated-UI pattern.', 2, text);
}
if (radius >= 8 && sideWidths.some((width) => width > 0 && width <= 1.5) && style.boxShadow !== 'none' && /\s(?:1[6-9]|[2-9]\d)px/.test(style.boxShadow)) {
addFinding(findings, 'visual-hairline-shadow', 'visual', 'Hairline border with wide shadow', 'A thin border is paired with a large diffuse shadow.', 1, text);
}
if (radius >= 24 && rect.width >= 120 && rect.width <= 520 && rect.height >= 55 && rect.height <= 320 && !/^(button|input)$/i.test(element.tagName)) {
addFinding(findings, 'visual-extreme-radius', 'visual', 'Over-rounded cards', 'Small content panels use very large corner radii, producing the common soft-blob aesthetic.', 1, text);
}
const isSmallSquare = rect.width >= 28 && rect.width <= 84 && rect.height >= 28 && rect.height <= 84 && Math.abs(rect.width - rect.height) <= 14;
if (isSmallSquare && radius >= 6 && element.parentElement?.querySelector(':scope > h2, :scope > h3, :scope > h4')) {
addFinding(findings, 'typography-icon-tile', 'typography', 'Icon tile stacked above a heading', 'A rounded-square icon container sits above a card heading, a frequent generated feature-card template.', 2, text);
}
if (/^H1$/.test(element.tagName) && text.split(/\s+/).length >= 6 && pixels(style.fontSize) >= 48) {
addFinding(findings, 'typography-oversized-hero', 'typography', 'Oversized sentence-length hero', 'A long sentence is rendered at display size in the opening hierarchy.', 1, text);
}
if (/^H1$/.test(element.tagName) && style.fontStyle === 'italic' && /serif/.test(style.fontFamily) && pixels(style.fontSize) >= 44) {
addFinding(findings, 'typography-italic-serif-hero', 'typography', 'Italic serif hero headline', 'An oversized italic serif hero is a common recent AI-startup default.', 2, text);
}
const nextHeading = element.nextElementSibling;
const uppercaseShort = text.length >= 2 && text.length <= 40 && text === text.toUpperCase() && /[A-Z]/.test(text);
const pill = radius >= rect.height * 0.4 && rect.width < 260;
if ((uppercaseShort || pill) && pixels(style.fontSize) <= 15 && nextHeading && /^H[1-3]$/.test(nextHeading.tagName)) {
addFinding(findings, 'typography-hero-eyebrow', 'typography', 'Hero eyebrow or pill', 'A small tracked label or pill appears directly above a large heading.', 2, text);
}
const animation = style.animationName;
const infinite = style.animationIterationCount.split(',').some((value) => value.trim() === 'infinite');
if (animation !== 'none' && infinite && durationSeconds(style.animationDuration) <= 4) {
if (/(?:status|pulse|ping|dot|live|online)/i.test(`${element.className} ${element.id} ${animation}`)) {
addFinding(findings, 'motion-pulsing-status', 'motion', 'Decorative pulsing status', 'A continuously animated status-like element may imply activity that is not data-driven.', 1, text);
} else if (rect.width >= 150) {
addFinding(findings, 'motion-looping-decoration', 'motion', 'Continuous decorative animation', 'A large element loops a short animation indefinitely.', 1, text);
}
}
if (element.tagName === 'MARQUEE') {
addFinding(findings, 'motion-marquee', 'motion', 'Auto-scrolling marquee', 'Content scrolls continuously instead of allowing the visitor to read at their own pace.', 2, text);
}
}
const overusedFonts = ['inter', 'geist', 'space grotesk', 'instrument serif'];
const overusedCount = [...fontCounts.entries()].reduce((sum, [font, count]) => sum + (overusedFonts.some((name) => font.includes(name)) ? count : 0), 0);
if (styled.length >= 20 && overusedCount / styled.length >= 0.55) {
addFinding(findings, 'typography-overused-font', 'typography', 'Overused generated-UI font', 'Most visible elements use Inter, Geist, Space Grotesk, or Instrument Serif.', 1, `${Math.round(overusedCount / styled.length * 100)}% of sampled elements`);
}
if (styled.length >= 30 && fontCounts.size === 1) {
addFinding(findings, 'typography-single-font', 'typography', 'Single font throughout the page', 'The sampled page uses one font family for every role, flattening typographic hierarchy.', 1, [...fontCounts.keys()][0]);
}
let nestedCards = 0;
const cardSet = new Set(cards);
for (const card of cards) {
let parent = card.parentElement;
while (parent && parent !== document.body) {
if (cardSet.has(parent)) {
nestedCards += 1;
break;
}
parent = parent.parentElement;
}
}
if (nestedCards >= 2) {
addFinding(findings, 'layout-nested-cards', 'layout', 'Cards nested inside cards', 'Several bordered or elevated panels are nested, creating generated “cardocalypse” depth.', 2, `${nestedCards} nested cards`);
}
for (const parent of styled) {
const children = [...parent.children].filter((child): child is HTMLElement => child instanceof HTMLElement && visible(child));
if (children.length < 3) continue;
const groups = new Map<string, HTMLElement[]>();
for (const child of children) {
const signature = structureSignature(child);
const group = groups.get(signature) || [];
group.push(child);
groups.set(signature, group);
}
if ([...groups.values()].some((group) => group.length >= 3 && group[0]?.querySelector('h2,h3,h4') && group[0]?.querySelector('p'))) {
addFinding(findings, 'layout-identical-card-grid', 'layout', 'Repeated identical feature cards', 'Three or more sibling cards repeat the same icon/heading/copy structure.', 3);
break;
}
}
const pageText = (document.body?.innerText || '').slice(0, 80_000);
for (const finding of analyseCopyPatterns(pageText)) {
findings.set(finding.ruleId, { ...finding, samples: new Set(finding.sample ? [finding.sample] : []) });
}
return [...findings.values()]
.map(finaliseFinding)
.sort((left, right) => right.score - left.score || right.occurrences - left.occurrences)
.slice(0, 30);
}
+121
View File
@@ -0,0 +1,121 @@
import browser from 'webextension-polyfill';
import { clearBadges, renderBadges } from './badges';
import { discoverPage } from './discovery';
import { clearNotice, renderNotice } from './notice';
import {
DEFAULT_SETTINGS,
type ContentRequest,
type ExtensionSettings,
type RuntimeResponse,
type ScanResult
} from '../shared/types';
interface SlopDetectGlobal {
__slopdetectInstalled?: boolean;
}
const state = globalThis as typeof globalThis & SlopDetectGlobal;
const RESCAN_COOLDOWN_MS = 10_000;
const MAX_AUTOMATIC_SCANS_PER_URL = 2;
if (!state.__slopdetectInstalled) {
state.__slopdetectInstalled = true;
let scheduledScan: number | undefined;
let lastScanAt = 0;
let lastUrl = location.href;
let automaticScansForUrl = 0;
let mutationCount = 0;
let scanSequence = 0;
const loadSettings = async (): Promise<ExtensionSettings> => {
const saved = await browser.storage.local.get('settings');
return { ...DEFAULT_SETTINGS, ...(saved.settings as Partial<ExtensionSettings> | undefined) };
};
const scan = async (mode: 'auto' | 'manual'): Promise<RuntimeResponse<ScanResult>> => {
const settings = await loadSettings();
if (mode === 'auto' && !settings.autoScan) return { ok: false, error: 'Automatic scanning is disabled.' };
const requestedUrl = location.href;
const sequence = ++scanSequence;
const response = await browser.runtime.sendMessage({ type: 'analyse-page', page: discoverPage(), mode }) as RuntimeResponse<ScanResult>;
if (!response.ok || !response.data) return response;
if (sequence !== scanSequence || requestedUrl !== location.href || response.data.pageUrl !== location.href) {
return { ok: false, error: 'The page changed before scanning finished.' };
}
lastScanAt = Date.now();
mutationCount = 0;
if (mode === 'auto') automaticScansForUrl += 1;
if (settings.showPageBadges) renderBadges(response.data);
else clearBadges();
if (settings.showInPageNotice) renderNotice(response.data);
else clearNotice();
return response;
};
const scheduleAutomaticScan = (delay: number, respectCooldown = true): void => {
if (automaticScansForUrl >= MAX_AUTOMATIC_SCANS_PER_URL) return;
if (scheduledScan !== undefined) window.clearTimeout(scheduledScan);
const cooldown = respectCooldown ? Math.max(0, RESCAN_COOLDOWN_MS - (Date.now() - lastScanAt)) : 0;
scheduledScan = window.setTimeout(() => {
scheduledScan = undefined;
void scan('auto').catch(() => {
// Protected or unloading pages may disappear during an automatic scan.
});
}, Math.max(delay, cooldown));
};
const resetForNavigation = (): void => {
if (location.href === lastUrl) return;
lastUrl = location.href;
automaticScansForUrl = 0;
mutationCount = 0;
clearBadges();
clearNotice(true);
scheduleAutomaticScan(350, false);
};
browser.runtime.onMessage.addListener((message: unknown) => {
const request = message as ContentRequest;
if (request?.type === 'clear-badges') {
clearBadges();
clearNotice(true);
return Promise.resolve({ ok: true } satisfies RuntimeResponse);
}
if (request?.type !== 'scan-page') return undefined;
return scan('manual');
});
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local' || !changes.settings) return;
const settings = { ...DEFAULT_SETTINGS, ...(changes.settings.newValue as Partial<ExtensionSettings> | undefined) };
if (!settings.showPageBadges) clearBadges();
if (!settings.showInPageNotice) clearNotice();
if (settings.autoScan) scheduleAutomaticScan(150, false);
else if (scheduledScan !== undefined) {
window.clearTimeout(scheduledScan);
scheduledScan = undefined;
}
});
const observer = new MutationObserver((mutations) => {
resetForNavigation();
for (const mutation of mutations) {
const additions = [...mutation.addedNodes].filter((node) => {
if (!(node instanceof HTMLElement)) return false;
return node.id !== 'slopdetect-page-notice' && node.id !== 'slopdetect-badge-layer';
});
mutationCount += additions.length;
}
if (mutationCount >= 25) scheduleAutomaticScan(1600);
});
observer.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('popstate', resetForNavigation);
window.addEventListener('hashchange', resetForNavigation);
window.addEventListener('load', () => {
if (automaticScansForUrl === 0) scheduleAutomaticScan(200, false);
}, { once: true });
scheduleAutomaticScan(200, false);
}
+175
View File
@@ -0,0 +1,175 @@
import type { Evidence, PageStatus, ScanResult } from '../shared/types';
const HOST_ID = 'slopdetect-page-notice';
const AUTO_COLLAPSE_MS = 9000;
let dismissedUrl: string | undefined;
interface NoticePresentation {
icon: string;
title: string;
summary: string;
tone: 'purple' | 'amber' | 'orange' | 'blue' | 'red';
}
const PRESENTATION: Partial<Record<PageStatus, NoticePresentation>> = {
'verified-ai': { icon: 'AI', title: 'Verified AI provenance', summary: 'A valid credential identifies AI-generated or AI-edited content.', tone: 'purple' },
'provenance-detected': { icon: 'AI', title: 'AI provenance detected', summary: 'AI provenance was found, but its signer is not trusted locally.', tone: 'amber' },
'watermark-detected': { icon: 'AI', title: 'AI watermark detected', summary: 'A compatible machine-readable watermark was detected.', tone: 'purple' },
'publisher-disclosed': { icon: 'AI', title: 'Publisher disclosed AI', summary: 'The publisher explicitly labels content on this page as AI-generated.', tone: 'amber' },
'heuristic-suspected': { icon: 'S', title: 'AI-style patterns suspected', summary: 'Several generated-site patterns appear together. This is a suspicion, not proof.', tone: 'orange' },
'mixed-provenance': { icon: '≋', title: 'Mixed provenance signals', summary: 'The page contains a mixture of AI, credential, or heuristic signals.', tone: 'blue' },
'invalid-credential': { icon: '!', title: 'Invalid provenance credential', summary: 'A credential was found but failed integrity validation.', tone: 'red' }
};
export function shouldShowNotice(result: ScanResult): boolean {
return PRESENTATION[result.summary.status] !== undefined;
}
function strongestFindings(result: ScanResult): Evidence[] {
const rank = { invalid: 5, verified: 4, detected: 3, disclosed: 2, clue: 1, none: 0 } as const;
return [result.page, ...result.images, ...result.texts]
.flatMap((item) => item.evidence)
.filter((evidence) => evidence.kind !== 'limitation')
.sort((left, right) => rank[right.strength] - rank[left.strength] || (right.score || 0) - (left.score || 0))
.slice(0, 5);
}
export function clearNotice(resetDismissal = false): void {
document.getElementById(HOST_ID)?.remove();
if (resetDismissal) dismissedUrl = undefined;
}
export function renderNotice(result: ScanResult): void {
clearNotice();
if (dismissedUrl === result.pageUrl) return;
const presentation = PRESENTATION[result.summary.status];
if (!presentation) return;
const host = document.createElement('aside');
host.id = HOST_ID;
host.style.cssText = 'all:initial;position:fixed;right:18px;bottom:18px;z-index:2147483646;';
const root = host.attachShadow({ mode: 'closed' });
const style = document.createElement('style');
style.textContent = `
:host { color-scheme: light dark; }
* { box-sizing: border-box; }
.wrap { --accent:#b05d16; width:min(360px,calc(100vw - 24px)); color:#211f2c; font:13px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; filter:drop-shadow(0 12px 32px #15122038); }
.wrap.purple { --accent:#6847ee; } .wrap.amber { --accent:#956100; } .wrap.orange { --accent:#b05d16; }
.wrap.blue { --accent:#4761a8; } .wrap.red { --accent:#bd2535; }
.card { overflow:hidden; background:#fff; border:1px solid #dedbe8; border-radius:16px; box-shadow:0 10px 30px #1512202b; }
.head { display:grid; grid-template-columns:36px 1fr auto; gap:10px; align-items:center; padding:12px; }
.mark { width:36px;height:36px;border-radius:11px;display:grid;place-items:center;background:var(--accent);color:#fff;font-weight:900;font-size:12px; }
h2 { margin:0;font-size:14px;line-height:1.25;color:inherit; }
.score { margin-top:2px;color:#686476;font-size:11px;font-weight:650; }
button { appearance:none;border:0;font:inherit;cursor:pointer; }
.icon { width:30px;height:30px;border-radius:8px;background:transparent;color:#686476;font-size:18px; }
.icon:hover { background:#f1eff7;color:#211f2c; }
.body { padding:0 14px 14px;border-top:1px solid #ece9f2; }
.summary { margin:11px 0 9px;color:#555160; }
ul { list-style:none;margin:0;padding:0;display:grid;gap:6px; }
li { padding:7px 8px;border-radius:9px;background:#f6f5fa; }
li strong { display:block;font-size:11px; }
li span { display:block;margin-top:1px;color:#686476;font-size:10px; }
.hint { margin:9px 0 0;color:#777281;font-size:10px; }
.chip { margin-left:auto;display:flex;align-items:center;gap:8px;padding:7px 10px 7px 7px;border-radius:999px;background:#fff;color:#211f2c;border:1px solid #dedbe8;box-shadow:0 6px 20px #1512202b;font-weight:750; }
.chip .mark { width:27px;height:27px;border-radius:50%;font-size:10px; }
.hidden { display:none; }
@media (prefers-reduced-motion:no-preference) { .card,.chip { animation:enter .2s ease-out; } @keyframes enter { from { opacity:0;transform:translateY(7px) } } }
@media (prefers-color-scheme:dark) {
.wrap { color:#f4f1fb; } .card,.chip { background:#211f2c;border-color:#454150; } .score,.summary,li span,.hint { color:#b2adbd; }
.body { border-color:#3b3846; } li { background:#2d2a39; } .icon { color:#b2adbd; } .icon:hover { background:#353140;color:#fff; }
}
`;
const wrap = document.createElement('div');
wrap.className = `wrap ${presentation.tone}`;
const card = document.createElement('section');
card.className = 'card';
card.setAttribute('role', 'status');
card.setAttribute('aria-live', 'polite');
const scoreLabel = result.summary.heuristicScore > 0
? `Slop score ${result.summary.heuristicScore} · ${result.summary.heuristicFindings} pattern${result.summary.heuristicFindings === 1 ? '' : 's'}`
: `${result.summary.aiItems} AI evidence item${result.summary.aiItems === 1 ? '' : 's'}`;
const head = document.createElement('div');
head.className = 'head';
const mark = document.createElement('div');
mark.className = 'mark';
mark.textContent = presentation.icon;
const heading = document.createElement('div');
const title = document.createElement('h2');
title.textContent = presentation.title;
const score = document.createElement('div');
score.className = 'score';
score.textContent = scoreLabel;
heading.append(title, score);
head.append(mark, heading);
const close = document.createElement('button');
close.className = 'icon';
close.type = 'button';
close.title = 'Dismiss for this page';
close.setAttribute('aria-label', 'Dismiss SlopDetect result for this page');
close.textContent = '×';
head.append(close);
const body = document.createElement('div');
body.className = 'body';
const summary = document.createElement('p');
summary.className = 'summary';
summary.textContent = presentation.summary;
body.append(summary);
const findings = strongestFindings(result);
if (findings.length > 0) {
const list = document.createElement('ul');
for (const finding of findings) {
const item = document.createElement('li');
const itemTitle = document.createElement('strong');
itemTitle.textContent = finding.title;
const meta = document.createElement('span');
meta.textContent = finding.kind === 'heuristic'
? `${finding.category || 'pattern'} · +${finding.score || 0}${(finding.occurrences || 0) > 1 ? ` · ${finding.occurrences} matches` : ''}`
: finding.strength;
item.append(itemTitle, meta);
list.append(item);
}
body.append(list);
}
const hint = document.createElement('p');
hint.className = 'hint';
hint.textContent = 'Click the toolbar icon for complete evidence and limitations.';
body.append(hint);
card.append(head, body);
const chip = document.createElement('button');
chip.className = 'chip hidden';
chip.type = 'button';
chip.setAttribute('aria-label', `${presentation.title}. ${scoreLabel}. Show details.`);
const chipMark = document.createElement('span');
chipMark.className = 'mark';
chipMark.textContent = presentation.icon;
const chipText = document.createElement('span');
chipText.textContent = result.summary.heuristicScore > 0 ? `Score ${result.summary.heuristicScore}` : presentation.title;
chip.append(chipMark, chipText);
const collapseTimer = window.setTimeout(() => {
card.classList.add('hidden');
chip.classList.remove('hidden');
}, AUTO_COLLAPSE_MS);
close.addEventListener('click', () => {
window.clearTimeout(collapseTimer);
dismissedUrl = result.pageUrl;
host.remove();
});
chip.addEventListener('click', () => {
window.clearTimeout(collapseTimer);
chip.classList.add('hidden');
card.classList.remove('hidden');
});
card.addEventListener('click', () => window.clearTimeout(collapseTimer), { once: true });
wrap.append(card, chip);
root.append(style, wrap);
document.documentElement.append(host);
}
+1
View File
@@ -0,0 +1 @@
declare module '*.css';
+92
View File
@@ -0,0 +1,92 @@
import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import browser from 'webextension-polyfill';
import { DEFAULT_SETTINGS, type ExtensionSettings } from '../shared/types';
import './options.css';
function App(): preact.JSX.Element {
const [settings, setSettings] = useState<ExtensionSettings>(DEFAULT_SETTINGS);
const [notice, setNotice] = useState('');
useEffect(() => {
void browser.storage.local.get('settings').then((saved) => {
setSettings({ ...DEFAULT_SETTINGS, ...(saved.settings as Partial<ExtensionSettings> | undefined) });
});
}, []);
const update = <K extends keyof ExtensionSettings>(key: K, value: ExtensionSettings[K]): void => {
setSettings((current) => ({ ...current, [key]: value }));
};
const save = async (): Promise<void> => {
const safeSettings = {
...settings,
maxImageBytes: Math.min(100, Math.max(1, settings.maxImageBytes / 1048576)) * 1048576,
maxImagesPerPage: Math.min(200, Math.max(1, Math.round(settings.maxImagesPerPage))),
maxAutoImagesPerPage: Math.min(30, Math.max(0, Math.round(settings.maxAutoImagesPerPage))),
heuristicThreshold: Math.min(20, Math.max(3, Math.round(settings.heuristicThreshold)))
};
await browser.storage.local.set({ settings: safeSettings });
setSettings(safeSettings);
setNotice('Settings saved.');
window.setTimeout(() => setNotice(''), 2000);
};
return <main class="shell">
<header class="heading"><img src="icons/icon.svg" alt="" /><div><h1>SlopDetect settings</h1><div class="muted">Privacy-first AI provenance inspection</div></div></header>
<section class="card">
<h2>Automatic protection</h2>
<p class="muted">SlopDetect runs locally after ordinary HTTP and HTTPS pages load. Pages without meaningful evidence stay visually untouched.</p>
<div class="setting">
<div><label htmlFor="auto-scan">Scan pages automatically</label><p class="muted">Runs on initial load and once more when a page fills itself dynamically.</p></div>
<input id="auto-scan" type="checkbox" checked={settings.autoScan} onChange={(event) => update('autoScan', event.currentTarget.checked)} />
</div>
<div class="setting">
<div><label htmlFor="page-notice">Show in-page results</label><p class="muted">Displays a compact result card only for flagged pages, then collapses it to a score chip.</p></div>
<input id="page-notice" type="checkbox" checked={settings.showInPageNotice} onChange={(event) => update('showInPageNotice', event.currentTarget.checked)} />
</div>
</section>
<section class="card">
<h2>Scanning</h2>
<div class="setting">
<div><label htmlFor="badges">Show badges on inspected images</label><p class="muted">Adds small AI, Content Credential, or invalid-credential markers to the page.</p></div>
<input id="badges" type="checkbox" checked={settings.showPageBadges} onChange={(event) => update('showPageBadges', event.currentTarget.checked)} />
</div>
<div class="setting">
<div><label htmlFor="heuristics">Detect AI-style page patterns</label><p class="muted">Scores deterministic visual, layout, motion, typography, and copy clues. These are suspicions, not provenance proof.</p></div>
<input id="heuristics" type="checkbox" checked={settings.enableHeuristicDetection} onChange={(event) => update('enableHeuristicDetection', event.currentTarget.checked)} />
</div>
<div class="setting">
<div><label htmlFor="threshold">Suspicion threshold</label><p class="muted">Lower values flag more pages but increase false positives. Default: 6.</p></div>
<input id="threshold" type="number" min="3" max="20" value={settings.heuristicThreshold} onInput={(event) => update('heuristicThreshold', Number(event.currentTarget.value))} />
</div>
<div class="setting">
<div><label htmlFor="raw">Include raw credential data</label><p class="muted">Useful for auditing, but consumes more memory. It remains local.</p></div>
<input id="raw" type="checkbox" checked={settings.includeRawEvidence} onChange={(event) => update('includeRawEvidence', event.currentTarget.checked)} />
</div>
<div class="setting">
<div><label htmlFor="auto-limit">Images in automatic scans</label><p class="muted">Checks the largest visible images first. Lower values reduce page-load work; zero scans only the site itself.</p></div>
<input id="auto-limit" type="number" min="0" max="30" value={settings.maxAutoImagesPerPage} onInput={(event) => update('maxAutoImagesPerPage', Number(event.currentTarget.value))} />
</div>
<div class="setting">
<div><label htmlFor="limit">Images in manual scans</label><p class="muted">A toolbar rescan may inspect more images than the automatic page-load scan.</p></div>
<input id="limit" type="number" min="1" max="200" value={settings.maxImagesPerPage} onInput={(event) => update('maxImagesPerPage', Number(event.currentTarget.value))} />
</div>
<div class="setting">
<div><label htmlFor="size">Maximum image size (MB)</label><p class="muted">Resources larger than this are left as unknown.</p></div>
<input id="size" type="number" min="1" max="100" value={Math.round(settings.maxImageBytes / 1048576)} onInput={(event) => update('maxImageBytes', Number(event.currentTarget.value) * 1048576)} />
</div>
<div class="row"><button class="button" onClick={save}>Save settings</button><span class="notice">{notice}</span></div>
</section>
<section class="card">
<h2>Detection policy</h2>
<p>SlopDetect keeps verified provenance, unsigned generator metadata, and stylistic suspicions separate. Heuristics can produce false positives and never prove who authored a site. C2PA inspection happens locally using <code>@contentauth/c2pa-web</code>. Remote services are not enabled.</p>
</section>
</main>;
}
const app = document.getElementById('app');
if (app) render(<App />, app);
+22
View File
@@ -0,0 +1,22 @@
@import '../ui/base.css';
body { min-height: 100vh; }
.shell { width: min(760px, calc(100% - 32px)); margin: 40px auto; display: grid; gap: 18px; }
.heading { display: flex; align-items: center; gap: 14px; }
.heading img { width: 48px; height: 48px; }
h1 { margin: 0; font-size: 28px; }
h2 { margin: 0 0 8px; font-size: 17px; }
p { line-height: 1.55; }
.setting { display: flex; justify-content: space-between; gap: 24px; align-items: start; padding: 12px 0; border-bottom: 1px solid #e3e0eb; }
.setting:last-child { border: 0; }
.setting label { font-weight: 700; }
.setting p { margin: 4px 0 0; font-size: 13px; }
.setting input[type="number"] { width: 90px; padding: 7px; border: 1px solid #aaa5b8; border-radius: 8px; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.notice { font-size: 13px; min-height: 20px; color: #276755; }
code { background: #ece9f4; padding: 2px 5px; border-radius: 4px; }
@media (prefers-color-scheme: dark) {
.setting { border-color: #393645; }
code { background: #312e3d; }
}
+112
View File
@@ -0,0 +1,112 @@
import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import browser from 'webextension-polyfill';
import type { AssetResult, PageStatus, RuntimeResponse, ScanResult } from '../shared/types';
import './popup.css';
const STATUS: Record<PageStatus, { title: string; detail: string }> = {
'verified-ai': { title: 'Verified AI provenance', detail: 'A valid provenance credential identifies AI-generated or manipulated content.' },
'provenance-detected': { title: 'AI provenance detected', detail: 'A readable credential identifies AI origin, but its signer is not trusted by the bundled trust list.' },
'watermark-detected': { title: 'AI watermark detected', detail: 'A compatible watermark detector found machine-readable AI evidence.' },
'publisher-disclosed': { title: 'Publisher disclosed AI', detail: 'The page contains an explicit AI-content disclosure.' },
'heuristic-suspected': { title: 'AI-style patterns suspected', detail: 'Several visual, layout, motion, or copy patterns associated with generated sites appear together. This is not proof of AI authorship.' },
'mixed-provenance': { title: 'Mixed provenance', detail: 'The inspected items contain different or partial provenance signals.' },
'invalid-credential': { title: 'Invalid credential', detail: 'Provenance metadata was found but failed integrity validation.' },
'no-supported-evidence': { title: 'No supported evidence', detail: 'No compatible marking was found. This does not mean the content is human-made.' },
'unable-to-inspect': { title: 'Unable to inspect', detail: 'The page or its relevant resources could not be accessed.' }
};
async function activeTab(): Promise<browser.Tabs.Tab> {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab?.id) throw new Error('No active browser tab is available.');
return tab;
}
function EvidenceList({ result }: { result: ScanResult }): preact.JSX.Element | null {
const items = [result.page, ...result.images, ...result.texts].filter(
(item) => item.evidence.length > 0 && (item.strength !== 'none' || item.evidence.some((entry) => entry.kind === 'limitation'))
);
if (items.length === 0) return null;
return <div class="results">
{items.map((item: AssetResult) => <details class="item" key={item.id}>
<summary>{item.kind === 'image' ? 'Image' : item.kind === 'text' ? 'Text' : 'Page'}: {item.label.slice(0, 80)}</summary>
{item.evidence.map((evidence) => <div class="evidence" key={evidence.id}>
<strong>{evidence.title}</strong>
<span>{evidence.detail}</span>
{evidence.kind === 'heuristic' && <div class="evidence-meta">{evidence.category} · +{evidence.score} point{evidence.score === 1 ? '' : 's'}{(evidence.occurrences || 0) > 1 ? ` · ${evidence.occurrences} matches` : ''}</div>}
{evidence.issuer && <div>Issuer: {evidence.issuer}</div>}
</div>)}
</details>)}
</div>;
}
function App(): preact.JSX.Element {
const [result, setResult] = useState<ScanResult>();
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
void (async () => {
try {
const tab = await activeTab();
const response = await browser.runtime.sendMessage({ type: 'get-tab-result', tabId: tab.id }) as RuntimeResponse<ScanResult | undefined>;
if (response.ok && response.data?.pageUrl === tab.url) setResult(response.data);
} catch {
// The active tab may be a protected Firefox page.
}
})();
}, []);
const scan = async (): Promise<void> => {
setBusy(true);
setError('');
try {
const tab = await activeTab();
const response = await browser.tabs.sendMessage(tab.id!, { type: 'scan-page' }) as RuntimeResponse<ScanResult>;
if (!response.ok || !response.data) throw new Error(response.error || 'The scan did not return a result.');
setResult(response.data);
} catch (scanError) {
setError(scanError instanceof Error ? scanError.message : String(scanError));
} finally {
setBusy(false);
}
};
const status = result ? STATUS[result.summary.status] : undefined;
return <main class="shell">
<header class="header">
<div><div class="eyebrow">Automatic page check</div><h1>SlopDetect</h1></div>
</header>
<div class="toolbar-actions">
<button class="button" disabled={busy} onClick={scan}>{busy ? 'Scanning…' : result ? 'Scan again' : 'Scan this page'}</button>
<button class="button secondary" onClick={() => browser.runtime.openOptionsPage()}>Settings</button>
</div>
{error && <div class="error">{error}</div>}
{!result ? <section class="empty-copy">
<h2>No result for this page yet</h2>
<p class="muted">Pages scan automatically after loading. Use the button above to request a fresh full scan.</p>
</section> : <>
<section class={`card status ${result.summary.status}`}>
<div class="status-label"><span></span>Current page</div>
<h2>{status?.title}</h2>
<p class="muted">{status?.detail}</p>
</section>
<section class="stats">
<div class="stat"><strong>{result.summary.heuristicScore}</strong><span>slop score</span></div>
<div class="stat"><strong>{result.summary.inspectedImages}/{result.summary.totalImages}</strong><span>images inspected</span></div>
<div class="stat"><strong>{result.summary.heuristicFindings}</strong><span>style clues</span></div>
</section>
{result.warnings.length > 0 && <ul class="warnings">{result.warnings.map((warning) => <li key={warning}>{warning}</li>)}</ul>}
<EvidenceList result={result} />
<p class="muted footnote">No mark found means unknown, not human-made. Scanned {new Date(result.scannedAt).toLocaleString()}.</p>
</>}
</main>;
}
const app = document.getElementById('app');
if (app) render(<App />, app);
+45
View File
@@ -0,0 +1,45 @@
@import '../ui/base.css';
body { width: 360px; }
.shell { padding: 13px; display: grid; gap: 10px; }
.header { display: flex; align-items: center; min-height: 32px; }
.eyebrow { margin-bottom: 1px; color: #777281; font-size: 9px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; }
h1 { font-size: 17px; line-height: 1.1; margin: 0; }
.toolbar-actions { display: grid; grid-template-columns: 1fr auto; gap: 7px; }
.toolbar-actions .button { min-height: 36px; padding: 8px 13px; border-radius: 9px; }
.status { --status:#74707e; border-left: 4px solid var(--status); padding: 11px 12px; }
.status.verified-ai, .status.watermark-detected { --status:#6847ee; }
.status.provenance-detected, .status.publisher-disclosed { --status:#956100; }
.status.heuristic-suspected { --status:#b05d16; }
.status.mixed-provenance { --status:#4761a8; }
.status.invalid-credential { --status:#bd2535; }
.status-label { display:flex;align-items:center;gap:6px;margin-bottom:4px;color:#777281;font-size:9px;font-weight:800;letter-spacing:.08em;text-transform:uppercase; }
.status-label span { width:7px;height:7px;border-radius:50%;background:var(--status); }
.status h2 { margin: 0 0 3px; font-size: 14px; }
.status p { margin: 0; font-size: 12px; line-height: 1.4; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
.stat { text-align: center; padding: 7px 4px; border-radius: 8px; background: #f1eff7; }
.stat strong { display: block; font-size: 15px; }
.stat span { font-size: 10px; color: #686476; }
.warnings { margin: 0; padding-left: 20px; font-size: 12px; color: #73510d; }
.results { display: grid; gap: 6px; max-height: 220px; overflow: auto; }
details.item { border: 1px solid #e2dfeb; border-radius: 9px; padding: 8px 10px; }
details.item summary { cursor: pointer; font-size: 12px; font-weight: 700; }
.evidence { margin-top: 8px; padding-top: 7px; border-top: 1px solid #ece9f2; font-size: 11px; line-height: 1.4; }
.evidence strong { display: block; }
.evidence-meta { margin-top: 3px; color: #7a4a15; font-weight: 700; text-transform: capitalize; }
.empty-copy { border-top: 1px solid #e2dfeb; padding: 10px 2px 2px; }
.empty-copy h2 { font-size: 13px; margin: 0 0 4px; }
.empty-copy p { font-size: 11px; line-height: 1.45; margin: 0; }
.footnote { margin: 0; font-size: 10px; line-height: 1.4; }
@media (prefers-color-scheme: dark) {
.stat { background: #2d2a39; }
.stat span { color: #aaa5b8; }
details.item { border-color: #3b3846; }
.evidence { border-color: #3b3846; }
.evidence-meta { color: #e4a55f; }
.warnings { color: #e4c16f; }
.eyebrow, .status-label { color: #aaa5b8; }
.empty-copy { border-color: #3b3846; }
}
+95
View File
@@ -0,0 +1,95 @@
import type {
AssetResult,
Evidence,
EvidenceStrength,
OriginVerdict,
PageStatus,
ScanSummary
} from './types';
import { heuristicScore } from '../background/heuristics';
const strengthRank: Record<EvidenceStrength, number> = {
none: 0,
clue: 1,
disclosed: 2,
detected: 3,
verified: 4,
invalid: 5
};
export function strongestEvidence(evidence: Evidence[]): EvidenceStrength {
return evidence.reduce<EvidenceStrength>(
(strongest, item) => (strengthRank[item.strength] > strengthRank[strongest] ? item.strength : strongest),
'none'
);
}
export function verdictFromEvidence(evidence: Evidence[]): OriginVerdict {
if (evidence.some((item) => item.verdict === 'invalid')) return 'invalid';
if (evidence.some((item) => item.verdict === 'ai')) return 'ai';
if (evidence.some((item) => item.verdict === 'not-ai-asserted')) return 'not-ai-asserted';
return 'unknown';
}
export function createAssetResult(
id: string,
kind: AssetResult['kind'],
label: string,
evidence: Evidence[],
sourceUrl?: string
): AssetResult {
return {
id,
kind,
label,
sourceUrl,
verdict: verdictFromEvidence(evidence),
strength: strongestEvidence(evidence),
evidence
};
}
export function aggregateSummary(
page: AssetResult,
images: AssetResult[],
texts: AssetResult[],
totalImages: number,
totalTextBlocks: number,
heuristicThreshold = 6
): ScanSummary {
const items = [page, ...images, ...texts];
const aiItems = items.filter((item) => item.verdict === 'ai').length;
const invalidItems = items.filter((item) => item.verdict === 'invalid').length;
const disclosedItems = items.filter((item) => item.strength === 'disclosed').length;
const inaccessibleItems = items.filter((item) =>
item.evidence.some((evidence) => evidence.kind === 'limitation')
).length;
const evidenceBearingItems = items.filter((item) => item.strength !== 'none').length;
const pageHeuristicScore = heuristicScore(page.evidence);
const heuristicFindings = page.evidence.filter((evidence) => evidence.kind === 'heuristic').length;
let status: PageStatus;
if (invalidItems > 0) status = 'invalid-credential';
else if (aiItems > 0 && aiItems < items.length && evidenceBearingItems > aiItems) status = 'mixed-provenance';
else if (items.some((item) => item.verdict === 'ai' && item.strength === 'verified')) status = 'verified-ai';
else if (items.some((item) => item.evidence.some((evidence) => evidence.kind === 'watermark' && evidence.verdict === 'ai'))) status = 'watermark-detected';
else if (items.some((item) => item.verdict === 'ai' && item.strength === 'detected')) status = 'provenance-detected';
else if (items.some((item) => item.verdict === 'ai' && item.strength === 'disclosed')) status = 'publisher-disclosed';
else if (pageHeuristicScore >= heuristicThreshold && heuristicFindings >= 2) status = 'heuristic-suspected';
else if (inaccessibleItems > 0 && images.length === 0 && texts.length === 0) status = 'unable-to-inspect';
else status = 'no-supported-evidence';
return {
status,
inspectedImages: images.length,
totalImages,
inspectedTextBlocks: texts.length,
totalTextBlocks,
aiItems,
invalidItems,
disclosedItems,
inaccessibleItems,
heuristicScore: pageHeuristicScore,
heuristicFindings
};
}
+142
View File
@@ -0,0 +1,142 @@
export type EvidenceStrength = 'verified' | 'detected' | 'disclosed' | 'clue' | 'invalid' | 'none';
export type OriginVerdict = 'ai' | 'not-ai-asserted' | 'unknown' | 'invalid';
export type AssetKind = 'image' | 'text' | 'page';
export interface Evidence {
id: string;
kind: 'c2pa' | 'watermark' | 'disclosure' | 'generator' | 'heuristic' | 'limitation';
strength: EvidenceStrength;
verdict: OriginVerdict;
title: string;
detail: string;
detector?: string;
issuer?: string;
category?: HeuristicCategory;
score?: number;
occurrences?: number;
raw?: unknown;
}
export type HeuristicCategory = 'visual' | 'typography' | 'layout' | 'motion' | 'copy';
export interface HeuristicFinding {
ruleId: string;
category: HeuristicCategory;
title: string;
detail: string;
score: number;
occurrences: number;
sample?: string;
}
export interface ImageCandidate {
id: string;
url: string;
alt: string;
width: number;
height: number;
}
export interface TextCandidate {
id: string;
text: string;
tokenEstimate: number;
}
export interface PageDisclosureCandidate {
source: string;
value: string;
}
export interface PageCandidate {
url: string;
title: string;
images: ImageCandidate[];
texts: TextCandidate[];
disclosures: PageDisclosureCandidate[];
heuristics: HeuristicFinding[];
}
export interface AssetResult {
id: string;
kind: AssetKind;
label: string;
sourceUrl?: string;
verdict: OriginVerdict;
strength: EvidenceStrength;
evidence: Evidence[];
}
export type PageStatus =
| 'verified-ai'
| 'provenance-detected'
| 'watermark-detected'
| 'publisher-disclosed'
| 'heuristic-suspected'
| 'mixed-provenance'
| 'invalid-credential'
| 'no-supported-evidence'
| 'unable-to-inspect';
export interface ScanSummary {
status: PageStatus;
inspectedImages: number;
totalImages: number;
inspectedTextBlocks: number;
totalTextBlocks: number;
aiItems: number;
invalidItems: number;
disclosedItems: number;
inaccessibleItems: number;
heuristicScore: number;
heuristicFindings: number;
}
export interface ScanResult {
scanId: string;
pageUrl: string;
pageTitle: string;
scannedAt: string;
summary: ScanSummary;
page: AssetResult;
images: AssetResult[];
texts: AssetResult[];
warnings: string[];
}
export interface ExtensionSettings {
autoScan: boolean;
showInPageNotice: boolean;
showPageBadges: boolean;
includeRawEvidence: boolean;
maxImageBytes: number;
maxImagesPerPage: number;
maxAutoImagesPerPage: number;
enableHeuristicDetection: boolean;
heuristicThreshold: number;
}
export const DEFAULT_SETTINGS: ExtensionSettings = {
autoScan: true,
showInPageNotice: true,
showPageBadges: true,
includeRawEvidence: false,
maxImageBytes: 25 * 1024 * 1024,
maxImagesPerPage: 50,
maxAutoImagesPerPage: 10,
enableHeuristicDetection: true,
heuristicThreshold: 6
};
export type RuntimeRequest =
| { type: 'analyse-page'; page: PageCandidate; mode?: 'auto' | 'manual' }
| { type: 'get-tab-result'; tabId: number }
| { type: 'clear-tab-result'; tabId: number };
export type ContentRequest = { type: 'scan-page' } | { type: 'clear-badges' };
export interface RuntimeResponse<T = unknown> {
ok: boolean;
data?: T;
error?: string;
}
+36
View File
@@ -0,0 +1,36 @@
:root {
color-scheme: light dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f6f5fb;
color: #211f2c;
font-synthesis: none;
}
* { box-sizing: border-box; }
body { margin: 0; }
button, input { font: inherit; }
button { cursor: pointer; }
.button {
border: 0;
border-radius: 10px;
padding: 10px 14px;
background: #6847ee;
color: white;
font-weight: 700;
}
.button:hover { background: #5738d5; }
.button:disabled { cursor: wait; opacity: .65; }
.button.secondary { background: #e7e3f9; color: #31266a; }
.button.danger { background: #f7dfe2; color: #7e1826; }
.muted { color: #686476; }
.card { background: white; border: 1px solid #dedbe8; border-radius: 14px; padding: 14px; }
.error { color: #9d1f31; background: #fbe9ec; border-radius: 10px; padding: 10px; }
@media (prefers-color-scheme: dark) {
:root { background: #171620; color: #f4f1fb; }
.card { background: #211f2c; border-color: #393645; }
.muted { color: #aaa5b8; }
.button.secondary { background: #38324f; color: #eee9ff; }
}
+57
View File
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { evidenceFromManifestStore, FirefoxExtensionWorkerUrl } from '../src/background/c2pa';
describe('C2PA evidence interpretation', () => {
it('recognises a trusted AI digital source type', () => {
const result = evidenceFromManifestStore({
active_manifest: 'example:claim',
validation_state: 'Trusted',
manifests: {
'example:claim': {
signature_info: { issuer: 'Example CA' },
assertions: [{ data: { digitalSourceType: 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia' } }]
}
}
}, false);
expect(result[0]).toMatchObject({ strength: 'verified', verdict: 'ai', issuer: 'Example CA' });
});
it('does not claim trusted status for a merely valid signer', () => {
const result = evidenceFromManifestStore({
active_manifest: 'example:claim',
validation_state: 'Valid',
manifests: {
'example:claim': { source_type: 'trainedAlgorithmicMedia' }
}
}, false);
expect(result[0]).toMatchObject({ strength: 'detected', verdict: 'ai' });
});
it('prioritises a failed integrity status', () => {
const result = evidenceFromManifestStore({
active_manifest: 'example:claim',
validation_state: 'Invalid',
manifests: { 'example:claim': {} },
validation_status: [{ code: 'assertion.dataHash.mismatch' }]
}, false);
expect(result[0]).toMatchObject({ strength: 'invalid', verdict: 'invalid' });
});
it('returns no evidence when a manifest is absent', () => {
expect(evidenceFromManifestStore({ validation_state: 'Valid' }, false)).toEqual([]);
});
});
describe('Firefox worker URL compatibility', () => {
it('passes the pinned SDK HTTPS guard but resolves to the packaged extension worker', () => {
const packaged = 'moz-extension://12345678-1234-1234-1234-123456789abc/c2pa_worker.js';
const workerUrl = new FirefoxExtensionWorkerUrl(packaged);
expect(workerUrl.protocol).toBe('https:');
expect(workerUrl.toString()).toBe(packaged);
});
it('refuses non-extension worker resources', () => {
expect(() => new FirefoxExtensionWorkerUrl('https://example.com/worker.js')).toThrow(/packaged Firefox extension resource/);
});
});
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { ResultCache, hashBlob } from '../src/background/cache';
describe('result cache', () => {
it('evicts the least recently used entry', () => {
const cache = new ResultCache<number>(2);
cache.set('one', 1);
cache.set('two', 2);
expect(cache.get('one')).toBe(1);
cache.set('three', 3);
expect(cache.get('two')).toBeUndefined();
expect(cache.get('one')).toBe(1);
});
it('hashes identical content consistently', async () => {
const first = await hashBlob(new Blob(['same content']));
const second = await hashBlob(new Blob(['same content']));
expect(first).toBe(second);
expect(first).toMatch(/^[a-f0-9]{64}$/);
});
});
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { analyseDisclosures } from '../src/background/disclosures';
describe('publisher disclosure parsing', () => {
it('recognises explicit AI-origin metadata', () => {
const result = analyseDisclosures([{ source: 'meta:ai-generated', value: 'true' }]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({ strength: 'disclosed', verdict: 'ai' });
});
it('treats generator metadata as a clue rather than proof', () => {
const result = analyseDisclosures([{ source: 'meta:generator', value: 'Framer AI' }]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({ strength: 'clue', verdict: 'unknown' });
});
it('ignores ordinary website-generator metadata', () => {
expect(analyseDisclosures([{ source: 'meta:generator', value: 'WordPress 7.0' }])).toEqual([]);
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { analyseCopyPatterns } from '../src/content/heuristics';
import { evidenceFromHeuristics, heuristicScore } from '../src/background/heuristics';
describe('page heuristics', () => {
it('finds dense buzzwords and em-dash cadence without declaring an AI verdict', () => {
const text = 'Supercharge your workflow with our world-class, enterprise-grade platform — move faster — work smarter — without compromise.';
const findings = analyseCopyPatterns(text);
expect(findings.map((finding) => finding.ruleId)).toEqual(expect.arrayContaining([
'copy-marketing-buzzwords',
'copy-em-dash-cadence'
]));
expect(evidenceFromHeuristics(findings).every((item) => item.verdict === 'unknown')).toBe(true);
});
it('adds a diversity point when independent categories agree', () => {
const evidence = evidenceFromHeuristics([
{ ruleId: 'one', category: 'visual', title: 'One', detail: 'One', score: 2, occurrences: 1 },
{ ruleId: 'two', category: 'layout', title: 'Two', detail: 'Two', score: 3, occurrences: 1 }
]);
expect(heuristicScore(evidence)).toBe(6);
});
it('ignores ordinary direct prose', () => {
expect(analyseCopyPatterns('The archive contains monthly invoices. Select a year, then download the required PDF.')).toEqual([]);
});
});
+14
View File
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { inspectImageGeneratorMetadata } from '../src/background/image-metadata';
describe('image generator metadata clues', () => {
it('recognises a Stable Diffusion parameters block', async () => {
const blob = new Blob(['PNG metadata\0Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 42']);
const evidence = await inspectImageGeneratorMetadata(blob);
expect(evidence[0]).toMatchObject({ kind: 'generator', strength: 'detected', verdict: 'ai' });
});
it('does not flag an ordinary image byte sequence', async () => {
expect(await inspectImageGeneratorMetadata(new Blob(['ordinary image metadata']))).toEqual([]);
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { shouldShowNotice } from '../src/content/notice';
import type { PageStatus, ScanResult } from '../src/shared/types';
function resultWithStatus(status: PageStatus): ScanResult {
return {
scanId: 'scan',
pageUrl: 'https://example.com/',
pageTitle: 'Example',
scannedAt: new Date(0).toISOString(),
summary: {
status,
inspectedImages: 0,
totalImages: 0,
inspectedTextBlocks: 0,
totalTextBlocks: 0,
aiItems: 0,
invalidItems: 0,
disclosedItems: 0,
inaccessibleItems: 0,
heuristicScore: status === 'heuristic-suspected' ? 7 : 0,
heuristicFindings: status === 'heuristic-suspected' ? 3 : 0
},
page: { id: 'page', kind: 'page', label: 'Example', verdict: 'unknown', strength: 'none', evidence: [] },
images: [],
texts: [],
warnings: []
};
}
describe('in-page result visibility', () => {
it('shows meaningful heuristic and provenance results', () => {
expect(shouldShowNotice(resultWithStatus('heuristic-suspected'))).toBe(true);
expect(shouldShowNotice(resultWithStatus('verified-ai'))).toBe(true);
expect(shouldShowNotice(resultWithStatus('invalid-credential'))).toBe(true);
});
it('stays silent on unknown or inaccessible pages', () => {
expect(shouldShowNotice(resultWithStatus('no-supported-evidence'))).toBe(false);
expect(shouldShowNotice(resultWithStatus('unable-to-inspect'))).toBe(false);
});
});
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest';
import { aggregateSummary, createAssetResult, strongestEvidence, verdictFromEvidence } from '../src/shared/status';
import type { Evidence } from '../src/shared/types';
const evidence = (overrides: Partial<Evidence>): Evidence => ({
id: crypto.randomUUID(),
kind: 'c2pa',
strength: 'none',
verdict: 'unknown',
title: 'Evidence',
detail: 'Test evidence',
...overrides
});
describe('evidence precedence', () => {
it('treats invalid credentials as stronger than positive evidence', () => {
const items = [
evidence({ strength: 'verified', verdict: 'ai' }),
evidence({ strength: 'invalid', verdict: 'invalid' })
];
expect(strongestEvidence(items)).toBe('invalid');
expect(verdictFromEvidence(items)).toBe('invalid');
});
it('never turns empty evidence into a human-origin assertion', () => {
const result = createAssetResult('one', 'image', 'Unsigned image', []);
expect(result.verdict).toBe('unknown');
expect(result.strength).toBe('none');
});
});
describe('page aggregation', () => {
const unknownPage = createAssetResult('page', 'page', 'Page', []);
it('reports verified AI when a trusted AI credential is present', () => {
const image = createAssetResult('image', 'image', 'Image', [
evidence({ strength: 'verified', verdict: 'ai' })
]);
expect(aggregateSummary(unknownPage, [image], [], 1, 0).status).toBe('verified-ai');
});
it('reports mixed provenance when AI and non-AI assertions coexist', () => {
const ai = createAssetResult('ai', 'image', 'AI', [evidence({ strength: 'verified', verdict: 'ai' })]);
const camera = createAssetResult('camera', 'image', 'Camera', [
evidence({ strength: 'verified', verdict: 'not-ai-asserted' })
]);
expect(aggregateSummary(unknownPage, [ai, camera], [], 2, 0).status).toBe('mixed-provenance');
});
it('prioritises invalid credentials', () => {
const invalid = createAssetResult('bad', 'image', 'Invalid', [
evidence({ strength: 'invalid', verdict: 'invalid' })
]);
expect(aggregateSummary(unknownPage, [invalid], [], 1, 0).status).toBe('invalid-credential');
});
it('reports suspicion without asserting AI authorship when heuristic clues cross the threshold', () => {
const suspectedPage = createAssetResult('page', 'page', 'Page', [
evidence({ kind: 'heuristic', strength: 'clue', category: 'visual', score: 3 }),
evidence({ kind: 'heuristic', strength: 'clue', category: 'layout', score: 2 })
]);
const summary = aggregateSummary(suspectedPage, [], [], 0, 0, 6);
expect(summary.status).toBe('heuristic-suspected');
expect(suspectedPage.verdict).toBe('unknown');
expect(summary.heuristicScore).toBe(6);
});
it('does not elevate a single weak style clue', () => {
const page = createAssetResult('page', 'page', 'Page', [
evidence({ kind: 'heuristic', strength: 'clue', category: 'visual', score: 2 })
]);
expect(aggregateSummary(page, [], [], 0, 0, 6).status).toBe('no-supported-evidence');
});
});
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"skipLibCheck": true,
"types": ["firefox-webext-browser", "vitest/globals"]
},
"include": ["src", "tests", "scripts"]
}