diff --git a/CLAUDE.md b/CLAUDE.md index f1ef271..32dfd3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,20 +45,20 @@ npm run copy:profile-evaluator # Copy profile evaluator from ../profile-evaluat **`src/lib/types.ts`** — `ConformanceReport` extends `CrJson` with conformance-specific fields: `usedITL`, `usedTestCerts`, and `_conformanceToolVersion` (git metadata injected at build time). -**`src/lib/profileEvaluator.ts`** — Dynamically imports the profile evaluator WASM from `public/profile-evaluator/`. Used by `AssetProfilePage.svelte`. +**`src/lib/rubrics/`** — Client-side evaluator for YAML-authored C2PA asset rubrics. Ported from the Python reference at `../../c2pa/conformance/asset-rubrics`. See the Rubrics section below for details. **`src/lib/version.ts`** — Auto-generated before each build/dev start via `scripts/generate-version.js`. Do not edit manually. ### Routing -`App.svelte` handles navigation between three pages: +`App.svelte` handles navigation between two pages: - Main validation page (default) - Test Certificates (`CertificateManager.svelte`) -- Asset Profiles (`AssetProfilePage.svelte`) ### WASM Modules -Two WASM binaries are committed to `public/`: +One WASM binary is committed to `public/`: - `public/c2pa.wasm` — Official C2PA reader (copied from `@contentauth/c2pa-web` during `postinstall`) -- `public/profile-evaluator/` — Profile evaluator (from sibling `profile-evaluator-rs` repo, updated via `copy:profile-evaluator` script) + +(`public/profile-evaluator/` is a legacy directory — the profile-evaluator page was removed when Rubrics replaced it. Safe to delete when convenient.) ### Trust Lists - **C2PA Trust List**: Fetched at runtime from GitHub diff --git a/index.html b/index.html index 0745a78..b42a285 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,9 @@ C2PA Verify + + +
diff --git a/package-lock.json b/package-lock.json index 1fbccfb..ae21cbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,13 @@ "name": "c2pa-conformance-tool", "version": "0.1.0", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { + "@adobe/json-formula": "^2.0.0", "@contentauth/c2pa-web": "^0.6.1", "@peculiar/x509": "^1.14.3", - "highlight.js": "^11.11.1" + "highlight.js": "^11.11.1", + "yaml": "^2.8.3" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", @@ -48,6 +51,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@adobe/json-formula": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@adobe/json-formula/-/json-formula-2.0.0.tgz", + "integrity": "sha512-017RwSGD4ByL0ZVekSgsUDEX6wjlpxKsvMCFxuaP7eVtb3sUL6ROHZdTc2XQeB9xR8cTnWeaxESN8ULNuLrNmg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -4554,10 +4566,9 @@ "license": "MIT" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 7a03d41..398e3cf 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@tsconfig/svelte": "^5.0.2", "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.23", + "gh-pages": "^6.2.0", "happy-dom": "^20.4.0", "jsdom": "^27.4.0", "postcss": "^8.5.6", @@ -36,8 +37,10 @@ "vitest": "^4.0.18" }, "dependencies": { + "@adobe/json-formula": "^2.0.0", "@contentauth/c2pa-web": "^0.6.1", "@peculiar/x509": "^1.14.3", - "highlight.js": "^11.11.1" + "highlight.js": "^11.11.1", + "yaml": "^2.8.3" } } diff --git a/public/local-c2pa/c2pa_local.d.ts b/public/local-c2pa/c2pa_local.d.ts index 5053187..9d0ae76 100644 --- a/public/local-c2pa/c2pa_local.d.ts +++ b/public/local-c2pa/c2pa_local.d.ts @@ -10,23 +10,59 @@ export function init(): void; export function read_manifest_store(file_bytes: Uint8Array, format: string, settings_json?: string | null): Promise; +/** + * Inspect a detached (`.c2pa`) manifest store without an asset. + * + * Used when the user drops a sidecar without the matching asset. We feed + * the manifest bytes to `with_manifest_data_and_stream_async` paired with + * an empty stream. The signature, certificate chain, and JUMBF structure + * are validated normally; the asset-hash bindings will report + * `assertion.dataHash.mismatch` because there is no asset to bind to — + * that is expected, and callers should label this as an integrity-only + * inspection in the UI. + * + * * `manifest_bytes` - raw bytes of the `.c2pa` sidecar (JUMBF manifest store). + * * `settings_json` - trust settings (same shape as `read_manifest_store`). + */ +export function read_sidecar_integrity_only(manifest_bytes: Uint8Array, settings_json?: string | null): Promise; + +/** + * Validate a detached (`.c2pa`) manifest store against its referenced asset. + * + * This is the sidecar-with-asset case: the C2PA manifest lives in its own file + * (`manifest_bytes`) and the asset whose hash-bindings the manifest claims + * lives separately (`asset_bytes`). We feed both into c2pa-rs's + * `with_manifest_data_and_stream_async`, which evaluates the asset-hash + * assertions *against the actual asset bytes* — something we cannot do with + * the single-blob `read_manifest_store` path. + * + * * `manifest_bytes` - raw bytes of the `.c2pa` sidecar (JUMBF manifest store). + * * `asset_bytes` - raw bytes of the referenced asset. + * * `asset_format` - MIME type of the asset (e.g. "image/jpeg"). The + * sidecar's own format is always `application/c2pa` and the SDK infers that. + * * `settings_json` - trust settings (same shape as `read_manifest_store`). + */ +export function read_sidecar_manifest_store(manifest_bytes: Uint8Array, asset_bytes: Uint8Array, asset_format: string, settings_json?: string | null): Promise; + export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; - readonly get_version: () => [number, number]; readonly init: () => void; readonly read_manifest_store: (a: number, b: number, c: number, d: number, e: number, f: number) => any; - readonly wasm_bindgen__closure__destroy__h0667ea7aa0a255a7: (a: number, b: number) => void; - readonly wasm_bindgen__convert__closures_____invoke__hca39902318df4249: (a: number, b: number, c: any) => [number, number]; - readonly wasm_bindgen__convert__closures_____invoke__h2986024ebf27e018: (a: number, b: number, c: any, d: any) => void; - readonly wasm_bindgen__convert__closures_____invoke__hdd2e4c5eb311bd94: (a: number, b: number) => void; + readonly read_sidecar_manifest_store: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => any; + readonly read_sidecar_integrity_only: (a: number, b: number, c: number, d: number) => any; + readonly get_version: () => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h1aa715c00eec31bd: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h0ef758fe9ac3980a: (a: number, b: number, c: any, d: any) => void; + readonly wasm_bindgen__convert__closures_____invoke__h2b3ed67cbe4afe0c: (a: number, b: number) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_exn_store: (a: number) => void; readonly __externref_table_alloc: () => number; readonly __wbindgen_externrefs: WebAssembly.Table; readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_destroy_closure: (a: number, b: number) => void; readonly __externref_table_dealloc: (a: number) => void; readonly __wbindgen_start: () => void; } diff --git a/public/local-c2pa/c2pa_local.js b/public/local-c2pa/c2pa_local.js index 1c27fab..c435806 100644 --- a/public/local-c2pa/c2pa_local.js +++ b/public/local-c2pa/c2pa_local.js @@ -38,30 +38,89 @@ export function read_manifest_store(file_bytes, format, settings_json) { return ret; } +/** + * Inspect a detached (`.c2pa`) manifest store without an asset. + * + * Used when the user drops a sidecar without the matching asset. We feed + * the manifest bytes to `with_manifest_data_and_stream_async` paired with + * an empty stream. The signature, certificate chain, and JUMBF structure + * are validated normally; the asset-hash bindings will report + * `assertion.dataHash.mismatch` because there is no asset to bind to — + * that is expected, and callers should label this as an integrity-only + * inspection in the UI. + * + * * `manifest_bytes` - raw bytes of the `.c2pa` sidecar (JUMBF manifest store). + * * `settings_json` - trust settings (same shape as `read_manifest_store`). + * @param {Uint8Array} manifest_bytes + * @param {string | null} [settings_json] + * @returns {Promise} + */ +export function read_sidecar_integrity_only(manifest_bytes, settings_json) { + const ptr0 = passArray8ToWasm0(manifest_bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(settings_json) ? 0 : passStringToWasm0(settings_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + const ret = wasm.read_sidecar_integrity_only(ptr0, len0, ptr1, len1); + return ret; +} + +/** + * Validate a detached (`.c2pa`) manifest store against its referenced asset. + * + * This is the sidecar-with-asset case: the C2PA manifest lives in its own file + * (`manifest_bytes`) and the asset whose hash-bindings the manifest claims + * lives separately (`asset_bytes`). We feed both into c2pa-rs's + * `with_manifest_data_and_stream_async`, which evaluates the asset-hash + * assertions *against the actual asset bytes* — something we cannot do with + * the single-blob `read_manifest_store` path. + * + * * `manifest_bytes` - raw bytes of the `.c2pa` sidecar (JUMBF manifest store). + * * `asset_bytes` - raw bytes of the referenced asset. + * * `asset_format` - MIME type of the asset (e.g. "image/jpeg"). The + * sidecar's own format is always `application/c2pa` and the SDK infers that. + * * `settings_json` - trust settings (same shape as `read_manifest_store`). + * @param {Uint8Array} manifest_bytes + * @param {Uint8Array} asset_bytes + * @param {string} asset_format + * @param {string | null} [settings_json] + * @returns {Promise} + */ +export function read_sidecar_manifest_store(manifest_bytes, asset_bytes, asset_format, settings_json) { + const ptr0 = passArray8ToWasm0(manifest_bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(asset_bytes, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passStringToWasm0(asset_format, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len2 = WASM_VECTOR_LEN; + var ptr3 = isLikeNone(settings_json) ? 0 : passStringToWasm0(settings_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len3 = WASM_VECTOR_LEN; + const ret = wasm.read_sidecar_manifest_store(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3); + return ret; +} function __wbg_get_imports() { const import0 = { __proto__: null, - __wbg___wbindgen_debug_string_5398f5bb970e0daa: function(arg0, arg1) { + __wbg___wbindgen_debug_string_edece8177ad01481: function(arg0, arg1) { const ret = debugString(arg1); const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); }, - __wbg___wbindgen_is_function_3c846841762788c1: function(arg0) { + __wbg___wbindgen_is_function_5cd60d5cf78b4eef: function(arg0) { const ret = typeof(arg0) === 'function'; return ret; }, - __wbg___wbindgen_is_object_781bc9f159099513: function(arg0) { + __wbg___wbindgen_is_object_b4593df85baada48: function(arg0) { const val = arg0; const ret = typeof(val) === 'object' && val !== null; return ret; }, - __wbg___wbindgen_is_undefined_52709e72fb9f179c: function(arg0) { + __wbg___wbindgen_is_undefined_35bb9f4c7fd651d5: function(arg0) { const ret = arg0 === undefined; return ret; }, - __wbg___wbindgen_string_get_395e606bd0ee4427: function(arg0, arg1) { + __wbg___wbindgen_string_get_d109740c0d18f4d7: function(arg0, arg1) { const obj = arg1; const ret = typeof(obj) === 'string' ? obj : undefined; var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); @@ -69,38 +128,38 @@ function __wbg_get_imports() { getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); }, - __wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) { + __wbg___wbindgen_throw_9c31b086c2b26051: function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }, - __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) { + __wbg__wbg_cb_unref_3fa391f3fcdb55f8: function(arg0) { arg0._wbg_cb_unref(); }, - __wbg_abort_5ef96933660780b7: function(arg0) { - arg0.abort(); - }, - __wbg_abort_6479c2d794ebf2ee: function(arg0, arg1) { + __wbg_abort_89f7368e16055f5f: function(arg0, arg1) { arg0.abort(arg1); }, - __wbg_append_608dfb635ee8998f: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + __wbg_abort_b363e6285472a358: function(arg0) { + arg0.abort(); + }, + __wbg_append_263958599fd198c1: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { arg0.append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments); }, - __wbg_arrayBuffer_eb8e9ca620af2a19: function() { return handleError(function (arg0) { + __wbg_arrayBuffer_cb5d4748b5f3cad5: function() { return handleError(function (arg0) { const ret = arg0.arrayBuffer(); return ret; }, arguments); }, - __wbg_call_2d781c1f4d5c0ef8: function() { return handleError(function (arg0, arg1, arg2) { - const ret = arg0.call(arg1, arg2); + __wbg_call_13665d9f14390edc: function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); return ret; }, arguments); }, - __wbg_call_e133b57c9155d22c: function() { return handleError(function (arg0, arg1) { - const ret = arg0.call(arg1); + __wbg_call_dfde26266607c996: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); return ret; }, arguments); }, __wbg_clearTimeout_6b8d9a38b9263d65: function(arg0) { const ret = clearTimeout(arg0); return ret; }, - __wbg_done_08ce71ee07e3bd17: function(arg0) { + __wbg_done_54b8da57023b7ed2: function(arg0) { const ret = arg0.done; return ret; }, @@ -115,7 +174,7 @@ function __wbg_get_imports() { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } }, - __wbg_fetch_5550a88cf343aaa9: function(arg0, arg1) { + __wbg_fetch_2998af8c54e0997c: function(arg0, arg1) { const ret = arg0.fetch(arg1); return ret; }, @@ -126,23 +185,23 @@ function __wbg_get_imports() { __wbg_getRandomValues_3f44b700395062e5: function() { return handleError(function (arg0, arg1) { globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); }, arguments); }, - __wbg_getTime_1dad7b5386ddd2d9: function(arg0) { + __wbg_getTime_09f1dd40a44edb30: function(arg0) { const ret = arg0.getTime(); return ret; }, - __wbg_get_326e41e095fb2575: function() { return handleError(function (arg0, arg1) { + __wbg_get_3e9a707ab7d352eb: function() { return handleError(function (arg0, arg1) { const ret = Reflect.get(arg0, arg1); return ret; }, arguments); }, - __wbg_has_926ef2ff40b308cf: function() { return handleError(function (arg0, arg1) { + __wbg_has_ef192b1f278770eb: function() { return handleError(function (arg0, arg1) { const ret = Reflect.has(arg0, arg1); return ret; }, arguments); }, - __wbg_headers_eb2234545f9ff993: function(arg0) { + __wbg_headers_18f39f24d3837dc1: function(arg0) { const ret = arg0.headers; return ret; }, - __wbg_instanceof_Response_9b4d9fd451e051b1: function(arg0) { + __wbg_instanceof_Response_ecfc823e8fb354e2: function(arg0) { let result; try { result = arg0 instanceof Response; @@ -152,19 +211,19 @@ function __wbg_get_imports() { const ret = result; return ret; }, - __wbg_iterator_d8f549ec8fb061b1: function() { + __wbg_iterator_1441b47f341dc34f: function() { const ret = Symbol.iterator; return ret; }, - __wbg_length_ea16607d7b61445b: function(arg0) { + __wbg_length_56fcd3e2b7e0299d: function(arg0) { const ret = arg0.length; return ret; }, - __wbg_new_0837727332ac86ba: function() { return handleError(function () { - const ret = new Headers(); + __wbg_new_02d162bc6cf02f60: function() { + const ret = new Object(); return ret; - }, arguments); }, - __wbg_new_0_1dcafdf5e786e876: function() { + }, + __wbg_new_0_2722fcdb71a888a6: function() { const ret = new Date(); return ret; }, @@ -172,30 +231,30 @@ function __wbg_get_imports() { const ret = new Error(); return ret; }, - __wbg_new_5f486cdf45a04d78: function(arg0) { + __wbg_new_7ddec6de44ff8f5d: function(arg0) { const ret = new Uint8Array(arg0); return ret; }, - __wbg_new_ab79df5bd7c26067: function() { - const ret = new Object(); - return ret; - }, - __wbg_new_c518c60af666645b: function() { return handleError(function () { + __wbg_new_af86d8f14640f1f3: function() { return handleError(function () { const ret = new AbortController(); return ret; }, arguments); }, - __wbg_new_from_slice_22da9388ac046e50: function(arg0, arg1) { + __wbg_new_ee0be486d8f01282: function() { return handleError(function () { + const ret = new Headers(); + return ret; + }, arguments); }, + __wbg_new_from_slice_269e35316ed2d061: function(arg0, arg1) { const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); return ret; }, - __wbg_new_typed_aaaeaf29cf802876: function(arg0, arg1) { + __wbg_new_typed_c072c4ce9a2a0cdf: function(arg0, arg1) { try { var state0 = {a: arg0, b: arg1}; var cb0 = (arg0, arg1) => { const a = state0.a; state0.a = 0; try { - return wasm_bindgen__convert__closures_____invoke__h2986024ebf27e018(a, state0.b, arg0, arg1); + return wasm_bindgen__convert__closures_____invoke__h0ef758fe9ac3980a(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -203,36 +262,36 @@ function __wbg_get_imports() { const ret = new Promise(cb0); return ret; } finally { - state0.a = state0.b = 0; + state0.a = 0; } }, - __wbg_new_with_str_and_init_b4b54d1a819bc724: function() { return handleError(function (arg0, arg1, arg2) { + __wbg_new_with_str_and_init_ffe9977c986ea039: function() { return handleError(function (arg0, arg1, arg2) { const ret = new Request(getStringFromWasm0(arg0, arg1), arg2); return ret; }, arguments); }, - __wbg_next_11b99ee6237339e3: function() { return handleError(function (arg0) { - const ret = arg0.next(); - return ret; - }, arguments); }, - __wbg_next_e01a967809d1aa68: function(arg0) { + __wbg_next_2a4e19f4f5083b0f: function(arg0) { const ret = arg0.next; return ret; }, - __wbg_now_16f0c993d5dd6c27: function() { + __wbg_next_6429a146bf756f93: function() { return handleError(function (arg0) { + const ret = arg0.next(); + return ret; + }, arguments); }, + __wbg_now_81363d44c96dd239: function() { const ret = Date.now(); return ret; }, - __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) { + __wbg_prototypesetcall_5f9bdc8d75e07276: function(arg0, arg1, arg2) { Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); }, - __wbg_queueMicrotask_0c399741342fb10f: function(arg0) { + __wbg_queueMicrotask_78d584b53af520f5: function(arg0) { const ret = arg0.queueMicrotask; return ret; }, - __wbg_queueMicrotask_a082d78ce798393e: function(arg0) { + __wbg_queueMicrotask_b39ea83c7f01971a: function(arg0) { queueMicrotask(arg0); }, - __wbg_resolve_ae8d83246e5bcc12: function(arg0) { + __wbg_resolve_d17db9352f5a220e: function(arg0) { const ret = Promise.resolve(arg0); return ret; }, @@ -240,28 +299,28 @@ function __wbg_get_imports() { const ret = setTimeout(arg0, arg1); return ret; }, - __wbg_set_body_a3d856b097dfda04: function(arg0, arg1) { + __wbg_set_body_7f56457720e81672: function(arg0, arg1) { arg0.body = arg1; }, - __wbg_set_cache_ec7e430c6056ebda: function(arg0, arg1) { + __wbg_set_cache_9ed01a3813d96de2: function(arg0, arg1) { arg0.cache = __wbindgen_enum_RequestCache[arg1]; }, - __wbg_set_credentials_ed63183445882c65: function(arg0, arg1) { + __wbg_set_credentials_55b92faec8dcc6a4: function(arg0, arg1) { arg0.credentials = __wbindgen_enum_RequestCredentials[arg1]; }, - __wbg_set_headers_3c8fecc693b75327: function(arg0, arg1) { + __wbg_set_headers_97ed66619adb1e3e: function(arg0, arg1) { arg0.headers = arg1; }, - __wbg_set_method_8c015e8bcafd7be1: function(arg0, arg1, arg2) { + __wbg_set_method_4d69a1a7e34c0aca: function(arg0, arg1, arg2) { arg0.method = getStringFromWasm0(arg1, arg2); }, - __wbg_set_mode_5a87f2c809cf37c2: function(arg0, arg1) { + __wbg_set_mode_dfc59bbbe25b1d14: function(arg0, arg1) { arg0.mode = __wbindgen_enum_RequestMode[arg1]; }, - __wbg_set_signal_0cebecb698f25d21: function(arg0, arg1) { + __wbg_set_signal_2a5bd3615938edbc: function(arg0, arg1) { arg0.signal = arg1; }, - __wbg_signal_166e1da31adcac18: function(arg0) { + __wbg_signal_304beac95c8c5ea0: function(arg0) { const ret = arg0.signal; return ret; }, @@ -272,57 +331,57 @@ function __wbg_get_imports() { getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); }, - __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { - const ret = typeof global === 'undefined' ? null : global; + __wbg_static_accessor_GLOBAL_THIS_02344c9b09eb08a9: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); }, - __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { - const ret = typeof globalThis === 'undefined' ? null : globalThis; + __wbg_static_accessor_GLOBAL_ac6d4ac874d5cd54: function() { + const ret = typeof global === 'undefined' ? null : global; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); }, - __wbg_static_accessor_SELF_f207c857566db248: function() { + __wbg_static_accessor_SELF_9b2406c23aeb2023: function() { const ret = typeof self === 'undefined' ? null : self; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); }, - __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { + __wbg_static_accessor_WINDOW_b34d2126934e16ba: function() { const ret = typeof window === 'undefined' ? null : window; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); }, - __wbg_status_318629ab93a22955: function(arg0) { + __wbg_status_0853c9f5752c7ee2: function(arg0) { const ret = arg0.status; return ret; }, - __wbg_stringify_5ae93966a84901ac: function() { return handleError(function (arg0) { + __wbg_stringify_ef0c105b1ccc3849: function() { return handleError(function (arg0) { const ret = JSON.stringify(arg0); return ret; }, arguments); }, - __wbg_then_098abe61755d12f6: function(arg0, arg1) { + __wbg_then_837494e384b37459: function(arg0, arg1) { const ret = arg0.then(arg1); return ret; }, - __wbg_then_9e335f6dd892bc11: function(arg0, arg1, arg2) { + __wbg_then_bd927500e8905df2: function(arg0, arg1, arg2) { const ret = arg0.then(arg1, arg2); return ret; }, - __wbg_url_7fefc1820fba4e0c: function(arg0, arg1) { + __wbg_url_1a5ea6a8a7f22ff8: function(arg0, arg1) { const ret = arg1.url; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); }, - __wbg_value_21fc78aab0322612: function(arg0) { + __wbg_value_9cc0518af87a489c: function(arg0) { const ret = arg0.value; return ret; }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 188, function: Function { arguments: [Externref], shim_idx: 299, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h0667ea7aa0a255a7, wasm_bindgen__convert__closures_____invoke__hca39902318df4249); + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 299, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h1aa715c00eec31bd); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 188, function: Function { arguments: [], shim_idx: 433, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h0667ea7aa0a255a7, wasm_bindgen__convert__closures_____invoke__hdd2e4c5eb311bd94); + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 440, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b3ed67cbe4afe0c); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { @@ -346,19 +405,19 @@ function __wbg_get_imports() { }; } -function wasm_bindgen__convert__closures_____invoke__hdd2e4c5eb311bd94(arg0, arg1) { - wasm.wasm_bindgen__convert__closures_____invoke__hdd2e4c5eb311bd94(arg0, arg1); +function wasm_bindgen__convert__closures_____invoke__h2b3ed67cbe4afe0c(arg0, arg1) { + wasm.wasm_bindgen__convert__closures_____invoke__h2b3ed67cbe4afe0c(arg0, arg1); } -function wasm_bindgen__convert__closures_____invoke__hca39902318df4249(arg0, arg1, arg2) { - const ret = wasm.wasm_bindgen__convert__closures_____invoke__hca39902318df4249(arg0, arg1, arg2); +function wasm_bindgen__convert__closures_____invoke__h1aa715c00eec31bd(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h1aa715c00eec31bd(arg0, arg1, arg2); if (ret[1]) { throw takeFromExternrefTable0(ret[0]); } } -function wasm_bindgen__convert__closures_____invoke__h2986024ebf27e018(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures_____invoke__h2986024ebf27e018(arg0, arg1, arg2, arg3); +function wasm_bindgen__convert__closures_____invoke__h0ef758fe9ac3980a(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures_____invoke__h0ef758fe9ac3980a(arg0, arg1, arg2, arg3); } @@ -378,7 +437,7 @@ function addToExternrefTable0(obj) { const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(state => state.dtor(state.a, state.b)); + : new FinalizationRegistry(state => wasm.__wbindgen_destroy_closure(state.a, state.b)); function debugString(val) { // primitive types @@ -459,8 +518,7 @@ function getDataViewMemory0() { } function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return decodeText(ptr, len); + return decodeText(ptr >>> 0, len); } let cachedUint8ArrayMemory0 = null; @@ -484,8 +542,8 @@ function isLikeNone(x) { return x === undefined || x === null; } -function makeMutClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; +function makeMutClosure(arg0, arg1, f) { + const state = { a: arg0, b: arg1, cnt: 1 }; const real = (...args) => { // First up with a closure we increment the internal reference @@ -503,7 +561,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { }; real._wbg_cb_unref = () => { if (--state.cnt === 0) { - state.dtor(state.a, state.b); + wasm.__wbindgen_destroy_closure(state.a, state.b); state.a = 0; CLOSURE_DTORS.unregister(state); } @@ -591,8 +649,9 @@ if (!('encodeInto' in cachedTextEncoder)) { let WASM_VECTOR_LEN = 0; -let wasmModule, wasm; +let wasmModule, wasmInstance, wasm; function __wbg_finalize_init(instance, module) { + wasmInstance = instance; wasm = instance.exports; wasmModule = module; cachedDataViewMemory0 = null; diff --git a/public/local-c2pa/c2pa_local_bg.wasm b/public/local-c2pa/c2pa_local_bg.wasm index 8e2e3cb..0e8222e 100644 Binary files a/public/local-c2pa/c2pa_local_bg.wasm and b/public/local-c2pa/c2pa_local_bg.wasm differ diff --git a/public/local-c2pa/c2pa_local_bg.wasm.d.ts b/public/local-c2pa/c2pa_local_bg.wasm.d.ts index 666cdb8..a9559a3 100644 --- a/public/local-c2pa/c2pa_local_bg.wasm.d.ts +++ b/public/local-c2pa/c2pa_local_bg.wasm.d.ts @@ -1,18 +1,20 @@ /* tslint:disable */ /* eslint-disable */ export const memory: WebAssembly.Memory; -export const get_version: () => [number, number]; export const init: () => void; export const read_manifest_store: (a: number, b: number, c: number, d: number, e: number, f: number) => any; -export const wasm_bindgen__closure__destroy__h0667ea7aa0a255a7: (a: number, b: number) => void; -export const wasm_bindgen__convert__closures_____invoke__hca39902318df4249: (a: number, b: number, c: any) => [number, number]; -export const wasm_bindgen__convert__closures_____invoke__h2986024ebf27e018: (a: number, b: number, c: any, d: any) => void; -export const wasm_bindgen__convert__closures_____invoke__hdd2e4c5eb311bd94: (a: number, b: number) => void; +export const read_sidecar_manifest_store: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => any; +export const read_sidecar_integrity_only: (a: number, b: number, c: number, d: number) => any; +export const get_version: () => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h1aa715c00eec31bd: (a: number, b: number, c: any) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h0ef758fe9ac3980a: (a: number, b: number, c: any, d: any) => void; +export const wasm_bindgen__convert__closures_____invoke__h2b3ed67cbe4afe0c: (a: number, b: number) => void; export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_exn_store: (a: number) => void; export const __externref_table_alloc: () => number; export const __wbindgen_externrefs: WebAssembly.Table; export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_destroy_closure: (a: number, b: number) => void; export const __externref_table_dealloc: (a: number) => void; export const __wbindgen_start: () => void; diff --git a/public/rubrics/asset-rubric-conformance0.1-spec2.2.yml b/public/rubrics/asset-rubric-conformance0.1-spec2.2.yml new file mode 100644 index 0000000..35cbb4f --- /dev/null +++ b/public/rubrics/asset-rubric-conformance0.1-spec2.2.yml @@ -0,0 +1,305 @@ +rubric_metadata: + name: C2PA Asset Conformance 0.1 Spec 2.2 Rubric + issuer: C2PA Conformance Task Force + date: '2026-03-31T05:00:00Z' + version: 0.1.0 + language: en +variables: + $well_formed_error_codes: + - assertion.action.malformed + - assertion.bmffHash.malformed + - assertion.boxesHash.malformed + - assertion.collectionHash.malformed + - assertion.dataHash.malformed + - assertion.external-reference.malformed + - assertion.ingredient.malformed + - assertion.alternativeContentRepresentation.malformed + - assertion.multiAssetHash.malformed + - assertion.cbor.invalid + - assertion.json.invalid + - claim.cbor.invalid + - claim.hardBindings.missing + - claim.malformed + - claim.missing + - claim.multiple + - hashedURI.missing + - ingredient.manifest.missing + - assertion.boxesHash.unknownBox + - assertion.multipleHardBindings + - manifest.compressed.invalid + - manifest.html.multipleManifests + - manifest.inaccessible + - manifest.multipleParents + - manifest.structuredText.emptyReference + - manifest.structuredText.malformedReference + - manifest.structuredText.multipleReferences + - manifest.structuredText.noManifest + - manifest.structuredText.noResolutionPath + - manifest.timestamp.invalid + - manifest.timestamp.wrongParents + - manifest.update.invalid + - manifest.update.wrongParents + - MISSING_VALIDATION_RESULTS + $valid_error_codes: + - assertion.action.ingredientMismatch + - assertion.action.redactionMismatch + - assertion.bmffHash.mismatch + - assertion.boxesHash.mismatch + - assertion.collectionHash.mismatch + - assertion.dataHash.mismatch + - assertion.external-reference.hashMismatch + - assertion.alternativeContentRepresentation.hashMismatch + - assertion.multiAssetHash.mismatch + - assertion.hashedURI.mismatch + - hashedURI.mismatch + - ingredient.manifest.mismatch + - ingredient.claimSignature.missing + - ingredient.claimSignature.mismatch + - claimSignature.missing + - MISSING_VALIDATION_RESULTS + $trust_error_codes: + - claimSignature.mismatch + - claimSignature.outsideValidity + - signingCredential.invalid + - signingCredential.ocsp.revoked + - signingCredential.untrusted + - MISSING_VALIDATION_RESULTS + $deprecated_assertion_labels: + - c2pa.claim + - c2pa.hash.bmff.v2 + - c2pa.asset-type + - c2pa.ingredient + - c2pa.ingredient.v2 + - c2pa.actions + $hard_binding_assertion_labels: + - c2pa.hash.data + - c2pa.hash.boxes + - c2pa.hash.bmff.v2 + - c2pa.hash.bmff.v3 + - c2pa.hash.collection.data + - c2pa.hash.multi-asset + $deprecated_action_labels: + - c2pa.copied + - c2pa.formatted + - c2pa.version_updated + - c2pa.printed + - c2pa.managed + - c2pa.produced + - c2pa.saved + - c2pa.color_adjustments + - c2pa.watermarked + $allowed_assertions_v22: + - c2pa.actions.v2 + - c2pa.ingredient.v3 + - c2pa.hash.boxes + - c2pa.hash.bmff.v3 + - c2pa.hash.collection.data + - c2pa.soft-binding + - c2pa.cloud-data + - c2pa.embedded-data + - c2pa.metadata + - c2pa.certificate-status + - c2pa.asset-ref + - c2pa.asset-type.v2 + - c2pa.hash.multi-asset + - c2pa.thumbnail.claim + - c2pa.thumbnail.ingredient + - c2pa.depthmap.GDepth + - c2pa.icon + - c2pa.assertion.metadata + - c2pa.time-stamp + $valid_relationship_values: + - parentOf + - inputTo + - componentOf + $human_entry_source_types: + - humanEntry.anonymous + - humanEntry.credentialed +expressions: + _manifest_validationResults: '($arg0.validationResults || { failure: [{code: "MISSING_VALIDATION_RESULTS"}] })' + _manifest_assertionKeys: keys($arg0.assertions || `{}`) + _manifest_assertionValues: values($arg0.assertions || `{}`) + _manifest_actionsV2: $arg0.assertions.'c2pa.actions.v2'.actions || `[]` + _manifest_createdAssertions: $arg0.'claim.v2'.created_assertions || `[]` + _manifest_ingredientAssertions: values($arg0.assertions || `{}`)[? relationship != null] + _activeManifest: manifests[0] + _validationResults: _manifest_validationResults(_activeManifest()) + _assertionKeys: _manifest_assertionKeys(_activeManifest()) + _assertionValues: _manifest_assertionValues(_activeManifest()) + _actionsV2: _manifest_actionsV2(_activeManifest()) + _createdAssertions: _manifest_createdAssertions(_activeManifest()) + _ingredientAssertions: _manifest_ingredientAssertions(_activeManifest()) + +--- + +- id: validation:no_dst_for_opened_action + description: Check that c2pa.opened actions do not have digitalSourceType + expression: | + length(_actionsV2()[?action == "c2pa.opened" && digitalSourceType != null()]) == 0 + reportText: + 'true': + en: No c2pa.opened actions have a digitalSourceType + 'false': + en: Found c2pa.opened actions with a digitalSourceType +- id: validation:well_formed_data_present + description: Check if validation results are present for structural analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for structural analysis + 'false': + en: Missing validation results in the manifest +- id: validation:well_formed_success + description: |- + Check if the asset has any structural or malformation failures (from the well-formed list) + failIfMatched: true + expression: '_validationResults().failure[?contains($well_formed_error_codes, code)].code + + ' + reportText: + 'true': + en: No structural failures found + 'false': + en: 'Found structural failures: {{matches}}' +- id: validation:valid_data_present + description: Check if validation results are present for integrity analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for integrity analysis + 'false': + en: Missing validation results in the manifest +- id: validation:valid_success + description: Check if the asset has any validation mismatches (from the valid list) + failIfMatched: true + expression: '_validationResults().failure[?contains($valid_error_codes, code)].code + + ' + reportText: + 'true': + en: No validation mismatches found + 'false': + en: 'Found validation mismatches: {{matches}}' +- id: validation:trusted_data_present + description: Check if validation results are present for trust analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for trust analysis + 'false': + en: Missing validation results in the manifest +- id: validation:trusted_success + description: Check if the asset has any trust failures (from the trusted list) + failIfMatched: true + expression: '_validationResults().failure[?contains($trust_error_codes, code)].code + + ' + reportText: + 'true': + en: Asset is trusted + 'false': + en: 'Found trust failures: {{matches}}' +- id: validation:active_manifest_urn + description: Check if the active manifest URN uses 'urn:c2pa:' prefix (v2.2+ requirement) + expression: 'startsWith(notNull(manifests[0].label, ""), "urn:c2pa:") + + ' + reportText: + 'true': + en: Active manifest URN uses standard 'urn:c2pa:' prefix + 'false': + en: Active manifest URN uses old 'urn:uuid' prefix (pre-v2.2) +- id: validation:no_deprecated_assertions + description: Check for known deprecated assertion labels (e.g. from v2.0 or earlier specs) + failIfMatched: true + expression: | + _assertionKeys()[?startsWith(@, "stds.") || contains($deprecated_assertion_labels, @)] + reportText: + 'true': + en: No deprecated standard assertions found + 'false': + en: 'Found deprecated standard assertions: {{matches}}' +- id: validation:no_unsupported_assertions + description: Check if all C2PA standard assertions are in the allow-list for Spec 2.2 + failIfMatched: true + expression: | + _assertionKeys()[?startsWith(@, "c2pa.") && + !startsWith(@, "c2pa.hash.data") && + !contains($allowed_assertions_v22, @) && + !(contains(@, "__") && contains(startsWith(@, $allowed_assertions_v22), `true`)) && + !contains($deprecated_assertion_labels, @)] + reportText: + 'true': + en: All standard assertions are supported for Spec 2.2 + 'false': + en: 'Found unsupported or unknown standard C2PA assertions: {{matches}}' +- id: validation:no_deprecated_actions + description: Check for deprecated action types in c2pa.actions.v2 + expression: 'length(_actionsV2()[?contains($deprecated_action_labels, action)]) == 0 + + ' + reportText: + 'true': + en: No deprecated actions used + 'false': + en: Found deprecated actions (e.g. c2pa.copied, c2pa.printed, etc.) +- id: validation:review_ratings_datasource + description: Check that reviewRatings is absent when dataSource type is human entry + expression: | + length(_assertionValues()[? reviewRatings != null && contains($human_entry_source_types, dataSource.type)]) == 0 + reportText: + 'true': + en: No invalid co-occurrence of reviewRatings and human entry dataSource + 'false': + en: Found reviewRatings in an assertion with human entry dataSource +- id: validation:ingredient_v3_no_active_manifest + description: Check that ingredient v3 has no validationResults if no activeManifest + expression: | + length(_ingredientAssertions()[? activeManifest == null && validationResults != null]) == 0 + reportText: + 'true': + en: Ingredient v3 assertions without activeManifest correctly omit validationResults + 'false': + en: |- + Found ingredient v3 assertion without activeManifest but containing validationResults +- id: validation:ingredient_relationship_values + description: Check that the relationship field in all ingredient assertions is valid + expression: | + length(_ingredientAssertions()[? !contains($valid_relationship_values, relationship)]) == 0 + reportText: + 'true': + en: All ingredient assertions have valid relationship values + 'false': + en: Found ingredient assertion with invalid relationship value +- id: validation:update_manifest_constraints + description: |- + Check constraints for Update Manifests (no thumbnail, exactly one parentOf ingredient) + expression: |- + length(_assertionKeys()[?contains($hard_binding_assertion_labels, @)]) > `0` || length(_ingredientAssertions()) == `0` || (manifests[0].assertions.'c2pa.thumbnail.claim' == null && length(_ingredientAssertions()[? relationship == "parentOf"]) == `1`) + reportText: + 'true': + en: |- + Update manifest constraints are satisfied (or manifest is not an update manifest) + 'false': + en: |- + Update manifest violates constraints (has thumbnail or does not have exactly one parentOf ingredient) +- id: validation:inception_action_position + description: |- + Check if an inception action is the first action in the first actions assertion in created_assertions + expression: | + endsWith(notNull(_createdAssertions()[?contains(url, "c2pa.actions")] | [0].url, ""), "c2pa.actions.v2") && + (length(_actionsV2()[?action == "c2pa.created"]) == 0 || + contains(`["c2pa.created", "c2pa.opened"]`, _actionsV2()[0].action)) + reportText: + 'true': + en: |- + Inception action is correctly positioned as the first item in the first created actions assertion + 'false': + en: |- + Inception action is not in the first actions assertion of created_assertions (or first item is not an actions assertion) diff --git a/public/rubrics/asset-rubric-conformance0.2-spec2.2.yml b/public/rubrics/asset-rubric-conformance0.2-spec2.2.yml new file mode 100644 index 0000000..ea87117 --- /dev/null +++ b/public/rubrics/asset-rubric-conformance0.2-spec2.2.yml @@ -0,0 +1,317 @@ +rubric_metadata: + name: C2PA Asset Conformance 0.2 Spec 2.2 Rubric + issuer: C2PA Conformance Task Force + date: '2026-03-31T05:00:00Z' + version: 0.2.0 + language: en +variables: + $well_formed_error_codes: + - assertion.action.malformed + - assertion.bmffHash.malformed + - assertion.boxesHash.malformed + - assertion.collectionHash.malformed + - assertion.dataHash.malformed + - assertion.external-reference.malformed + - assertion.ingredient.malformed + - assertion.alternativeContentRepresentation.malformed + - assertion.multiAssetHash.malformed + - assertion.cbor.invalid + - assertion.json.invalid + - claim.cbor.invalid + - claim.hardBindings.missing + - claim.malformed + - claim.missing + - claim.multiple + - hashedURI.missing + - ingredient.manifest.missing + - assertion.boxesHash.unknownBox + - assertion.multipleHardBindings + - manifest.compressed.invalid + - manifest.html.multipleManifests + - manifest.inaccessible + - manifest.multipleParents + - manifest.structuredText.emptyReference + - manifest.structuredText.malformedReference + - manifest.structuredText.multipleReferences + - manifest.structuredText.noManifest + - manifest.structuredText.noResolutionPath + - manifest.timestamp.invalid + - manifest.timestamp.wrongParents + - manifest.update.invalid + - manifest.update.wrongParents + - MISSING_VALIDATION_RESULTS + $valid_error_codes: + - assertion.action.ingredientMismatch + - assertion.action.redactionMismatch + - assertion.bmffHash.mismatch + - assertion.boxesHash.mismatch + - assertion.collectionHash.mismatch + - assertion.dataHash.mismatch + - assertion.external-reference.hashMismatch + - assertion.alternativeContentRepresentation.hashMismatch + - assertion.multiAssetHash.mismatch + - assertion.hashedURI.mismatch + - hashedURI.mismatch + - ingredient.manifest.mismatch + - ingredient.claimSignature.missing + - ingredient.claimSignature.mismatch + - claimSignature.missing + - MISSING_VALIDATION_RESULTS + $trust_error_codes: + - claimSignature.mismatch + - claimSignature.outsideValidity + - signingCredential.invalid + - signingCredential.ocsp.revoked + - signingCredential.untrusted + - MISSING_VALIDATION_RESULTS + $deprecated_assertion_labels: + - c2pa.claim + - c2pa.hash.bmff.v2 + - c2pa.asset-type + - c2pa.ingredient + - c2pa.ingredient.v2 + - c2pa.actions + $hard_binding_assertion_labels: + - c2pa.hash.data + - c2pa.hash.boxes + - c2pa.hash.bmff.v2 + - c2pa.hash.bmff.v3 + - c2pa.hash.collection.data + - c2pa.hash.multi-asset + $deprecated_action_labels: + - c2pa.copied + - c2pa.formatted + - c2pa.version_updated + - c2pa.printed + - c2pa.managed + - c2pa.produced + - c2pa.saved + - c2pa.color_adjustments + - c2pa.watermarked + $allowed_assertions_v22: + - c2pa.actions.v2 + - c2pa.ingredient.v3 + - c2pa.hash.boxes + - c2pa.hash.bmff.v3 + - c2pa.hash.collection.data + - c2pa.soft-binding + - c2pa.cloud-data + - c2pa.embedded-data + - c2pa.metadata + - c2pa.certificate-status + - c2pa.asset-ref + - c2pa.asset-type.v2 + - c2pa.hash.multi-asset + - c2pa.thumbnail.claim + - c2pa.thumbnail.ingredient + - c2pa.depthmap.GDepth + - c2pa.icon + - c2pa.assertion.metadata + - c2pa.time-stamp + $valid_relationship_values: + - parentOf + - inputTo + - componentOf + $human_entry_source_types: + - humanEntry.anonymous + - humanEntry.credentialed +expressions: + _manifest_validationResults: '($arg0.validationResults || { failure: [{code: "MISSING_VALIDATION_RESULTS"}] })' + _manifest_assertionKeys: keys($arg0.assertions || `{}`) + _manifest_assertionValues: values($arg0.assertions || `{}`) + _manifest_actionsV2: $arg0.assertions.'c2pa.actions.v2'.actions || `[]` + _manifest_claimV2: $arg0.'claim.v2' + _manifest_createdAssertions: $arg0.'claim.v2'.created_assertions || `[]` + _manifest_ingredientAssertions: values($arg0.assertions || `{}`)[? relationship != null] + _activeManifest: manifests[0] + _validationResults: _manifest_validationResults(_activeManifest()) + _assertionKeys: _manifest_assertionKeys(_activeManifest()) + _assertionValues: _manifest_assertionValues(_activeManifest()) + _actionsV2: _manifest_actionsV2(_activeManifest()) + _claimV2: _manifest_claimV2(_activeManifest()) + _createdAssertions: _manifest_createdAssertions(_activeManifest()) + _ingredientAssertions: _manifest_ingredientAssertions(_activeManifest()) + +--- + +- id: validation:no_dst_for_opened_action + description: Check that c2pa.opened actions do not have digitalSourceType + expression: | + length(_actionsV2()[?action == "c2pa.opened" && digitalSourceType != null()]) == 0 + reportText: + 'true': + en: No c2pa.opened actions have a digitalSourceType + 'false': + en: Found c2pa.opened actions with a digitalSourceType +- id: validation:mandatory_spec_version + description: Check if specVersion is present in claim_generator_info (mandatory since v0.2) + expression: '_claimV2().claim_generator_info.specVersion != null + + ' + reportText: + 'true': + en: Found 'specVersion' in claim_generator_info + 'false': + en: Missing mandatory 'specVersion' in claim_generator_info +- id: validation:well_formed_data_present + description: Check if validation results are present for structural analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for structural analysis + 'false': + en: Missing validation results in the manifest +- id: validation:well_formed_success + description: |- + Check if the asset has any structural or malformation failures (from the well-formed list) + failIfMatched: true + expression: '_validationResults().failure[?contains($well_formed_error_codes, code)].code + + ' + reportText: + 'true': + en: No structural failures found + 'false': + en: 'Found structural failures: {{matches}}' +- id: validation:valid_data_present + description: Check if validation results are present for integrity analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for integrity analysis + 'false': + en: Missing validation results in the manifest +- id: validation:valid_success + description: Check if the asset has any validation mismatches (from the valid list) + failIfMatched: true + expression: '_validationResults().failure[?contains($valid_error_codes, code)].code + + ' + reportText: + 'true': + en: No validation mismatches found + 'false': + en: 'Found validation mismatches: {{matches}}' +- id: validation:trusted_data_present + description: Check if validation results are present for trust analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for trust analysis + 'false': + en: Missing validation results in the manifest +- id: validation:trusted_success + description: Check if the asset has any trust failures (from the trusted list) + failIfMatched: true + expression: '_validationResults().failure[?contains($trust_error_codes, code)].code + + ' + reportText: + 'true': + en: Asset is trusted + 'false': + en: 'Found trust failures: {{matches}}' +- id: validation:active_manifest_urn + description: Check if the active manifest URN uses 'urn:c2pa:' prefix (v2.2+ requirement) + expression: 'startsWith(notNull(manifests[0].label, ""), "urn:c2pa:") + + ' + reportText: + 'true': + en: Active manifest URN uses standard 'urn:c2pa:' prefix + 'false': + en: Active manifest URN uses old 'urn:uuid' prefix (pre-v2.2) +- id: validation:no_deprecated_assertions + description: Check for known deprecated assertion labels (e.g. from v2.0 or earlier specs) + failIfMatched: true + expression: | + _assertionKeys()[?startsWith(@, "stds.") || contains($deprecated_assertion_labels, @)] + reportText: + 'true': + en: No deprecated standard assertions found + 'false': + en: 'Found deprecated standard assertions: {{matches}}' +- id: validation:no_unsupported_assertions + description: Check if all C2PA standard assertions are in the allow-list for Spec 2.2 + failIfMatched: true + expression: | + _assertionKeys()[?startsWith(@, "c2pa.") && + !startsWith(@, "c2pa.hash.data") && + !contains($allowed_assertions_v22, @) && + !(contains(@, "__") && contains(startsWith(@, $allowed_assertions_v22), `true`)) && + !contains($deprecated_assertion_labels, @)] + reportText: + 'true': + en: All standard assertions are supported for Spec 2.2 + 'false': + en: 'Found unsupported or unknown standard C2PA assertions: {{matches}}' +- id: validation:no_deprecated_actions + description: Check for deprecated action types in c2pa.actions.v2 + expression: 'length(_actionsV2()[?contains($deprecated_action_labels, action)]) == 0 + + ' + reportText: + 'true': + en: No deprecated actions used + 'false': + en: Found deprecated actions (e.g. c2pa.copied, c2pa.printed, etc.) +- id: validation:review_ratings_datasource + description: Check that reviewRatings is absent when dataSource type is human entry + expression: | + length(_assertionValues()[? reviewRatings != null && contains($human_entry_source_types, dataSource.type)]) == 0 + reportText: + 'true': + en: No invalid co-occurrence of reviewRatings and human entry dataSource + 'false': + en: Found reviewRatings in an assertion with human entry dataSource +- id: validation:ingredient_v3_no_active_manifest + description: Check that ingredient v3 has no validationResults if no activeManifest + expression: | + length(_ingredientAssertions()[? activeManifest == null && validationResults != null]) == 0 + reportText: + 'true': + en: Ingredient v3 assertions without activeManifest correctly omit validationResults + 'false': + en: |- + Found ingredient v3 assertion without activeManifest but containing validationResults +- id: validation:ingredient_relationship_values + description: Check that the relationship field in all ingredient assertions is valid + expression: | + length(_ingredientAssertions()[? !contains($valid_relationship_values, relationship)]) == 0 + reportText: + 'true': + en: All ingredient assertions have valid relationship values + 'false': + en: Found ingredient assertion with invalid relationship value +- id: validation:update_manifest_constraints + description: |- + Check constraints for Update Manifests (no thumbnail, exactly one parentOf ingredient) + expression: |- + length(_assertionKeys()[?contains($hard_binding_assertion_labels, @)]) > `0` || length(_ingredientAssertions()) == `0` || (manifests[0].assertions.'c2pa.thumbnail.claim' == null && length(_ingredientAssertions()[? relationship == "parentOf"]) == `1`) + reportText: + 'true': + en: |- + Update manifest constraints are satisfied (or manifest is not an update manifest) + 'false': + en: |- + Update manifest violates constraints (has thumbnail or does not have exactly one parentOf ingredient) +- id: validation:inception_action_position + description: |- + Check if an inception action is the first action in the first actions assertion in created_assertions + expression: | + endsWith(notNull(_createdAssertions()[?contains(url, "c2pa.actions")] | [0].url, ""), "c2pa.actions.v2") && + (length(_actionsV2()[?action == "c2pa.created"]) == 0 || + contains(`["c2pa.created", "c2pa.opened"]`, _actionsV2()[0].action)) + reportText: + 'true': + en: |- + Inception action is correctly positioned as the first item in the first created actions assertion + 'false': + en: |- + Inception action is not in the first actions assertion of created_assertions (or first item is not an actions assertion) diff --git a/public/rubrics/asset-rubric-conformance0.2-spec2.4.yml b/public/rubrics/asset-rubric-conformance0.2-spec2.4.yml new file mode 100644 index 0000000..4019f0d --- /dev/null +++ b/public/rubrics/asset-rubric-conformance0.2-spec2.4.yml @@ -0,0 +1,416 @@ +rubric_metadata: + name: C2PA Asset Conformance 0.2 Spec 2.4 Rubric + issuer: C2PA Conformance Task Force + date: '2026-03-31T05:00:00Z' + version: 0.2.0 + language: en +variables: + $well_formed_error_codes: + - assertion.action.malformed + - assertion.bmffHash.malformed + - assertion.boxesHash.malformed + - assertion.collectionHash.malformed + - assertion.dataHash.malformed + - assertion.external-reference.malformed + - assertion.ingredient.malformed + - assertion.alternativeContentRepresentation.malformed + - assertion.multiAssetHash.malformed + - assertion.cbor.invalid + - assertion.json.invalid + - claim.cbor.invalid + - claim.hardBindings.missing + - claim.malformed + - claim.missing + - claim.multiple + - hashedURI.missing + - ingredient.manifest.missing + - assertion.boxesHash.unknownBox + - assertion.multipleHardBindings + - manifest.compressed.invalid + - manifest.html.multipleManifests + - manifest.inaccessible + - manifest.multipleParents + - manifest.structuredText.emptyReference + - manifest.structuredText.malformedReference + - manifest.structuredText.multipleReferences + - manifest.structuredText.noManifest + - manifest.structuredText.noResolutionPath + - manifest.timestamp.invalid + - manifest.timestamp.wrongParents + - manifest.update.invalid + - manifest.update.wrongParents + - MISSING_VALIDATION_RESULTS + $valid_error_codes: + - assertion.action.ingredientMismatch + - assertion.action.redactionMismatch + - assertion.bmffHash.mismatch + - assertion.boxesHash.mismatch + - assertion.collectionHash.mismatch + - assertion.dataHash.mismatch + - assertion.external-reference.hashMismatch + - assertion.alternativeContentRepresentation.hashMismatch + - assertion.multiAssetHash.mismatch + - assertion.hashedURI.mismatch + - hashedURI.mismatch + - ingredient.manifest.mismatch + - ingredient.claimSignature.missing + - ingredient.claimSignature.mismatch + - claimSignature.missing + - MISSING_VALIDATION_RESULTS + $trust_error_codes: + - claimSignature.mismatch + - claimSignature.outsideValidity + - signingCredential.invalid + - signingCredential.ocsp.revoked + - signingCredential.untrusted + - MISSING_VALIDATION_RESULTS + $deprecated_assertion_labels: + - c2pa.claim + - c2pa.hash.bmff.v2 + - c2pa.asset-type + - c2pa.ingredient + - c2pa.ingredient.v2 + - c2pa.actions + $hard_binding_assertion_labels: + - c2pa.hash.data + - c2pa.hash.boxes + - c2pa.hash.bmff.v2 + - c2pa.hash.bmff.v3 + - c2pa.hash.collection.data + - c2pa.hash.multi-asset + $deprecated_action_labels: + - c2pa.copied + - c2pa.formatted + - c2pa.version_updated + - c2pa.printed + - c2pa.managed + - c2pa.produced + - c2pa.saved + - c2pa.color_adjustments + - c2pa.watermarked + $allowed_assertions_v24: + - c2pa.actions.v2 + - c2pa.ingredient.v3 + - c2pa.hash.boxes + - c2pa.hash.bmff.v3 + - c2pa.hash.collection.data + - c2pa.soft-binding + - c2pa.cloud-data + - c2pa.embedded-data + - c2pa.metadata + - c2pa.certificate-status + - c2pa.asset-ref + - c2pa.asset-type.v2 + - c2pa.hash.multi-asset + - c2pa.thumbnail.claim + - c2pa.thumbnail.ingredient + - c2pa.depthmap.GDepth + - c2pa.icon + - c2pa.alternative-content-representation + - c2pa.external-reference + - c2pa.session-keys + - c2pa.environmental-sustainability + - c2pa.repository-receipt + - c2pa.ai-disclosure + - c2pa.assertion.metadata + - c2pa.time-stamp + $valid_relationship_values: + - parentOf + - inputTo + - componentOf + $human_entry_source_types: + - humanEntry.anonymous + - humanEntry.credentialed + $non_dst_required_actions: + - c2pa.opened + - c2pa.converted + - c2pa.published + - c2pa.repackaged + - c2pa.transcoded + - c2pa.resized.proportional + - c2pa.enhanced + - c2pa.edited.metadata + - c2pa.watermarked + - c2pa.watermarked.bound + - c2pa.watermarked.unbound + - c2pa.redacted +expressions: + _manifest_validationResults: '($arg0.validationResults || { failure: [{code: "MISSING_VALIDATION_RESULTS"}] })' + _manifest_assertionKeys: keys($arg0.assertions || `{}`) + _manifest_assertionValues: values($arg0.assertions || `{}`) + _manifest_actionsV2: $arg0.assertions.'c2pa.actions.v2'.actions || `[]` + _manifest_claimV2: $arg0.'claim.v2' + _manifest_createdAssertions: $arg0.'claim.v2'.created_assertions || `[]` + _manifest_ingredientAssertions: values($arg0.assertions || `{}`)[? relationship != null] + _activeManifest: manifests[0] + _validationResults: _manifest_validationResults(_activeManifest()) + _assertionKeys: _manifest_assertionKeys(_activeManifest()) + _assertionValues: _manifest_assertionValues(_activeManifest()) + _actionsV2: _manifest_actionsV2(_activeManifest()) + _claimV2: _manifest_claimV2(_activeManifest()) + _createdAssertions: _manifest_createdAssertions(_activeManifest()) + _ingredientAssertions: _manifest_ingredientAssertions(_activeManifest()) + +--- + +- id: validation:no_dst_for_opened_action + description: Check that c2pa.opened actions do not have digitalSourceType + expression: | + length(_actionsV2()[?action == "c2pa.opened" && digitalSourceType != null()]) == 0 + reportText: + 'true': + en: No c2pa.opened actions have a digitalSourceType + 'false': + en: Found c2pa.opened actions with a digitalSourceType +- id: validation:mandatory_spec_version + description: Check if specVersion is present in claim_generator_info (mandatory since v0.2) + expression: '_claimV2().claim_generator_info.specVersion != null + + ' + reportText: + 'true': + en: Found 'specVersion' in claim_generator_info + 'false': + en: Missing mandatory 'specVersion' in claim_generator_info +- id: validation:well_formed_data_present + description: Check if validation results are present for structural analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for structural analysis + 'false': + en: Missing validation results in the manifest +- id: validation:well_formed_success + description: |- + Check if the asset has any structural or malformation failures (from the well-formed list) + failIfMatched: true + expression: '_validationResults().failure[?contains($well_formed_error_codes, code)].code + + ' + reportText: + 'true': + en: No structural failures found + 'false': + en: 'Found structural failures: {{matches}}' +- id: validation:valid_data_present + description: Check if validation results are present for integrity analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for integrity analysis + 'false': + en: Missing validation results in the manifest +- id: validation:valid_success + description: Check if the asset has any validation mismatches (from the valid list) + failIfMatched: true + expression: '_validationResults().failure[?contains($valid_error_codes, code)].code + + ' + reportText: + 'true': + en: No validation mismatches found + 'false': + en: 'Found validation mismatches: {{matches}}' +- id: validation:trusted_data_present + description: Check if validation results are present for trust analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for trust analysis + 'false': + en: Missing validation results in the manifest +- id: validation:trusted_success + description: Check if the asset has any trust failures (from the trusted list) + failIfMatched: true + expression: '_validationResults().failure[?contains($trust_error_codes, code)].code + + ' + reportText: + 'true': + en: Asset is trusted + 'false': + en: 'Found trust failures: {{matches}}' +- id: validation:active_manifest_urn + description: Check if the active manifest URN uses 'urn:c2pa:' prefix (v2.2+ requirement) + expression: 'startsWith(notNull(manifests[0].label, ""), "urn:c2pa:") + + ' + reportText: + 'true': + en: Active manifest URN uses standard 'urn:c2pa:' prefix + 'false': + en: Active manifest URN uses old 'urn:uuid' prefix (pre-v2.2) +- id: validation:no_deprecated_assertions + description: Check for known deprecated assertion labels (e.g. from v2.0 or earlier specs) + failIfMatched: true + expression: | + _assertionKeys()[?startsWith(@, "stds.") || contains($deprecated_assertion_labels, @)] + reportText: + 'true': + en: No deprecated standard assertions found + 'false': + en: 'Found deprecated standard assertions: {{matches}}' +- id: validation:no_unsupported_assertions + description: Check if all C2PA standard assertions are in the allow-list for Spec 2.4 + failIfMatched: true + expression: | + _assertionKeys()[?startsWith(@, "c2pa.") && + !startsWith(@, "c2pa.hash.data") && + !contains($allowed_assertions_v24, @) && + !(contains(@, "__") && contains(startsWith(@, $allowed_assertions_v24), `true`)) && + !contains($deprecated_assertion_labels, @)] + reportText: + 'true': + en: All standard assertions are supported for Spec 2.4 + 'false': + en: 'Found unsupported or unknown standard C2PA assertions: {{matches}}' +- id: validation:no_deprecated_actions + description: Check for deprecated action types in c2pa.actions.v2 + expression: 'length(_actionsV2()[?contains($deprecated_action_labels, action)]) == 0 + + ' + reportText: + 'true': + en: No deprecated actions used + 'false': + en: Found deprecated actions (e.g. c2pa.copied, c2pa.printed, etc.) +- id: validation:review_ratings_datasource + description: Check that reviewRatings is absent when dataSource type is human entry + expression: | + length(_assertionValues()[? reviewRatings != null && contains($human_entry_source_types, dataSource.type)]) == 0 + reportText: + 'true': + en: No invalid co-occurrence of reviewRatings and human entry dataSource + 'false': + en: Found reviewRatings in an assertion with human entry dataSource +- id: validation:ingredient_v3_no_active_manifest + description: Check that ingredient v3 has no validationResults if no activeManifest + expression: | + length(_ingredientAssertions()[? activeManifest == null && validationResults != null]) == 0 + reportText: + 'true': + en: Ingredient v3 assertions without activeManifest correctly omit validationResults + 'false': + en: |- + Found ingredient v3 assertion without activeManifest but containing validationResults +- id: validation:ingredient_relationship_values + description: Check that the relationship field in all ingredient assertions is valid + expression: | + length(_ingredientAssertions()[? !contains($valid_relationship_values, relationship)]) == 0 + reportText: + 'true': + en: All ingredient assertions have valid relationship values + 'false': + en: Found ingredient assertion with invalid relationship value +- id: validation:update_manifest_constraints + description: |- + Check constraints for Update Manifests (no thumbnail, exactly one parentOf ingredient) + expression: |- + length(_assertionKeys()[?contains($hard_binding_assertion_labels, @)]) > `0` || length(_ingredientAssertions()) == `0` || (manifests[0].assertions.'c2pa.thumbnail.claim' == null && length(_ingredientAssertions()[? relationship == "parentOf"]) == `1`) + reportText: + 'true': + en: |- + Update manifest constraints are satisfied (or manifest is not an update manifest) + 'false': + en: |- + Update manifest violates constraints (has thumbnail or does not have exactly one parentOf ingredient) +- id: validation:inception_action_position + description: |- + Check if an inception action is the first action in the first actions assertion in created_assertions + expression: | + endsWith(notNull(_createdAssertions()[?contains(url, "c2pa.actions")] | [0].url, ""), "c2pa.actions.v2") && + (length(_actionsV2()[?action == "c2pa.created"]) == 0 || + contains(`["c2pa.created", "c2pa.opened"]`, _actionsV2()[0].action)) + reportText: + 'true': + en: |- + Inception action is correctly positioned as the first item in the first created actions assertion + 'false': + en: |- + Inception action is not in the first actions assertion of created_assertions (or first item is not an actions assertion) +- id: validation:no_deprecated_claim_fields + description: |- + Check for deprecated fields in the claim (e.g. specVersion in claim instead of claim_generator_info) + expression: '_claimV2().specVersion == null + + ' + reportText: + 'true': + en: No deprecated claim fields found + 'false': + en: Found deprecated claim fields (e.g. specVersion in claim) +- id: validation:mandatory_dst_for_editorial_scenarios + description: |- + Check if every editorial and created action in the active manifest has a digitalSourceType + expression: | + length(notNull(_actionsV2()[? !contains($non_dst_required_actions, action) && digitalSourceType == null], `[]`)) == 0 + reportText: + 'true': + en: Every editorial and created action has a digitalSourceType + 'false': + en: Found editorial or created actions missing a digitalSourceType +- id: validation:ingredient_v3_choice + description: |- + Check that ingredient v3 does not contain both activeManifest and digitalSourceType + expression: | + length(_ingredientAssertions()[? activeManifest != null && digitalSourceType != null]) == 0 + reportText: + 'true': + en: No ingredient v3 assertion contains both activeManifest and digitalSourceType + 'false': + en: |- + Found ingredient v3 assertion containing both activeManifest and digitalSourceType +- id: validation:alternative_content_representation_choice + description: |- + Check that alternative content representation has exactly one of multiAssetPartIndex or embeddedOriginalPreservationImage + expression: | + length(_assertionValues()[? + (type == "exif.originalPreservationImage" || parameters.type == "exif.originalPreservationImage") && + ((parameters.multiAssetPartIndex != null && parameters.embeddedOriginalPreservationImage != null) || + (parameters.multiAssetPartIndex == null && parameters.embeddedOriginalPreservationImage == null)) + ]) == 0 + reportText: + 'true': + en: Alternative content representation assertions have valid choice of parameters + 'false': + en: |- + Found alternative content representation assertion with invalid choice of parameters (both or neither present) +- id: validation:forbidden_labels_in_external_references + description: Check that external references do not point to forbidden labels + expression: | + length(_assertionValues()[? label != null && url != null && relationship == null && + (contains(`["c2pa.actions", "c2pa.external-reference"]`, label) || + startsWith(label, "c2pa.hash") || + startsWith(label, "c2pa.ingredient"))]) == 0 + reportText: + 'true': + en: No external references point to forbidden labels + 'false': + en: Found external reference pointing to a forbidden label +- id: validation:no_url_in_hashes + description: Check that deprecated url field is not present in hash assertions (Spec 2.4) + expression: | + (manifests[0].assertions.'c2pa.hash.data' == null || + manifests[0].assertions.'c2pa.hash.data'.url == null) && + (manifests[0].assertions.'c2pa.hash.bmff.v3' == null || + manifests[0].assertions.'c2pa.hash.bmff.v3'.url == null) + reportText: + 'true': + en: No deprecated url field found in hash assertions + 'false': + en: Found deprecated url field in hash assertions +- id: validation:ingredient_v3_mandatory_validation_results + description: Check that ingredient v3 has validationResults if activeManifest is present + expression: | + length(_ingredientAssertions()[? activeManifest != null && validationResults == null]) == 0 + reportText: + 'true': + en: Ingredient v3 assertions with activeManifest correctly include validationResults + 'false': + en: Found ingredient v3 assertion with activeManifest but missing validationResults diff --git a/public/rubrics/asset-rubric-integrity.yml b/public/rubrics/asset-rubric-integrity.yml new file mode 100644 index 0000000..0aac271 --- /dev/null +++ b/public/rubrics/asset-rubric-integrity.yml @@ -0,0 +1,136 @@ +rubric_metadata: + name: C2PA Asset Integrity Rubric + issuer: C2PA Conformance Task Force + date: '2026-03-31T05:00:00Z' + version: 1.1.0 + language: en +variables: + $well_formed_error_codes: + - assertion.action.malformed + - assertion.bmffHash.malformed + - assertion.boxesHash.malformed + - assertion.collectionHash.malformed + - assertion.dataHash.malformed + - assertion.external-reference.malformed + - assertion.ingredient.malformed + - assertion.alternativeContentRepresentation.malformed + - assertion.multiAssetHash.malformed + - assertion.cbor.invalid + - assertion.json.invalid + - claim.cbor.invalid + - claim.hardBindings.missing + - claim.malformed + - claim.missing + - claim.multiple + - hashedURI.missing + - ingredient.manifest.missing + - assertion.boxesHash.unknownBox + - assertion.multipleHardBindings + - manifest.compressed.invalid + - manifest.html.multipleManifests + - manifest.inaccessible + - manifest.multipleParents + - manifest.structuredText.emptyReference + - manifest.structuredText.malformedReference + - manifest.structuredText.multipleReferences + - manifest.structuredText.noManifest + - manifest.structuredText.noResolutionPath + - manifest.timestamp.invalid + - manifest.timestamp.wrongParents + - manifest.update.invalid + - manifest.update.wrongParents + - MISSING_VALIDATION_RESULTS + $valid_error_codes: + - assertion.action.ingredientMismatch + - assertion.action.redactionMismatch + - assertion.bmffHash.mismatch + - assertion.boxesHash.mismatch + - assertion.collectionHash.mismatch + - assertion.dataHash.mismatch + - assertion.external-reference.hashMismatch + - assertion.alternativeContentRepresentation.hashMismatch + - assertion.multiAssetHash.mismatch + - assertion.hashedURI.mismatch + - hashedURI.mismatch + - ingredient.manifest.mismatch + - ingredient.claimSignature.missing + - ingredient.claimSignature.mismatch + - claimSignature.missing + - MISSING_VALIDATION_RESULTS + $trust_error_codes: + - claimSignature.mismatch + - claimSignature.outsideValidity + - signingCredential.invalid + - signingCredential.ocsp.revoked + - signingCredential.untrusted + - MISSING_VALIDATION_RESULTS +expressions: + _validationResults: |- + (manifests[0].validationResults || { failure: [{code: "MISSING_VALIDATION_RESULTS"}] }) + +--- + +- id: validation:well_formed_data_present + description: Check if validation results are present for structural analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for structural analysis + 'false': + en: Missing validation results in the manifest +- id: validation:well_formed_success + description: |- + Check if the asset has any structural or malformation failures (from the well-formed list) + failIfMatched: true + expression: '_validationResults().failure[?contains($well_formed_error_codes, code)].code + + ' + reportText: + 'true': + en: No structural failures found + 'false': + en: 'Found structural failures: {{matches}}' +- id: validation:valid_data_present + description: Check if validation results are present for integrity analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for integrity analysis + 'false': + en: Missing validation results in the manifest +- id: validation:valid_success + description: Check if the asset has any validation mismatches (from the valid list) + failIfMatched: true + expression: '_validationResults().failure[?contains($valid_error_codes, code)].code + + ' + reportText: + 'true': + en: No validation mismatches found + 'false': + en: 'Found validation mismatches: {{matches}}' +- id: validation:trusted_data_present + description: Check if validation results are present for trust analysis + expression: 'manifests[0].validationResults != null + + ' + reportText: + 'true': + en: Validation results are present for trust analysis + 'false': + en: Missing validation results in the manifest +- id: validation:trusted_success + description: Check if the asset has any trust failures (from the trusted list) + failIfMatched: true + expression: '_validationResults().failure[?contains($trust_error_codes, code)].code + + ' + reportText: + 'true': + en: Asset is trusted + 'false': + en: 'Found trust failures: {{matches}}' diff --git a/public/rubrics/asset-rubric-signals-local.yml b/public/rubrics/asset-rubric-signals-local.yml new file mode 100644 index 0000000..44aa1e5 --- /dev/null +++ b/public/rubrics/asset-rubric-signals-local.yml @@ -0,0 +1,188 @@ +rubric_metadata: + name: C2PA Asset Signals Rubric (Local) + issuer: C2PA Conformance Task Force + date: '2026-03-31T05:00:00Z' + version: 1.0.0 + language: en +variables: + $non_editorial_actions: + - c2pa.created + - c2pa.opened + - c2pa.converted + - c2pa.published + - c2pa.repackaged + - c2pa.transcoded + - c2pa.resized.proportional + - c2pa.enhanced + - c2pa.edited.metadata + - c2pa.watermarked + - c2pa.watermarked.bound + - c2pa.watermarked.unbound + - c2pa.redacted + $non_editorial_transformation_actions: + - c2pa.converted + - c2pa.published + - c2pa.repackaged + - c2pa.transcoded + - c2pa.resized.proportional + - c2pa.enhanced + - c2pa.edited.metadata + - c2pa.watermarked + - c2pa.watermarked.bound + - c2pa.watermarked.unbound + - c2pa.redacted + $genai_dst_types: + - http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia + - |- + http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia + - http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic + $possibly_genai_dst_types: + - http://cv.iptc.org/newscodes/digitalsourcetype/digitalArt + - http://cv.iptc.org/newscodes/digitalsourcetype/composite +expressions: + _manifest_hasActionsV2: |- + $arg0.assertions.'c2pa.actions.v2' != null && length($arg0.'claim.v2'.created_assertions[?contains(url, "c2pa.actions.v2")]) > 0 + _manifest_signalActions: $arg0.assertions.'c2pa.actions.v2'.actions + _manifest_createdWithDst: |- + _manifest_hasActionsV2($arg0) && length(_manifest_signalActions($arg0)[? action == "c2pa.created" && digitalSourceType == $arg1]) > 0 + _hasActionsV2: _manifest_hasActionsV2(@) + _signalActions: _manifest_signalActions(@) + _createdWithDst: _manifest_createdWithDst(@, $arg0) + +--- + +- id: inception:signal_blankCanvas + description: Check if the manifest contains a Blank Canvas inception signal + expression: '_createdWithDst("http://c2pa.org/digitalsourcetype/empty") + + ' + reportText: + 'true': + en: Contains Blank Canvas + 'false': + en: Does not contain Blank Canvas +- id: inception:signal_capturedMedia + description: Check if the manifest contains a Captured media inception signal + expression: "_createdWithDst(\"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture\") || \n_createdWithDst(\"http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture\")\n" + reportText: + 'true': + en: Contains Captured Media + 'false': + en: Does not contain Captured Media +- id: inception:signal_capturedMediaStitched + description: Check if the manifest contains a Stitched Captured Media inception signal + expression: | + _createdWithDst("http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture") + reportText: + 'true': + en: Contains Stitched Captured Media + 'false': + en: Does not contain Stitched Captured Media +- id: inception:signal_compositionMayContainGenAI + description: |- + Check if the manifest contains a Composition which may contain GenAI content inception signal + expression: '_createdWithDst("http://cv.iptc.org/newscodes/digitalsourcetype/composite") + + ' + reportText: + 'true': + en: Contains Composition which may contain GenAI content + 'false': + en: Does not contain Composition which may contain GenAI content +- id: inception:signal_fullyGenAIMedia + description: Check if the manifest contains a Fully GenAI Media inception signal + expression: | + _createdWithDst("http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia") + reportText: + 'true': + en: Contains Fully GenAI Media + 'false': + en: Does not contain Fully GenAI Media +- id: inception:signal_mediaUnknownProvenance + description: Check if the manifest contains a Media of Unknown Provenance inception signal + expression: | + assertions.'c2pa.ingredient.v3' != null && + assertions.'c2pa.ingredient.v3'.activeManifest == null + reportText: + 'true': + en: Contains Media of Unknown Provenance + 'false': + en: Does not contain Media of Unknown Provenance +- id: inception:signal_nonGenAIDigitalCreation + description: Check if the manifest contains a Non-GenAI digital creation inception signal + expression: | + _createdWithDst("http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation") + reportText: + 'true': + en: Contains Non-GenAI digital creation + 'false': + en: Does not contain Non-GenAI digital creation +- id: inception:signal_partlyGenAICreation + description: Check if the manifest contains a Partly GenAI Creation inception signal + expression: | + _createdWithDst("http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia") + reportText: + 'true': + en: Contains Partly GenAI Creation + 'false': + en: Does not contain Partly GenAI Creation +- id: inception:signal_screenCaptureMayContainGenAI + description: |- + Check if the manifest contains a Screen Capture Which May Contain GenAI Content inception signal + expression: '_createdWithDst("http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture") + + ' + reportText: + 'true': + en: Contains Screen Capture Which May Contain GenAI Content + 'false': + en: Does not contain Screen Capture Which May Contain GenAI Content +- id: transformation:signal_editorialAI + description: Check if the manifest contains an Editorial GenAI Transformations signal + expression: | + _hasActionsV2() && + length(_signalActions()[? action != null && + !contains($non_editorial_actions, action) && + contains($genai_dst_types, digitalSourceType)]) > 0 + reportText: + 'true': + en: Contains Editorial GenAI Transformations + 'false': + en: Does not contain Editorial GenAI Transformations +- id: transformation:signal_editorialNonAI + description: Check if the manifest contains an Editorial Non-GenAI Transformations signal + expression: | + _hasActionsV2() && + length(_signalActions()[? action != null && + !contains($non_editorial_actions, action) && + digitalSourceType != null && + !contains($genai_dst_types, digitalSourceType)]) > 0 + reportText: + 'true': + en: Contains Editorial Non-GenAI Transformations + 'false': + en: Does not contain Editorial Non-GenAI Transformations +- id: transformation:signal_editorialPossiblyGenAI + description: |- + Check if the manifest contains an Editorial Transformations Possibly Using GenAI signal + expression: | + _hasActionsV2() && + length(_signalActions()[? action != null && + !contains($non_editorial_actions, action) && + contains($possibly_genai_dst_types, digitalSourceType)]) > 0 + reportText: + 'true': + en: Contains Editorial Transformations Possibly Using GenAI + 'false': + en: Does not contain Editorial Transformations Possibly Using GenAI +- id: transformation:signal_nonEditorial + description: Check if the manifest contains a Non-Editorial Transformations signal + expression: | + _hasActionsV2() && + length(_signalActions()[? action != null && + contains($non_editorial_transformation_actions, action)]) > 0 + reportText: + 'true': + en: Contains Non-editorial Transformations + 'false': + en: Does not contain Non-editorial Transformations diff --git a/public/rubrics/index.json b/public/rubrics/index.json new file mode 100644 index 0000000..f32f567 --- /dev/null +++ b/public/rubrics/index.json @@ -0,0 +1,44 @@ +{ + "rubrics": [ + { + "id": "asset-integrity", + "filename": "asset-rubric-integrity.yml", + "name": "Asset Integrity", + "description": "Structural, integrity, and trust validation checks against the C2PA 2.x spec.", + "mode": "document", + "category": "Integrity" + }, + { + "id": "asset-conformance-0.1-spec2.2", + "filename": "asset-rubric-conformance0.1-spec2.2.yml", + "name": "Asset Conformance 0.1 \u00b7 Spec 2.2", + "description": "Full conformance rubric (integrity + structural + spec-compliance checks) for C2PA Spec 2.2, conformance profile 0.1.", + "mode": "document", + "category": "Conformance" + }, + { + "id": "asset-conformance-0.2-spec2.2", + "filename": "asset-rubric-conformance0.2-spec2.2.yml", + "name": "Asset Conformance 0.2 \u00b7 Spec 2.2", + "description": "Full conformance rubric for C2PA Spec 2.2, conformance profile 0.2 (adds the mandatory specVersion check).", + "mode": "document", + "category": "Conformance" + }, + { + "id": "asset-conformance-0.2-spec2.4", + "filename": "asset-rubric-conformance0.2-spec2.4.yml", + "name": "Asset Conformance 0.2 \u00b7 Spec 2.4", + "description": "Full conformance rubric for C2PA Spec 2.4 (conformance profile 0.2). Strictest variant.", + "mode": "document", + "category": "Conformance" + }, + { + "id": "asset-signals-local", + "filename": "asset-rubric-signals-local.yml", + "name": "Asset Signals (Local)", + "description": "Detects inception and transformation signals on each manifest (e.g. captured media, GenAI transformations).", + "mode": "per-manifest", + "category": "Signals" + } + ] +} diff --git a/src/App.svelte b/src/App.svelte index f26a48e..b0ecef1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,7 +1,7 @@ - + - -
- -
-
- -
-

Asset

-

- Select the asset to extract crJSON. Drop a file or click to browse. -

- {#if assetFile && assetCrJson && mediaUrl} - -
-
-
- - - -
- Media Preview -
-
-

{assetFile.name}

-
- {#if mediaType === 'image'} - Preview - {:else if mediaType === 'video'} - - {:else if mediaType === 'audio'} -
-
🎵
- -
- {:else if mediaType === 'document'} -
-
📄
- Download PDF -
- {:else} -

Preview not available

- {/if} -
-
-
- {:else} -
- -
- {/if} -
- - {assetFile ? assetFile.name : 'No asset selected'} - - {assetStatus} -
-
- - -
-

Asset profile

-

- Paste YAML below or load from file. Multiple documents: first is metadata; later documents define sections. -

-
- - - -
-
- -
-
-
- - -
-
- - {evaluationStatus} -
-
- Evaluator WASM: - {#if wasmStatus === 'checking'} - Checking… - {:else if wasmStatus === 'ok'} - OK - {:else if wasmStatus === 'fail'} - Failed - - {/if} -
- {#if wasmStatus === 'fail' && wasmDetail} -
-

Why the evaluator failed

-

{wasmDetail}

-

- Script URL: {PROFILE_EVALUATOR_SCRIPT_URL} -

-

- Ensure public/profile-evaluator/ contains - profile_evaluator_rs.js and - profile_evaluator_rs_bg.wasm - (run npm run copy:profile-evaluator from a built sibling profile-evaluator-rs repo). -

-
- {/if} - {#if error} -
- {error} -
- {/if} -
-
- - -
-
-
-

Output

-

- Formatted report or raw JSON. Download or copy the result. -

-
- {#if evaluationResult != null} -
-
- - -
- - -
- {/if} -
- - {#if isEmptyReport} -
- Empty report. The evaluator ran successfully but returned no content. Asset profiles must have multiple YAML documents: the first is metadata; subsequent documents define blocks and statements. Add at least one section document to your profile to get report output. -
- {/if} - - {#if evaluationResult == null} -
- {evaluatingProfile ? 'Evaluation in progress...' : 'No evaluation result yet. Select an asset and an asset profile, then run Evaluate Profile.'} -
- {:else if showJsonTab} -
-
-
- {:else} - - {#if isErrorResult} -
-

Evaluation failed

-

{(evaluationResult as { error?: string }).error ?? 'Unknown error'}

- {#if (evaluationResult as { detail?: string }).detail} -

{(evaluationResult as { detail: string }).detail}

- {/if} -
- {:else if displayResult && typeof displayResult === 'object' && !Array.isArray(displayResult)} - {@const entries = getSortedSectionEntries(displayResult as Record)} -
- -
- {#if profileComplianceStatement} - {@const compReportText = getReportText(profileComplianceStatement)} -
-
- {#if isProfileCompliant === true} - ✓ - {:else if isProfileCompliant === false} - ✗ - {:else} - ? - {/if} -
-
-

- {#if isProfileCompliant === true} - ✓ Profile compliant - {:else if isProfileCompliant === false} - ✗ Not compliant - {:else} - Profile compliance - {/if} -

- {#if compReportText} -

{compReportText}

- {/if} -
-
- {:else} -
-
- ? -
-
-

Overall conformance

-

No profile compliance data in report. Ensure the profile defines a c2pa:profile_compliance section.

-
-
- {/if} -
- - - {#each entries as [sectionKey, sectionValue]} - {@const isMeta = isMetadataSection(sectionValue)} - {@const blocks = getStatementBlocks(sectionValue)} - {@const isExpanded = expandedSections.has(sectionKey)} - {@const blockCount = isMeta ? 1 : blocks.length} -
- - {#if isExpanded} -
- {#if isMeta} - -
- {#each Object.entries(sectionValue as Record) as [k, v]} - {#if v != null} -
-
{k}
-
{typeof v === 'object' && !Array.isArray(v) ? JSON.stringify(v) : String(v)}
-
- {/if} - {/each} -
- {:else} -
- {#each blocks as block} - {@const titleObj = block[0] != null && typeof block[0] === 'object' && !Array.isArray(block[0]) ? getStatementObj(block[0]) : null} - {@const valueObjs = block.slice(1).filter((x): x is Record => x != null && typeof x === 'object' && !Array.isArray(x) && 'value' in x)} -
- {#if titleObj?.title} -
{titleObj.title}
- {/if} -
- {#each valueObjs as obj} - {@const stmt = getStatementObj(obj)} - {#if stmt} -
-
- {stmt.id || '—'} - {#if stmt.value === true} - ✓ Pass - {:else if stmt.value === false} - ✗ Fail - {:else if stmt.value != null} - {String(stmt.value)} - {/if} -
-

- Explanation: {stmt.report_text || '—'} -

-
- {/if} - {/each} -
-
- {/each} -
- {/if} -
- {/if} -
- {/each} -
- {:else} -
- Result is not an object with sections. Use the JSON tab to view raw output. -
- {/if} - {/if} -
-
diff --git a/src/lib/CertificateManager.svelte b/src/lib/CertificateManager.svelte index 00fb868..d288329 100644 --- a/src/lib/CertificateManager.svelte +++ b/src/lib/CertificateManager.svelte @@ -324,35 +324,35 @@ MCZvLxXCtwIgCxkR0Gbdwef8k0bf1tC3dz+4NDe0S8wdCx5ZgeRPkq4= } -
+
-
+
-

+

Test Certificates

-

+

Add test certificates for conformance testing. Session-only and clearly marked in results.

-
+
-
+
-

+

C2PA Test Mode

-

+

Load the built-in C2PA conformance test root certificate and download a signing certificate for testing

@@ -390,7 +390,7 @@ MCZvLxXCtwIgCxkR0Gbdwef8k0bf1tC3dz+4NDe0S8wdCx5ZgeRPkq4=
{#if testCertificates.length > 0} - + {testCertificates.length} {testCertificates.length === 1 ? 'certificate' : 'certificates'} loaded @@ -412,27 +412,27 @@ MCZvLxXCtwIgCxkR0Gbdwef8k0bf1tC3dz+4NDe0S8wdCx5ZgeRPkq4=
{#each testCertificates as cert, index} {@const isTestRoot = index === 0 && testModeEnabled && testRootLoaded} -
+
{:else} @@ -84,15 +92,17 @@ {dragOver ? 'Drop it here!' : 'Drop a file or click to browse'}

- Supports images, videos, audio, and PDF documents with C2PA manifests + Drop a media file with embedded provenance, a standalone .c2pa sidecar, or any asset paired with its .c2pa sidecar.

- +
Images Videos Audio PDFs + .c2pa sidecars + any asset + sidecar
@@ -100,7 +110,7 @@ bind:this={fileInput} type="file" on:change={handleFileInput} - accept="image/*,video/*,audio/*,.pdf,.dng,.arw,.cr2,.cr3,.nef,.orf,.rw2" + multiple class="hidden" />
diff --git a/src/lib/IngredientTree.svelte b/src/lib/IngredientTree.svelte new file mode 100644 index 0000000..7da45c5 --- /dev/null +++ b/src/lib/IngredientTree.svelte @@ -0,0 +1,81 @@ + + +{#each nodes as node} +
+
+ + +
+ {#if node.thumbnailSrc} + + {:else if node.format?.startsWith('image/')} + + + + + {:else if node.format?.startsWith('video/')} + + + + + {:else if node.format?.startsWith('audio/')} + + + + + {:else} + + + + + {/if} +
+ + +
+
+ {node.title} + {#if node.isRoot} + active + {:else if node.relationship} + {node.relationship} + {/if} +
+ {#if node.claimGenerator} +

{node.claimGenerator}

+ {/if} + {#if node.format} +

{formatLabel(node.format)}

+ {/if} +
+
+ + {#if node.children.length > 0} +
+ +
+ {/if} +
+{/each} diff --git a/src/lib/ManifestSummary.svelte b/src/lib/ManifestSummary.svelte index 8a8bf3f..b3d3ac4 100644 --- a/src/lib/ManifestSummary.svelte +++ b/src/lib/ManifestSummary.svelte @@ -1,5 +1,6 @@ {#if summary.sentence} diff --git a/src/lib/OverviewPanel.svelte b/src/lib/OverviewPanel.svelte new file mode 100644 index 0000000..9d73c6e --- /dev/null +++ b/src/lib/OverviewPanel.svelte @@ -0,0 +1,452 @@ + + +{#if tree} + +
{}} + > + +
+ + +
+ +
+ + +
+ + + +
+ + + {#if !signals} +

Loading signal data…

+ {/if} +
+{:else} +

No manifest data

+{/if} diff --git a/src/lib/ReportViewer.svelte b/src/lib/ReportViewer.svelte index 0219f21..b9d50c1 100644 --- a/src/lib/ReportViewer.svelte +++ b/src/lib/ReportViewer.svelte @@ -1,17 +1,22 @@ -
+
-

Conformance Report

-

Manifest validation details

+

{heading.title}

+

{heading.subtitle}

+ +
{/if} - {#if showRaw} + {#if activeTab === 'summary'} +
+ +
+ {:else if activeTab === 'crjson'}
@@ -669,15 +789,25 @@
+ {:else if activeTab === 'rubrics'} +
+ +
{:else}
- + {#if mediaType === 'sidecar'} + + {:else} + + {/if}
-

Media Preview

+

+ {mediaType === 'sidecar' ? 'Sidecar File' : 'Media Preview'} +

{#if file && mediaUrl} @@ -689,7 +819,9 @@
Type
-

{file.type || 'Unknown'}

+

+ {mediaType === 'sidecar' ? 'application/c2pa (sidecar)' : (file.type || 'Unknown')} +

Size
@@ -727,6 +859,24 @@ Download PDF
+ {:else if mediaType === 'sidecar'} +
+
+ +
+

Standalone manifest sidecar

+

+ This is a .c2pa file — a + C2PA manifest store detached from its referenced asset. No embedded media to preview; the manifest’s + contents are shown below. +

+ + + + + Download .c2pa + +
{:else}

Preview not available

@@ -742,6 +892,7 @@ manifest={activeManifest} ingredients={ingredientsList} mimeType={file?.type ?? ''} + signals={activeManifestSignals} {usedITL} {isTrusted} /> @@ -765,43 +916,66 @@

Validation Status Details

- {#if validationStatus && validationStatus.length > 0} -
- {#each validationStatus as status} -
-
-
- {status.isInterim ? 'i' : status.success ? '✓' : '✕'} -
-
-

- {status.code} - {#if status.isInterim} - ITL - {/if} -

- {#if status.explanation} -

{status.explanation}

- {/if} -
+ {#if validationGroups && validationGroups.length > 0} +
+ {#each validationGroups as group} +
+

+ {#if group.isActive} + Active Asset + {:else} + Ingredient {group.index} + {/if} + {group.label} + {#if group.sigInfo?.common_name} + + signed by {group.sigInfo.common_name} + + {/if} +

+ +
+ + {#each group.failure as status} +
+
+ +
+ {status.code} +

{status.explanation}

+
+
+
+ {/each} + + + {#each group.success as status} +
+
+ +
+ {status.code} + {#if status.isInterim} + ITL + {/if} +

{status.explanation}

+
+
+
+ {/each} + + + {#each group.informational as status} +
+
+ i +
+ {status.code} +

{status.explanation}

+
+
+
+ {/each}
{/each} @@ -840,26 +1014,19 @@

{signatureInfo.time}

{/if} - {#if signatureInfo.alg} -
-
Algorithm
-

{signatureInfo.alg}

-
- {/if} - {#if certValidityStatus !== 'unknown'}
Certificate Validity
{#if certValidityStatus === 'valid'}
- - Valid at time of signing + + Valid at time of signing
{:else if certValidityStatus === 'expired'}
- - Expired at time of signing + + Expired at time of signing
{/if}
@@ -871,23 +1038,23 @@
OCSP Revocation
{#if ocspStatus === 'not_revoked'}
- - Not revoked + + Not revoked
{:else if ocspStatus === 'revoked'}
- - Revoked + + Revoked
{:else if ocspStatus === 'no_staple'}
- - No OCSP staple present + + No OCSP staple present
{:else if ocspStatus === 'inaccessible'}
- - OCSP server inaccessible + + OCSP server inaccessible
{/if}
@@ -907,10 +1074,10 @@
Claim Generator

- {#if claimInfo?.claim_generator_info?.length > 0} + {#if claimInfo?.claim_generator_info?.[0]?.name} {claimInfo.claim_generator_info[0].name} {#if claimInfo.claim_generator_info[0].version} - v{claimInfo.claim_generator_info[0].version} + v{claimInfo.claim_generator_info[0].version} {/if} {:else if claimInfo?.claim_generator} {claimInfo.claim_generator} @@ -919,16 +1086,6 @@ {/if}

-
-
Instance ID
-

{claimInfo?.instance_id ?? activeManifest?.label ?? 'N/A'}

-
- {#if activeManifest?.label} -
-
Label
-

{activeManifest.label}

-
- {/if}
@@ -971,7 +1128,7 @@ {index + 1}
-

{assertion.label || assertion.url || 'Unknown'}

+

{assertion.label || 'Unknown'}

{#if assertion.data} - -
- {ingredientsList.length} -
-
-
-
- {#each ingredientsList as ingredient, index} - {@const ingredientManifest = getIngredientManifest(ingredient)} - {@const isExpanded = expandedIngredients.has(index)} -
-
-
- {index + 1} -
-
-

{ingredient.title || ingredient.instance_id || 'Unknown'}

- {#if ingredientManifest} - - {/if} -
-
-
-
- {#if ingredient.relationship} -
-
Relationship
-

{ingredient.relationship}

-
- {/if} - {#if ingredient.format} -
-
Format
-

{ingredient.format}

-
- {/if} -
- {#if ingredient.document_id} -
-
Document ID
-

{ingredient.document_id}

-
- {/if} - {#if ingredient.instance_id && !ingredient.title} -
-
Instance ID
-

{ingredient.instance_id}

-
- {/if} - - - {#if ingredientManifest && isExpanded} - {@const ingClaim = getClaimInfo(ingredientManifest)} - {@const ingSig = getSignatureInfo(ingredientManifest)} - {@const ingAssertions = getAssertionsList(ingredientManifest)} - {@const ingIngredients = getIngredientsFromManifest(ingredientManifest)} -
-
- - - - Manifest Details -
- -
- {#if ingClaim?.claim_generator} -
-
Claim Generator
-

{ingClaim.claim_generator}

-
- {/if} - {#if ingClaim?.claim_generator_info?.length > 0} -
-
Claim Generator
-

- {ingClaim.claim_generator_info[0].name} - {#if ingClaim.claim_generator_info[0].version} - v{ingClaim.claim_generator_info[0].version} - {/if} -

-
- {/if} - {#if ingSig?.common_name} -
-
Signed By
-

{ingSig.common_name}

-
- {/if} - {#if ingSig?.time} -
-
Signature Time
-

{ingSig.time}

-
- {/if} - {#if ingSig?.issuer} -
-
Issuer
-

{ingSig.issuer}

-
- {/if} -
- - {#if ingAssertions.length > 0} -
-
- Assertions - {ingAssertions.length} -
-
- {#each ingAssertions.slice(0, 5) as assertion} -
• {assertion.label || 'Unknown'}
- {/each} - {#if ingAssertions.length > 5} -
+ {ingAssertions.length - 5} more...
- {/if} -
-
- {/if} - - {#if ingIngredients.length > 0} -
-
- Nested Ingredients - {ingIngredients.length} -
-
- {#each ingIngredients.slice(0, 3) as nestedIngredient} -
• {nestedIngredient.title || nestedIngredient.instance_id || 'Unknown'}
- {/each} - {#if ingIngredients.length > 3} -
+ {ingIngredients.length - 3} more...
- {/if} -
-
- {/if} -
- {/if} -
-
- {/each} -
- - {/if} - {:else}
diff --git a/src/lib/ReportViewerComponent.test.ts b/src/lib/ReportViewerComponent.test.ts new file mode 100644 index 0000000..5e62719 --- /dev/null +++ b/src/lib/ReportViewerComponent.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from 'vitest' +import { render, fireEvent } from '@testing-library/svelte' +import ReportViewer from './ReportViewer.svelte' +import type { ConformanceReport } from './types' + +describe('ReportViewer Component', () => { + it('should render failures grouped by manifest in Validation Status Details', () => { + const mockReport: ConformanceReport = { + manifests: [ + { + label: 'active_manifest_label', + assertions: {}, + validationResults: { + success: [ + { code: 'signingCredential.trusted' }, + { code: 'timeStamp.trusted' }, + { code: 'claimSignature.validated' } + ], + failure: [ + { code: 'assertion.bmffHash.mismatch', explanation: 'BMFF hash mismatch' } + ] + }, + signature: { + certificateInfo: { + subject: { CN: 'Active Signer' } + } + } + } + ] + } + + const { container, getByText } = render(ReportViewer, { report: mockReport }) + + // Navigate to the Report tab (default is Summary) + fireEvent.click(getByText('Report')) + + const detailsSection = container.querySelector('#validation-status') + expect(detailsSection).toBeTruthy() + + // Should have 1 manifest group card + const groupCards = detailsSection?.querySelectorAll('.manifest-group-card') + expect(groupCards?.length).toBe(1) + + // Check header of the group + const header = groupCards?.[0].querySelector('h4') + expect(header?.textContent).toContain('Active Asset') + expect(header?.textContent).toContain('active_manifest_label') + expect(header?.textContent).toContain('signed by Active Signer') + + // Check status cards inside the group (1 failure + 3 successes) + const failureCards = groupCards?.[0].querySelectorAll('.bg-red-50\\/50') // escaped slash for selector + expect(failureCards?.length).toBe(1) + expect(failureCards?.[0].textContent).toContain('assertion.bmffHash.mismatch') + expect(failureCards?.[0].textContent).toContain('BMFF hash mismatch') + + const successCards = groupCards?.[0].querySelectorAll('.bg-green-50\\/50') + expect(successCards?.length).toBe(3) + expect(successCards?.[0].textContent).toContain('signingCredential.trusted') + }) + + it('should render ingredient failures in their own group card', () => { + const mockReport: ConformanceReport = { + manifests: [ + { + label: 'active_label', + assertions: {}, + validationResults: { + success: [{ code: 'signingCredential.trusted' }] + }, + signature: { + certificateInfo: { + subject: { CN: 'Active Signer' } + } + } + }, + { + label: 'ingredient_label', + assertions: {}, + validationResults: { + failure: [{ code: 'assertion.bmffHash.mismatch', explanation: 'BMFF hash mismatch' }] + }, + signature: { + certificateInfo: { + subject: { CN: 'Ingredient Signer' } + } + } + } + ] + } + + const { container, getByText } = render(ReportViewer, { report: mockReport }) + + // Navigate to the Report tab (default is Summary) + fireEvent.click(getByText('Report')) + + const detailsSection = container.querySelector('#validation-status') + expect(detailsSection).toBeTruthy() + + // Should have 2 manifest group cards (both have statuses to show) + const groupCards = detailsSection?.querySelectorAll('.manifest-group-card') + expect(groupCards?.length).toBe(2) + + // First group (Active Asset) + const header1 = groupCards?.[0].querySelector('h4') + expect(header1?.textContent).toContain('Active Asset') + expect(header1?.textContent).toContain('active_label') + expect(header1?.textContent).toContain('signed by Active Signer') + expect(groupCards?.[0].querySelectorAll('.bg-green-50\\/50').length).toBe(1) + expect(groupCards?.[0].querySelectorAll('.bg-red-50\\/50').length).toBe(0) + + // Second group (Ingredient 1) + const header2 = groupCards?.[1].querySelector('h4') + expect(header2?.textContent).toContain('Ingredient 1') + expect(header2?.textContent).toContain('ingredient_label') + expect(header2?.textContent).toContain('signed by Ingredient Signer') + expect(groupCards?.[1].querySelectorAll('.bg-green-50\\/50').length).toBe(0) + expect(groupCards?.[1].querySelectorAll('.bg-red-50\\/50').length).toBe(1) + expect(groupCards?.[1].querySelector('.bg-red-50\\/50')?.textContent).toContain('assertion.bmffHash.mismatch') + }) +}) diff --git a/src/lib/RubricsPanel.svelte b/src/lib/RubricsPanel.svelte new file mode 100644 index 0000000..432953d --- /dev/null +++ b/src/lib/RubricsPanel.svelte @@ -0,0 +1,561 @@ + + +
+ +
+
+
+
+ + + + + + +
+

Available Rubrics

+
+ {#if index.length > 0} +
+ + +
+ {/if} +
+ + {#if indexLoading} +

Loading rubrics…

+ {:else if indexError} +
+ Failed to load rubric index: {indexError} +
+ {:else if index.length === 0} +

No rubrics available.

+ {:else} +
+ {#each groups as group (group.category)} +
+ +
+

+ {group.category} +

+
+ + {group.entries.length} + +
+
    + {#each group.entries as rubric (rubric.id)} + {@const isChecked = selected.has(rubric.id)} +
  • + +
  • + {/each} +
+
+ {/each} +
+ +
+

+ {selected.size} of {index.length} selected +

+ +
+ {/if} +
+ + + {#if runError} +
+

Evaluation failed

+

{runError}

+
+ {/if} + + + {#if results.length > 0} +
+
+
+
+ + + + + +
+
+

Results

+

+ {#if docResults.length > 0} + + {docPassCount} of {docResults.length} pass/fail rubrics passed + + {/if} + {#if docResults.length > 0 && signalsResults.length > 0} + · + {/if} + {#if signalsResults.length > 0} + + {signalsResults.length} signals rubric{signalsResults.length === 1 ? '' : 's'} + + {/if} + {#if ranAt} + · evaluated {ranAt.toLocaleTimeString()} + {/if} +

+
+
+
+ +
    + {#each results as r (r.rubricId)} + {#if isDocumentResult(r)} + {@const grouped = groupByOutcome(r)} +
  • +
    + {#if r.overallPassed} +
    + + + +
    + {:else} +
    + + + +
    + {/if} +
    +
    + {r.rubricName} + {#if r.rubricVersion} + v{r.rubricVersion} + {/if} + + {r.overallPassed ? 'Pass' : 'Fail'} + + + {r.statements.filter((s) => s.passed === true).length}/{r.statements.length} checks passed + +
    +
    +
    + + + {#if grouped.failed.length > 0} +
    +
    Failed
    +
      + {#each grouped.failed as s (s.id)} +
    • + + + +
      +

      {s.message || s.description || s.id}

      +

      {s.id}

      +
      +
    • + {/each} +
    +
    + {/if} + + {#if grouped.errored.length > 0} +
    +
    Errored
    +
      + {#each grouped.errored as s (s.id)} +
    • + + + +
      +

      {s.message || s.description || s.id}

      +

      {s.id}

      +
      +
    • + {/each} +
    +
    + {/if} + + {#if grouped.passed.length > 0} +
    + + Passed ({grouped.passed.length}) + +
      + {#each grouped.passed as s (s.id)} +
    • + + + +
      +

      {s.message || s.description || s.id}

      +

      {s.id}

      +
      +
    • + {/each} +
    +
    + {/if} +
  • + {:else if isSignalsResult(r)} + +
  • +
    +
    + + + + + + +
    +
    +
    + {r.rubricName} + {#if r.rubricVersion} + v{r.rubricVersion} + {/if} + + Signals + + + {r.manifests.length} manifest{r.manifests.length === 1 ? '' : 's'} + +
    +
    +
    + +
      + {#each r.manifests as m, idx (idx)} +
    1. +
      +
      + #{idx} + {formatAssertedBy(m.assertedBy)} + {#if m.mimeType} + {m.mimeType} + {/if} +
      +
      + + allActionsIncluded: {m.allActionsIncluded} + +
      +
      + + {#if totalSignalCount(m) === 0 && m.ingredients.length === 0} +

      No signals detected on this manifest.

      + {/if} + + {#if m.localInceptions.length > 0} +
      +
      + Inception ({m.localInceptions.length}) +
      +
        + {#each m.localInceptions as sig (sig.trait)} +
      • + + + +
        +

        + {sig.reportText} + {#if sig.multiple} + ×multiple + {/if} +

        +

        {sig.trait}

        +
        +
      • + {/each} +
      +
      + {/if} + + {#if m.localTransformations.length > 0} +
      +
      + Transformation ({m.localTransformations.length}) +
      +
        + {#each m.localTransformations as sig (sig.trait)} +
      • + + + + +
        +

        + {sig.reportText} + {#if sig.multiple} + ×multiple + {/if} +

        +

        {sig.trait}

        +
        +
      • + {/each} +
      +
      + {/if} + + {#if m.ingredients.length > 0} +
      +
      + Ingredients ({m.ingredients.length}) +
      +
        + {#each m.ingredients as edge, eidx (eidx)} +
      • + → manifest #{edge.index} + {#if edge.relationship} + {edge.relationship} + {/if} +
      • + {/each} +
      +
      + {/if} +
    2. + {/each} +
    +
  • + {/if} + {/each} +
+
+ {/if} +
diff --git a/src/lib/TreeNode.svelte b/src/lib/TreeNode.svelte new file mode 100644 index 0000000..04a5651 --- /dev/null +++ b/src/lib/TreeNode.svelte @@ -0,0 +1,168 @@ + + +
+ + + + +
+ + {#if !isRoot && node.relationship} +

+ {formatRelationship(node.relationship)} +

+ {/if} + + +

+ {#if node.isStub} + {node.claimGenerator ?? 'Unknown file'} + {:else} + {isRoot ? (fileName ?? 'This File') : (node.claimGenerator ?? 'Unknown')} + {/if} +

+ + + {#if node.isStub} +

No Content Credentials

+ {:else} + + {#if node.date} +

{node.date}

+ {/if} + + + {#if node.inceptions.length > 0 || node.transformations.length > 0} +
+ {#each node.inceptions as s} + {s} + {/each} + {#each node.transformations as s} + {s} + {/each} +
+ {/if} + {/if} +
+ + + {#if node.children.length > 0} + + + + +
+ {#each node.children as child, i} +
+ +
+ {/each} +
+ {/if} +
diff --git a/src/lib/c2pa.test.ts b/src/lib/c2pa.test.ts index 72bb454..0d86bbf 100644 --- a/src/lib/c2pa.test.ts +++ b/src/lib/c2pa.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import type { ValidationStatus } from '@contentauth/c2pa-web' -import { processFile, getVersion } from './c2pa' +import { processFile, getVersion, isSidecarFile, resolveMimeType, SIDECAR_MIME } from './c2pa' import type { ConformanceReport } from './types' // Track which validation is being called @@ -133,6 +132,35 @@ describe('c2pa utilities', () => { }) }) + describe('sidecar detection', () => { + // Browsers almost never set a MIME for .c2pa files, so extension-based + // detection is doing the real work here. We cover both shapes just in + // case a future environment fills in `type`. + it('detects a .c2pa file with no browser-reported MIME as a sidecar', () => { + const f = new File([new Uint8Array([0])], 'my-manifest.c2pa', { type: '' }) + expect(isSidecarFile(f)).toBe(true) + expect(resolveMimeType(f)).toBe(SIDECAR_MIME) + }) + + it('detects a .c2pa file served as application/octet-stream', () => { + const f = new File([new Uint8Array([0])], 'my-manifest.c2pa', { type: 'application/octet-stream' }) + expect(isSidecarFile(f)).toBe(true) + expect(resolveMimeType(f)).toBe(SIDECAR_MIME) + }) + + it('detects a file whose MIME is already application/c2pa', () => { + const f = new File([new Uint8Array([0])], 'no-extension', { type: SIDECAR_MIME }) + expect(isSidecarFile(f)).toBe(true) + expect(resolveMimeType(f)).toBe(SIDECAR_MIME) + }) + + it('does NOT mis-detect a .jpg as a sidecar', () => { + const f = new File([new Uint8Array([0])], 'photo.jpg', { type: 'image/jpeg' }) + expect(isSidecarFile(f)).toBe(false) + expect(resolveMimeType(f)).toBe('image/jpeg') + }) + }) + describe('processFile', () => { it('should process a file and return manifest store with trusted signature', async () => { // Reset to simulate trusted signature from the start @@ -192,10 +220,10 @@ describe('c2pa utilities', () => { expect(result).toBeDefined() // Check if ITL validation succeeded const hasUntrusted = result.validationResults?.activeManifest?.failure?.some( - (f: ValidationStatus) => f.code === 'signingCredential.untrusted' + (f) => f.code === 'signingCredential.untrusted' ) const hasTrusted = result.validationResults?.activeManifest?.success?.some( - (s: ValidationStatus) => s.code === 'signingCredential.trusted' + (s) => s.code === 'signingCredential.trusted' ) if (hasTrusted && !hasUntrusted) { diff --git a/src/lib/c2pa.ts b/src/lib/c2pa.ts index 5acca25..1e02ffc 100644 --- a/src/lib/c2pa.ts +++ b/src/lib/c2pa.ts @@ -1,5 +1,5 @@ import { createC2pa } from '@contentauth/c2pa-web' -import type { Settings, ValidationStatus } from '@contentauth/c2pa-web' +import type { Settings } from '@contentauth/c2pa-web' import { VERSION_INFO } from './version' import type { ConformanceReport } from './types' import { VALIDATION_STATUS } from './constants' @@ -8,11 +8,22 @@ import { isCrJson, legacyToCrJson, getActiveManifestValidationStatus, type CrJso type ReaderHandle = { manifestStore: () => Promise free: () => Promise + resourceToBytes?: (uri: string) => Promise } type C2paInstance = { reader: { fromBlob: (format: string, file: Blob, settings?: Settings) => Promise + fromSidecarAndBlob?: ( + sidecarBytes: Uint8Array, + assetFormat: string, + assetFile: Blob, + settings?: Settings, + ) => Promise + fromSidecarIntegrityOnly?: ( + sidecarBytes: Uint8Array, + settings?: Settings, + ) => Promise } getVersion?: () => Promise | string } @@ -21,6 +32,16 @@ type LocalC2paModule = { default: () => Promise get_version: () => string read_manifest_store: (fileBytes: Uint8Array, format: string, settingsJson?: string) => Promise + read_sidecar_manifest_store?: ( + manifestBytes: Uint8Array, + assetBytes: Uint8Array, + assetFormat: string, + settingsJson?: string, + ) => Promise + read_sidecar_integrity_only?: ( + manifestBytes: Uint8Array, + settingsJson?: string, + ) => Promise } type ExtractedCrJsonResult = { @@ -34,6 +55,9 @@ const importModule = new Function('modulePath', 'return import(modulePath)') as type ITL = { allowed: string; anchors: string } let c2paInstance: C2paInstance | null = null +let packagedC2paInstance: C2paInstance | null = null +// Cached raw packaged-SDK promise, shared between getPackagedC2pa() and enrichThumbnailsViaPackagedSdk(). +let packagedSdkPromise: ReturnType | null = null let mainTrustListPem: string | null = null let itl: ITL | null = null @@ -80,6 +104,14 @@ async function createLocalC2pa(): Promise { const localModule = await importModule(moduleUrl) await localModule.default() + const parseCrJson = (raw: string): CrJson => { + const parsed = JSON.parse(raw) as CrJson + if (!isCrJson(parsed)) { + throw new Error('Local WASM returned non-crJSON format') + } + return parsed + } + return { reader: { fromBlob: async (format: string, file: Blob, settings?: Settings) => ({ @@ -90,14 +122,49 @@ async function createLocalC2pa(): Promise { format, toLocalSettingsJson(settings) ) - const parsed = JSON.parse(manifestStoreJson) as CrJson - if (!isCrJson(parsed)) { - throw new Error('Local WASM returned non-crJSON format') - } - return parsed + return parseCrJson(manifestStoreJson) }, free: async () => {}, }), + ...(typeof localModule.read_sidecar_manifest_store === 'function' + ? { + fromSidecarAndBlob: async ( + sidecarBytes: Uint8Array, + assetFormat: string, + assetFile: Blob, + settings?: Settings, + ) => ({ + manifestStore: async () => { + const assetBytes = new Uint8Array(await assetFile.arrayBuffer()) + const json = await localModule.read_sidecar_manifest_store!( + sidecarBytes, + assetBytes, + assetFormat, + toLocalSettingsJson(settings), + ) + return parseCrJson(json) + }, + free: async () => {}, + }), + } + : {}), + ...(typeof localModule.read_sidecar_integrity_only === 'function' + ? { + fromSidecarIntegrityOnly: async ( + sidecarBytes: Uint8Array, + settings?: Settings, + ) => ({ + manifestStore: async () => { + const json = await localModule.read_sidecar_integrity_only!( + sidecarBytes, + toLocalSettingsJson(settings), + ) + return parseCrJson(json) + }, + free: async () => {}, + }), + } + : {}), }, getVersion: () => localModule.get_version(), } @@ -180,6 +247,39 @@ async function fetchITL(): Promise { } } +/** Wrap a @contentauth/c2pa-web SDK instance as a C2paInstance (legacy JSON → crJSON). */ +function wrapPackagedSdk(sdk: Awaited>): C2paInstance { + return { + reader: { + fromBlob: async (format: string, file: Blob, settings?: Settings) => { + const reader = await sdk.reader.fromBlob(format, file, settings) + if (!reader) return null + return { + manifestStore: async () => { + const legacy = await reader.manifestStore() as Record + return legacyToCrJson(legacy) + }, + free: async () => { await reader.free() }, + ...(reader.resourceToBytes && { resourceToBytes: reader.resourceToBytes.bind(reader) }), + } + }, + }, + getVersion: () => '@contentauth/c2pa-web v0.6.1', + } +} + +/** + * Return the packaged SDK as a C2paInstance, lazily initialised and cached. + * Used for sidecar files and thumbnail fallback — never replaced by the local WASM. + */ +async function getPackagedC2pa(): Promise { + if (!packagedC2paInstance) { + if (!packagedSdkPromise) packagedSdkPromise = createC2pa({ wasmSrc: `${base}c2pa.wasm` }) + packagedC2paInstance = wrapPackagedSdk(await packagedSdkPromise) + } + return packagedC2paInstance +} + /** * Initialize the C2PA SDK */ @@ -189,37 +289,7 @@ async function initC2pa(): Promise { } try { - c2paInstance = await createLocalC2pa() - - if (!c2paInstance) { - const fallbackSdk = await createC2pa({ - wasmSrc: `${base}c2pa.wasm` - }) - - c2paInstance = { - reader: { - fromBlob: async (format: string, file: Blob, settings?: Settings) => { - const reader = await fallbackSdk.reader.fromBlob(format, file, settings) - - if (!reader) { - return null - } - - return { - manifestStore: async () => { - const legacy = await reader.manifestStore() as Record - return legacyToCrJson(legacy) - }, - free: async () => { - await reader.free() - }, - } - }, - }, - getVersion: () => '@contentauth/c2pa-web v0.6.1', - } - } - + c2paInstance = await createLocalC2pa() ?? await getPackagedC2pa() return c2paInstance } catch (error) { console.error('Failed to initialize C2PA SDK:', error) @@ -241,7 +311,10 @@ const MIME_TYPE_MAP: Record = { 'image/dng': 'image/x-adobe-dng', } -// Fallback MIME types by file extension, for when the browser can't determine the type +// Fallback MIME types by file extension, for when the browser can't determine the type. +// `.c2pa` is the standalone manifest-store sidecar format (RFC-style, no embedded asset). +// Browsers universally leave its type empty or fall back to application/octet-stream, so +// we resolve by extension. const EXTENSION_MIME_MAP: Record = { 'dng': 'image/x-adobe-dng', 'arw': 'image/x-sony-arw', @@ -250,200 +323,332 @@ const EXTENSION_MIME_MAP: Record = { 'nef': 'image/x-nikon-nef', 'orf': 'image/x-olympus-orf', 'rw2': 'image/x-panasonic-rw2', + 'c2pa': 'application/c2pa', } -function resolveMimeType(file: File): string { +/** + * The MIME type the C2PA SDK uses for standalone manifest-store sidecars. + * Re-exported so UI code can detect this class of file consistently. + */ +export const SIDECAR_MIME = 'application/c2pa' + +/** + * True when the given File looks like a C2PA sidecar (standalone manifest store). + * Matches either the MIME type (if the browser somehow set it) or the `.c2pa` + * extension — which is how we'll detect it in ~100% of real drops, since no + * browser recognises the type natively yet. + */ +export function isSidecarFile(file: File): boolean { + if (file.type === SIDECAR_MIME) return true + const ext = file.name.split('.').pop()?.toLowerCase() ?? '' + return ext === 'c2pa' || ext === 'json' +} + +export function resolveMimeType(file: File): string { const mapped = MIME_TYPE_MAP[file.type] if (mapped) return mapped if (file.type && file.type !== 'application/octet-stream') return file.type - // Fall back to extension-based detection + // Fall back to extension-based detection, then to generic bytes. + // `application/octet-stream` lets c2pa-rs hash arbitrary asset bytes for + // sidecar+asset validation (c2pa.hash.data is format-agnostic). const ext = file.name.split('.').pop()?.toLowerCase() ?? '' - return EXTENSION_MIME_MAP[ext] ?? file.type + return EXTENSION_MIME_MAP[ext] ?? (file.type || 'application/octet-stream') } -async function extractCrJsonWithMetadata(file: File, testCertificates: string[] = []): Promise { - const mimeType = resolveMimeType(file) - console.log('🔍 Starting file processing for:', file.name, 'Type:', file.type, mimeType !== file.type ? `(remapped to ${mimeType})` : '') +/** + * Read the manifest store under a given set of trust settings. Returning + * `null` means "the SDK could not construct a reader for these inputs" — + * typically no manifest present, or (in the sidecar+asset case) the asset + * bytes don't match the manifest's hash bindings. + */ +type ReadManifestStore = (settings: Settings) => Promise - // Initialize C2PA SDK if not already initialized - console.log('Initializing C2PA SDK...') - const c2pa = await initC2pa() - console.log('✅ C2PA SDK initialized') +/** + * Three-step trust validation flow, independent of how bytes are sourced: + * + * 1. Official C2PA trust list. + * 2. + Session-only test certificates, if they change the outcome. + * 3. + ITL (Interim Trust List), as a last-resort fallback. + * + * The "how do I read the manifest store" piece is injected so this flow + * works identically for embedded (`fromBlob`) and sidecar+asset + * (`fromSidecarAndBlob`) validation. + */ +async function runTrustValidationFlow( + readManifestStore: ReadManifestStore, + testCertificates: string[], + noManifestErrorMessage: string, +): Promise { + console.log('Fetching official C2PA trust lists...') + const [mainTrustList, itlData] = await Promise.all([ + fetchMainTrustList(), + fetchITL() + ]) + + console.log('Step 1: Validating with official trust list only...') + const officialSettings: Settings = { + verify: { verifyTrust: true, verifyAfterReading: true }, + trust: { trustAnchors: mainTrustList } + } - try { - console.log('Fetching official C2PA trust lists...') + const officialCrJson = await readManifestStore(officialSettings) + if (!officialCrJson) { + throw new Error(noManifestErrorMessage) + } - // Fetch main trust list and ITL separately - const [mainTrustList, itlData] = await Promise.all([ - fetchMainTrustList(), - fetchITL() - ]) + console.log('📋 Raw crJSON keys:', Object.keys(officialCrJson)) + console.log('📋 validationResults:', JSON.stringify(officialCrJson.validationResults ?? null)) + console.log('📋 manifests[0] vr:', JSON.stringify((officialCrJson.manifests?.[0] as Record)?.validationResults ?? null)) + + const officialVr = getActiveManifestValidationStatus(officialCrJson) + const officialUntrusted = officialVr?.failure?.some( + (status) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED + ) + + console.log('Official TL validation results:', { + isUntrusted: officialUntrusted, + success: officialVr?.success?.map((s) => s.code), + failure: officialVr?.failure?.map((f) => f.code) + }) + + let crJson = officialCrJson + let usedTestCerts = false + + if (testCertificates.length > 0) { + console.log('Step 2: Validating with test certificates added...') + const testSettings: Settings = { + verify: { verifyTrust: true, verifyAfterReading: true }, + trust: { trustAnchors: mainTrustList + '\n' + testCertificates.join('\n') } + } - console.log('Step 1: Validating with official trust list only...') - const officialSettings: Settings = { - verify: { - verifyTrust: true, - verifyAfterReading: true - }, - trust: { - trustAnchors: mainTrustList + const testCrJson = await readManifestStore(testSettings) + if (testCrJson) { + const testVr = getActiveManifestValidationStatus(testCrJson) + const testUntrusted = testVr?.failure?.some( + (status) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED + ) + + console.log('Test cert validation results:', { + isUntrusted: testUntrusted, + success: testVr?.success?.map((s) => s.code), + failure: testVr?.failure?.map((f) => f.code) + }) + + if (officialUntrusted && !testUntrusted) { + console.log('✅ Test certificates made the difference - signature now trusted') + usedTestCerts = true + crJson = testCrJson + } else { + console.log('ℹ️ Test certificates loaded but not needed for validation') } } + } - // First validation with official trust list only (no test certs) - const reader1 = await c2pa.reader.fromBlob(mimeType, file, officialSettings) - if (!reader1) { - throw new Error('No C2PA manifest found in this file') - } + const mainVr = getActiveManifestValidationStatus(crJson) + const isUntrusted = mainVr?.failure?.some( + (status) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED + ) - const officialCrJson = await reader1.manifestStore() - await reader1.free() + console.log('Main validation results:', { + isUntrusted, + success: mainVr?.success?.map((s) => s.code), + failure: mainVr?.failure?.map((f) => f.code) + }) - console.log('📋 Raw crJSON keys:', Object.keys(officialCrJson)) - console.log('📋 validationResults:', JSON.stringify(officialCrJson.validationResults ?? null)) - console.log('📋 manifests[0] vr:', JSON.stringify((officialCrJson.manifests?.[0] as Record)?.validationResults ?? null)) + let usedITL = false + let finalCrJson = crJson - const vr = getActiveManifestValidationStatus(officialCrJson) - const officialUntrusted = vr?.failure?.some( - (status: ValidationStatus) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED - ) + if (isUntrusted) { + console.log('⚠️ Signature untrusted on main list, checking ITL...') - console.log('Official TL validation results:', { - isUntrusted: officialUntrusted, - success: vr?.success?.map((s: ValidationStatus) => s.code), - failure: vr?.failure?.map((f: ValidationStatus) => f.code) - }) - - let crJson = officialCrJson - let usedTestCerts = false - - if (testCertificates.length > 0) { - console.log('Step 2: Validating with test certificates added...') - const testSettings: Settings = { - verify: { - verifyTrust: true, - verifyAfterReading: true - }, - trust: { - trustAnchors: mainTrustList + '\n' + testCertificates.join('\n') - } + // allowed.pem = leaf/end-entity certs → allowedList + // anchors.pem = root CAs → appended to trustAnchors + const itlSettings: Settings = { + verify: { verifyTrust: true, verifyAfterReading: true }, + trust: { + trustAnchors: mainTrustList + '\n' + itlData.anchors, + allowedList: itlData.allowed, } + } - const reader2 = await c2pa.reader.fromBlob(mimeType, file, testSettings) - if (reader2) { - const testCrJson = await reader2.manifestStore() - await reader2.free() + const itlCrJson = await readManifestStore(itlSettings) + if (itlCrJson) { + const itlVr = getActiveManifestValidationStatus(itlCrJson) + console.log('ITL validation results:', { + success: itlVr?.success?.map((s) => s.code), + failure: itlVr?.failure?.map((f) => ({ code: f.code, explanation: f.explanation })) + }) - const testVr = getActiveManifestValidationStatus(testCrJson) - const testUntrusted = testVr?.failure?.some( - (status: ValidationStatus) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED + const itlTrusted = itlVr?.success?.some( + (status) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_TRUSTED + ) + const itlStillUntrusted = itlVr?.failure?.some( + (status) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED + ) + + console.log('ITL validation check:', { itlTrusted, itlStillUntrusted }) + if (itlStillUntrusted) { + const untrustedFailure = itlVr?.failure?.find( + (status) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED ) + console.log('ITL still untrusted, reason:', untrustedFailure?.explanation) + } - console.log('Test cert validation results:', { - isUntrusted: testUntrusted, - success: testVr?.success?.map((s: ValidationStatus) => s.code), - failure: testVr?.failure?.map((f: ValidationStatus) => f.code) - }) - - if (officialUntrusted && !testUntrusted) { - console.log('✅ Test certificates made the difference - signature now trusted') - usedTestCerts = true - crJson = testCrJson - } else { - console.log('ℹ️ Test certificates loaded but not needed for validation') - } + if (itlTrusted && !itlStillUntrusted) { + console.log('✅ Signature validated by ITL') + usedITL = true + finalCrJson = itlCrJson + } else { + console.log('❌ Signature still not trusted even with ITL') } } + } - const mainVr = getActiveManifestValidationStatus(crJson) - const isUntrusted = mainVr?.failure?.some( - (status: ValidationStatus) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED - ) + console.log('✅ Manifest store retrieved with trust validation') - console.log('Main validation results:', { - isUntrusted, - success: mainVr?.success?.map((s: ValidationStatus) => s.code), - failure: mainVr?.failure?.map((f: ValidationStatus) => f.code) - }) - - let usedITL = false - let finalCrJson = crJson - - if (isUntrusted) { - console.log('⚠️ Signature untrusted on main list, checking ITL...') - - // allowed.pem = leaf/end-entity certs → allowedList - // anchors.pem = root CAs → appended to trustAnchors - const itlSettings: Settings = { - verify: { - verifyTrust: true, - verifyAfterReading: true - }, - trust: { - trustAnchors: mainTrustList + '\n' + itlData.anchors, - allowedList: itlData.allowed, - } + return { + crJson: finalCrJson, + usedITL, + usedTestCerts, + } +} + +/** + * When using the local WASM reader (which doesn't expose `resourceToBytes`), + * fall back to a packaged-SDK reader from the same file solely for resource resolution. + * The packaged SDK instance is cached so only one Web Worker is created. + */ +async function enrichThumbnailsViaPackagedSdk(crJson: CrJson, file: Blob, mimeType: string): Promise { + // Quick check: any unresolved thumbnail identifiers? + let hasUnresolved = false + outer: for (const manifest of (crJson.manifests ?? [])) { + const assertions = (manifest.assertions ?? {}) as Record> + for (const [key, assertion] of Object.entries(assertions)) { + if (key.startsWith('c2pa.thumbnail') && assertion && !assertion.data && typeof assertion.identifier === 'string') { + hasUnresolved = true + break outer } + } + } + if (!hasUnresolved) return - const reader2 = await c2pa.reader.fromBlob(mimeType, file, itlSettings) - if (reader2) { - const itlCrJson = await reader2.manifestStore() - await reader2.free() + try { + const sdk = await getPackagedC2pa() + const reader = await sdk.reader.fromBlob(mimeType, file) + if (!reader || !reader.resourceToBytes) return + try { + await enrichThumbnails(crJson, reader.resourceToBytes.bind(reader)) + } finally { + await reader.free() + } + } catch (e) { + console.warn('[thumbnails] Could not resolve thumbnails via packaged SDK:', e) + } +} - const itlVr = getActiveManifestValidationStatus(itlCrJson) - console.log('ITL validation results:', { - success: itlVr?.success?.map((s: ValidationStatus) => s.code), - failure: itlVr?.failure?.map((f: ValidationStatus) => ({ code: f.code, explanation: f.explanation })) - }) +/** + * Resolve JUMBF `identifier` URIs in thumbnail assertions to inline base64 `data` fields. + * Only runs when the reader exposes `resourceToBytes`; silently skips failures. + */ +async function enrichThumbnails(crJson: CrJson, resourceToBytes: (uri: string) => Promise): Promise { + for (const manifest of (crJson.manifests ?? [])) { + const assertions = (manifest.assertions ?? {}) as Record> + for (const [key, assertion] of Object.entries(assertions)) { + if (!key.startsWith('c2pa.thumbnail') || !assertion || typeof assertion !== 'object') continue + if (assertion.data) continue // already inlined + const identifier = assertion.identifier + if (typeof identifier !== 'string') continue + try { + const bytes = await resourceToBytes(identifier) + // Convert to base64 in chunks to avoid call-stack limits on large thumbnails + const chunkSize = 8192 + let binary = '' + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)) + } + assertion.data = `b64'${btoa(binary)}'` + } catch { + // Non-fatal: skip thumbnails we can't resolve + } + } + } +} - const itlTrusted = itlVr?.success?.some( - (status: ValidationStatus) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_TRUSTED - ) - const itlStillUntrusted = itlVr?.failure?.some( - (status: ValidationStatus) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED - ) +async function extractCrJsonWithMetadata(file: File, testCertificates: string[] = []): Promise { + // JSON sidecars are crJSON reports — parse them directly without the SDK. + const ext = file.name.split('.').pop()?.toLowerCase() ?? '' + if (ext === 'json' || file.type === 'application/json') { + let parsed: unknown + try { + parsed = JSON.parse(await file.text()) + } catch { + throw new Error('This file is not valid JSON.') + } + if (!isCrJson(parsed)) { + throw new Error('No C2PA manifest found in this file.') + } + return { crJson: parsed, usedITL: false, usedTestCerts: false } + } - console.log('ITL validation check:', { itlTrusted, itlStillUntrusted }) - if (itlStillUntrusted) { - const untrustedFailure = itlVr?.failure?.find( - (status: ValidationStatus) => status.code === VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED - ) - console.log('ITL still untrusted, reason:', untrustedFailure?.explanation) - } + const mimeType = resolveMimeType(file) + console.log('🔍 Starting file processing for:', file.name, 'Type:', file.type, mimeType !== file.type ? `(remapped to ${mimeType})` : '') - if (itlTrusted && !itlStillUntrusted) { - console.log('✅ Signature validated by ITL') - usedITL = true - finalCrJson = itlCrJson - } else { - console.log('❌ Signature still not trusted even with ITL') - } + // Sidecar JUMBF inspected without an asset: route through the local WASM's + // `read_sidecar_integrity_only`, which calls c2pa-rs + // `with_manifest_data_and_stream_async` with an empty asset stream. This + // validates the JUMBF structure, signature, and certificate chain while + // leaving the asset-hash assertions to report `dataHash.mismatch` (expected — + // there is no asset to hash). The packaged SDK has no equivalent path. + console.log('Initializing C2PA SDK...') + const c2pa = await initC2pa() + console.log('✅ C2PA SDK initialized') + + const fromSidecarIntegrityOnly = c2pa.reader.fromSidecarIntegrityOnly + const sidecarBytesPromise = mimeType === SIDECAR_MIME && fromSidecarIntegrityOnly + ? file.arrayBuffer().then(buf => new Uint8Array(buf)) + : null + + const readManifestStore: ReadManifestStore = async (settings) => { + const reader = sidecarBytesPromise && fromSidecarIntegrityOnly + ? await fromSidecarIntegrityOnly(await sidecarBytesPromise, settings) + : await c2pa.reader.fromBlob(mimeType, file, settings) + if (!reader) return null + try { + const crJson = await reader.manifestStore() + if (reader.resourceToBytes) { + await enrichThumbnails(crJson, reader.resourceToBytes.bind(reader)) + } else { + await enrichThumbnailsViaPackagedSdk(crJson, file, mimeType) } + return crJson + } finally { + await reader.free() } + } - console.log('✅ Manifest store retrieved with trust validation') + try { + const result = await runTrustValidationFlow( + readManifestStore, + testCertificates, + mimeType === SIDECAR_MIME + ? 'No C2PA manifest could be read from this sidecar. It may be corrupted or not a valid .c2pa file.' + : 'No C2PA manifest found in this file', + ) - return { - crJson: finalCrJson, - usedITL, - usedTestCerts, - } + return result } catch (error) { console.error('❌ Error in processFile:', error) - if (error instanceof Error) { - const msg = error.message - if (msg.includes('UnsupportedFormatError') || msg.includes('Unsupported format')) { - throw new Error(`Unsupported file format (${mimeType}). Supported formats include JPEG, PNG, WebP, AVIF, MP4, MOV, MP3, WAV, and PDF.`) - } - if (msg.includes('InvalidAsset') || msg.includes('Box size extends beyond') || msg.includes('box size')) { - throw new Error(`Could not parse this file. It may be corrupted, use an unsupported codec, or the C2PA manifest may be malformed.`) - } - if (msg.includes('NoManifest') || msg.includes('no manifest')) { - throw new Error(`No C2PA manifest found in this file.`) - } - throw new Error(`Failed to process file: ${msg}`) + const msg = error instanceof Error ? error.message : String(error) + if (msg.includes('UnsupportedFormatError') || msg.includes('Unsupported format')) { + throw new Error(`This file format (${mimeType}) cannot carry embedded C2PA provenance. To validate provenance for this asset, drop it together with its companion .c2pa sidecar file.`) } - throw error + if (msg.includes('InvalidAsset') || msg.includes('Box size extends beyond') || msg.includes('box size')) { + throw new Error(`Could not parse this file. It may be corrupted, use an unsupported codec, or the C2PA manifest may be malformed.`) + } + if (msg.includes('NoManifest') || msg.includes('no manifest') || msg.includes('No C2PA manifest') || msg.includes('no JUMBF data')) { + throw new Error(`No C2PA manifest found in this file.`) + } + throw new Error(`Failed to process file: ${msg}`) } } @@ -452,13 +657,11 @@ export async function extractCrJson(file: File, testCertificates: string[] = []) return crJson } -export async function processFile(file: File, testCertificates: string[] = []): Promise { - const { crJson, usedITL, usedTestCerts } = await extractCrJsonWithMetadata(file, testCertificates) - +function buildConformanceReport(extracted: ExtractedCrJsonResult): ConformanceReport { return { - ...crJson, - usedITL, - usedTestCerts, + ...extracted.crJson, + usedITL: extracted.usedITL, + usedTestCerts: extracted.usedTestCerts, _conformanceToolVersion: { commit: VERSION_INFO.sha, shortCommit: VERSION_INFO.shortSha, @@ -469,6 +672,68 @@ export async function processFile(file: File, testCertificates: string[] = []): } } +export async function processFile(file: File, testCertificates: string[] = []): Promise { + return buildConformanceReport(await extractCrJsonWithMetadata(file, testCertificates)) +} + +async function extractSidecarWithAssetCrJsonWithMetadata( + sidecar: File, + asset: File, + testCertificates: string[] = [], +): Promise { + const c2pa = await initC2pa() + const fromSidecarAndBlob = c2pa.reader.fromSidecarAndBlob + if (!fromSidecarAndBlob) { + throw new Error('read_sidecar_manifest_store is not available in the local WASM build.') + } + + const assetMimeType = resolveMimeType(asset) + const sidecarBytes = new Uint8Array(await sidecar.arrayBuffer()) + + const readManifestStore: ReadManifestStore = async (settings) => { + const reader = await fromSidecarAndBlob(sidecarBytes, assetMimeType, asset, settings) + if (!reader) return null + try { + const crJson = await reader.manifestStore() + if (reader.resourceToBytes) { + await enrichThumbnails(crJson, reader.resourceToBytes.bind(reader)) + } else { + await enrichThumbnailsViaPackagedSdk(crJson, asset, assetMimeType) + } + return crJson + } finally { + await reader.free() + } + } + + try { + return await runTrustValidationFlow( + readManifestStore, + testCertificates, + `No C2PA manifest could be read from sidecar "${sidecar.name}" paired with "${asset.name}".`, + ) + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + if (msg.includes('HashMismatch') || msg.includes('dataHash') || msg.includes('bmffHash')) { + throw new Error( + `Asset hash mismatch: the sidecar's hash bindings don't match "${asset.name}". ` + + `The sidecar and asset are probably not a matched pair.`, + ) + } + throw error + } +} + +export async function processSidecarWithAsset( + sidecar: File, + asset: File, + testCertificates: string[] = [], +): Promise { + return buildConformanceReport( + await extractSidecarWithAssetCrJsonWithMetadata(sidecar, asset, testCertificates), + ) +} + /** * Get the C2PA library version */ diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 50d138e..7ab9588 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -58,3 +58,22 @@ export const PEM_MARKERS = { TRUSTED_CERT_BEGIN: '-----BEGIN TRUSTED CERTIFICATE-----', TRUSTED_CERT_END: '-----END TRUSTED CERTIFICATE-----' } as const + +// Validation Failure Descriptions (used when these codes appear in the failure list) +export const VALIDATION_FAILURE_DESCRIPTIONS: Record = { + [VALIDATION_STATUS.SIGNING_CREDENTIAL_UNTRUSTED]: 'The signing certificate is not trusted by the configured trust store.', + [VALIDATION_STATUS.SIGNING_CREDENTIAL_EXPIRED]: 'The signing certificate has expired.', + [VALIDATION_STATUS.SIGNING_CREDENTIAL_OCSP_REVOKED]: 'The signing certificate has been revoked.', + [VALIDATION_STATUS.TIMESTAMP_UNTRUSTED]: 'The timestamp signature is untrusted.', + [VALIDATION_STATUS.CLAIM_SIGNATURE_INVALID]: 'The claim signature is invalid (the manifest may have been tampered with).', + [VALIDATION_STATUS.ASSERTION_HASHED_URI_MATCH]: 'An assertion hash did not match (possible tampering of assertion data).', + [VALIDATION_STATUS.ASSERTION_DATA_HASH_MATCH]: 'An assertion data hash did not match.', + 'assertion.hashedURI.mismatch': 'An assertion hash did not match (possible tampering of assertion data).', + 'assertion.dataHash.mismatch': 'An assertion data hash did not match.', + 'assertion.bmffHash.mismatch': 'BMFF hash mismatch. The media content may have been tampered with.', + 'manifest.multipleActive': 'Multiple active manifests found.', + 'manifest.update.invalid': 'Invalid manifest update.', + 'algorithm.unsupported': 'Unsupported cryptographic algorithm.', + 'general.error': 'An unexpected validation error occurred.' +} as const + diff --git a/src/lib/crjson.test.ts b/src/lib/crjson.test.ts new file mode 100644 index 0000000..8a5d80a --- /dev/null +++ b/src/lib/crjson.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest' +import { getAllValidationFailures, type CrJson } from './crjson' + +describe('crjson utilities', () => { + describe('getAllValidationFailures', () => { + it('should return empty array if no failures', () => { + const report: CrJson = { + manifests: [ + { + label: 'active', + assertions: {}, + validationResults: { + success: [{ code: 'signingCredential.trusted' }] + } + } + ] + } + expect(getAllValidationFailures(report)).toEqual([]) + }) + + it('should collect document-level failures', () => { + const report: CrJson = { + manifests: [{ label: 'active', assertions: {} }], + validationResults: { + failure: [{ code: 'general.error', explanation: 'error' }] + } + } + expect(getAllValidationFailures(report)).toEqual([ + { code: 'general.error', explanation: 'error' } + ]) + }) + + it('should collect activeManifest failures from document-level validationResults', () => { + const report: CrJson = { + manifests: [{ label: 'active', assertions: {} }], + validationResults: { + activeManifest: { + failure: [{ code: 'signingCredential.untrusted' }] + } + } + } + expect(getAllValidationFailures(report)).toEqual([ + { code: 'signingCredential.untrusted' } + ]) + }) + + it('should collect failures from active manifest per-manifest validationResults', () => { + const report: CrJson = { + manifests: [ + { + label: 'active', + assertions: {}, + validationResults: { + failure: [{ code: 'signingCredential.untrusted' }] + } + } + ] + } + expect(getAllValidationFailures(report)).toEqual([ + { code: 'signingCredential.untrusted' } + ]) + }) + + it('should collect failures from ingredient manifests', () => { + const report: CrJson = { + manifests: [ + { + label: 'active', + assertions: {}, + validationResults: { + success: [{ code: 'signingCredential.trusted' }] + } + }, + { + label: 'ingredient', + assertions: {}, + validationResults: { + failure: [{ code: 'claimSignature.invalid', explanation: 'bad sig' }] + } + } + ] + } + expect(getAllValidationFailures(report)).toEqual([ + { code: 'claimSignature.invalid', explanation: 'bad sig' } + ]) + }) + + it('should de-duplicate failures by code', () => { + const report: CrJson = { + manifests: [ + { + label: 'active', + assertions: {}, + validationResults: { + failure: [{ code: 'signingCredential.untrusted', explanation: '1' }] + } + }, + { + label: 'ingredient', + assertions: {}, + validationResults: { + failure: [{ code: 'signingCredential.untrusted', explanation: '2' }] + } + } + ] + } + expect(getAllValidationFailures(report)).toEqual([ + { code: 'signingCredential.untrusted', explanation: '1' } + ]) + }) + }) +}) diff --git a/src/lib/crjson.ts b/src/lib/crjson.ts index c9935ff..9b7a9f9 100644 --- a/src/lib/crjson.ts +++ b/src/lib/crjson.ts @@ -82,7 +82,7 @@ export interface CrJsonClaimInfo { /** Detect if parsed JSON is crJSON format */ export function isCrJson(obj: unknown): obj is CrJson { const o = obj as Record - return Array.isArray(o?.manifests) && o.manifests.length > 0 && o['@context'] != null + return Array.isArray(o?.manifests) && o.manifests.length > 0 } /** Read assertions as list from crJSON manifest.assertions (object → array of { label, data }) */ @@ -199,6 +199,88 @@ export function getActiveManifestValidationStatus(report: CrJson): CrJsonActiveM return docLevel ?? (perManifest ? { success: perManifest.success, informational: perManifest.informational, failure: perManifest.failure } : undefined) } +/** + * Get all validation failures from the report, including document-level, + * active manifest, and all ingredient manifests. + */ +export function getAllValidationFailures(report: CrJson): CrJsonValidationStatus[] { + const failures: CrJsonValidationStatus[] = [] + + // 1. Document-level failures + if (report.validationResults?.failure) { + failures.push(...report.validationResults.failure) + } + if (report.validationResults?.activeManifest?.failure) { + failures.push(...report.validationResults.activeManifest.failure) + } + + // 2. Per-manifest failures (active and ingredients) + if (report.manifests) { + for (const manifest of report.manifests) { + const perManifest = manifest.validationResults as CrJsonValidationResults | undefined + if (perManifest?.failure) { + failures.push(...perManifest.failure) + } + } + } + + // De-duplicate by code + const uniqueFailures: CrJsonValidationStatus[] = [] + const seenCodes = new Set() + for (const f of failures) { + if (!seenCodes.has(f.code)) { + seenCodes.add(f.code) + uniqueFailures.push(f) + } + } + + return uniqueFailures +} + +/** + * Get validation status for a specific manifest from crJSON. + * - Supports per-manifest results (native crJSON) on `m.validationResults`. + * - Fallback to document-level results (legacy) for the active manifest (isFirst = true). + */ +export function getManifestValidationStatus( + report: CrJson, + m: CrJsonManifestEntry, + isFirst: boolean +): CrJsonActiveManifestStatus | undefined { + // 1. Try per-manifest status (c2pa-rs style crJSON) + const perManifest = m.validationResults as CrJsonValidationResults | undefined + if (perManifest && (perManifest.success?.length ?? 0) + (perManifest.failure?.length ?? 0) + (perManifest.informational?.length ?? 0) > 0) { + return { + success: perManifest.success, + informational: perManifest.informational, + failure: perManifest.failure + } + } + + // 2. Fallback to document-level for active manifest (legacy) + if (isFirst) { + const docLevel = report.validationResults?.activeManifest + if (docLevel && (docLevel.success?.length ?? 0) + (docLevel.failure?.length ?? 0) + (docLevel.informational?.length ?? 0) > 0) { + return docLevel + } + // If legacy has it flat at root + if (report.validationResults) { + const vr = report.validationResults + if ((vr.success?.length ?? 0) + (vr.failure?.length ?? 0) + (vr.informational?.length ?? 0) > 0) { + return { + success: vr.success, + informational: vr.informational, + failure: vr.failure + } + } + } + } + + return undefined +} + + + /** * Convert legacy ManifestStore (from Reader.json() / packaged SDK) to crJSON. * Use only when receiving legacy format; native path is already crJSON. diff --git a/src/lib/generateSummary.test.ts b/src/lib/generateSummary.test.ts new file mode 100644 index 0000000..e604122 --- /dev/null +++ b/src/lib/generateSummary.test.ts @@ -0,0 +1,309 @@ +/** + * Tests for the rubric-driven manifest summary generator. + * + * The summary's _detection_ layer comes from the signals rubric — these tests + * feed `generateManifestSummary` synthesised `ManifestSignalsResult` values + * (the same shape the rubric evaluator returns) and assert the resulting + * sentence + details. Real-world parity is covered separately by the + * goldens against `__fixtures__/*.signals.json`. + */ +import { describe, expect, it } from 'vitest' +import { generateManifestSummary } from './generateSummary' +import type { CrJsonManifestEntry } from './crjson' +import type { ManifestSignalsResult, SignalHit } from './rubrics/types' + +// ── Fixture builders ────────────────────────────────────────────────── + +function manifest(overrides: Partial = {}): CrJsonManifestEntry { + return { + label: 'urn:c2pa:test', + assertions: {}, + ...overrides, + } +} + +function withCert(m: CrJsonManifestEntry, common_name: string, issuer = 'C2PA Test CA'): CrJsonManifestEntry { + // c2pa-rs crJSON shape: signature.certificateInfo.{subject,issuer} are DN + // objects keyed by CN/O/OU/etc. The crjson.ts getSignatureInfo helper + // pulls common_name from `subject.CN` (not from a top-level field). + return { + ...m, + signature: { + alg: 'es256', + certificateInfo: { + subject: { CN: common_name }, + issuer: { CN: issuer }, + }, + }, + } +} + +function withClaimGenerator(m: CrJsonManifestEntry, name: string): CrJsonManifestEntry { + return { ...m, claim: { claim_generator_info: [{ name }] } } +} + +function withCamera(m: CrJsonManifestEntry, make: string, model: string): CrJsonManifestEntry { + return { + ...m, + assertions: { + ...m.assertions, + 'stds.exif': { make, model }, + }, + } +} + +function withSoftware(m: CrJsonManifestEntry, agent: string): CrJsonManifestEntry { + return { + ...m, + assertions: { + ...m.assertions, + 'c2pa.actions': { + actions: [{ action: 'c2pa.edited', softwareAgent: agent }], + }, + }, + } +} + +function signals(traits: { inceptions?: string[]; transformations?: string[] } = {}): ManifestSignalsResult { + const hit = (id: string): SignalHit => ({ trait: id, reportText: id, multiple: false }) + return { + assertedBy: { CN: 'Test', O: 'Test' }, + mimeType: null, + localInceptions: (traits.inceptions ?? []).map(hit), + localTransformations: (traits.transformations ?? []).map(hit), + allActionsIncluded: false, + ingredients: [], + } +} + +// ── Origin-phrase tests ─────────────────────────────────────────────── + +describe('generateManifestSummary · origin phrases (signals-driven)', () => { + it('captured media + camera info → "photo taken with a {camera}"', () => { + const m = withCamera(withCert(manifest(), 'Pixel Camera'), 'Google', 'Pixel 8') + const s = signals({ inceptions: ['inception:signal_capturedMedia'] }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is a photo taken with a Google Pixel 8.') + }) + + it('captured media without exif → "captured {media}"', () => { + const m = withCert(manifest(), 'Some Camera') + const s = signals({ inceptions: ['inception:signal_capturedMedia'] }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is a captured image.') + }) + + it('stitched capture + camera → "stitched photo taken with a {camera}"', () => { + const m = withCamera(manifest(), 'Sony', 'A7') + const s = signals({ inceptions: ['inception:signal_capturedMediaStitched'] }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is a stitched photo taken with a Sony A7.') + }) + + it('fully GenAI + creator → "{media} generated by {creator}"', () => { + const m = withCert(manifest(), 'Adobe Firefly') + const s = signals({ inceptions: ['inception:signal_fullyGenAIMedia'] }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is an image generated by Adobe Firefly.') + }) + + it('fully GenAI without creator → "AI-generated {media}"', () => { + const s = signals({ inceptions: ['inception:signal_fullyGenAIMedia'] }) + const r = generateManifestSummary(manifest(), s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is an AI-generated image.') + }) + + it('non-GenAI digital creation + creator → "digital artwork created with {creator}"', () => { + const m = withSoftware(manifest(), 'Procreate') + const s = signals({ inceptions: ['inception:signal_nonGenAIDigitalCreation'] }) + const r = generateManifestSummary(m, s, [], 'image/png', false, true) + expect(r.sentence).toBe('This is a digital artwork created with Procreate.') + }) + + it('blank canvas → digital artwork branch', () => { + const m = withSoftware(manifest(), 'Photoshop') + const s = signals({ inceptions: ['inception:signal_blankCanvas'] }) + const r = generateManifestSummary(m, s, [], 'image/png', false, true) + expect(r.sentence).toBe('This is a digital artwork created with Photoshop.') + }) + + it('screen capture → "screen capture"', () => { + const s = signals({ inceptions: ['inception:signal_screenCaptureMayContainGenAI'] }) + const r = generateManifestSummary(manifest(), s, [], 'image/png', false, true) + expect(r.sentence).toBe('This is a screen capture.') + }) + + it('partly GenAI → "{media} composed by {creator}"', () => { + const m = withCert(manifest(), 'Some Editor') + const s = signals({ inceptions: ['inception:signal_partlyGenAICreation'] }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is an image composed by Some Editor.') + }) + + it('no signals fired + creator → generic "{media} from {creator}" fallback', () => { + const m = withCert(manifest(), 'Pixel Camera') + const s = signals() + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is an image from Pixel Camera.') + }) + + it('no signals + no creator → bare "{media}" fallback', () => { + const s = signals() + const r = generateManifestSummary(manifest(), s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is an image.') + }) + + it('signals=null behaves the same as no-signals fallback', () => { + const m = withCert(manifest(), 'Pixel Camera') + const r = generateManifestSummary(m, null, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is an image from Pixel Camera.') + }) +}) + +// ── Modification-phrase tests ───────────────────────────────────────── + +describe('generateManifestSummary · modification phrases', () => { + it('editorial AI overrides editorial non-AI', () => { + const m = withSoftware(manifest(), 'Photoshop') + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: [ + 'transformation:signal_editorialAI', + 'transformation:signal_editorialNonAI', + ], + }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toContain('modified using generative AI') + expect(r.sentence).not.toContain('edited in') + }) + + it('editorial possibly-GenAI also surfaces as "modified using generative AI"', () => { + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: ['transformation:signal_editorialPossiblyGenAI'], + }) + const r = generateManifestSummary(manifest(), s, [], 'image/jpeg', false, true) + expect(r.sentence).toContain('modified using generative AI') + }) + + it('editorial non-AI + tools → "edited in {tool1} and {tool2}"', () => { + const m = manifest({ + assertions: { + 'c2pa.actions': { + actions: [ + { action: 'c2pa.edited', softwareAgent: 'Photoshop' }, + { action: 'c2pa.filtered', softwareAgent: 'Lightroom' }, + ], + }, + }, + }) + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: ['transformation:signal_editorialNonAI'], + }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toContain('edited in Photoshop and Lightroom') + }) + + it('editorial non-AI without tool names → bare "edited"', () => { + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: ['transformation:signal_editorialNonAI'], + }) + const r = generateManifestSummary(manifest(), s, [], 'image/jpeg', false, true) + expect(r.sentence).toContain('edited') + expect(r.sentence).not.toContain('edited in') + }) + + it('non-editorial signal → "converted to a different format"', () => { + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: ['transformation:signal_nonEditorial'], + }) + const r = generateManifestSummary(manifest(), s, [], 'image/jpeg', false, true) + expect(r.sentence).toContain('converted to a different format') + }) +}) + +// ── Untrusted branch ────────────────────────────────────────────────── + +describe('generateManifestSummary · untrusted', () => { + it('untrusted asset uses claim-generator + signed-by, ignoring signals', () => { + const m = withClaimGenerator(withCert(manifest(), 'Some CN'), 'My App') + // Even with strong signals, untrusted branch wins. + const s = signals({ inceptions: ['inception:signal_fullyGenAIMedia'] }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, false) + expect(r.sentence).toBe('This is an image from My App signed by Some CN.') + }) +}) + +// ── Footnote details ────────────────────────────────────────────────── + +describe('generateManifestSummary · details', () => { + it('lists certificate issuer and ingredient count', () => { + const m = withCert(manifest(), 'Pixel Camera', 'C2PA CA Inc') + const s = signals({ inceptions: ['inception:signal_capturedMedia'] }) + const r = generateManifestSummary( + m, + s, + [{ title: 'src.jpg', document_id: 'a', instance_id: 'b' }], + 'image/jpeg', + false, + true, + ) + expect(r.details).toContain('Certificate issued by C2PA CA Inc') + expect(r.details).toContain('Based on 1 source asset') + }) + + it('pluralises source asset count > 1', () => { + const ingredients = [ + { title: 'a.jpg', document_id: '1', instance_id: 'a' }, + { title: 'b.jpg', document_id: '2', instance_id: 'b' }, + ] + const r = generateManifestSummary(manifest(), signals(), ingredients, 'image/jpeg', false, true) + expect(r.details).toContain('Based on 2 source assets') + }) +}) + +// ── Grammar / composition ───────────────────────────────────────────── + +describe('generateManifestSummary · grammar', () => { + it('uses "an" before image and "a" before video/audio', () => { + const r1 = generateManifestSummary(manifest(), null, [], 'image/jpeg', false, true) + expect(r1.sentence).toBe('This is an image.') + const r2 = generateManifestSummary(manifest(), null, [], 'video/mp4', false, true) + expect(r2.sentence).toBe('This is a video.') + const r3 = generateManifestSummary(manifest(), null, [], 'audio/mpeg', false, true) + expect(r3.sentence).toBe('This is an audio file.') + }) + + it('joins origin + one modification with a comma', () => { + const m = withCamera(manifest(), 'Pixel', '') + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: ['transformation:signal_editorialAI'], + }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe('This is a photo taken with a Pixel camera, modified using generative AI.') + }) + + it('joins two modifications with " and "', () => { + const m = withSoftware(manifest(), 'Photoshop') + const s = signals({ + inceptions: ['inception:signal_capturedMedia'], + transformations: [ + 'transformation:signal_editorialNonAI', + 'transformation:signal_nonEditorial', + ], + }) + const r = generateManifestSummary(m, s, [], 'image/jpeg', false, true) + expect(r.sentence).toBe( + 'This is a captured image, edited in Photoshop and converted to a different format.', + ) + }) + + it('returns empty summary when manifest is null', () => { + const r = generateManifestSummary(null, null, [], 'image/jpeg', false, true) + expect(r).toEqual({ sentence: '', details: [] }) + }) +}) diff --git a/src/lib/generateSummary.ts b/src/lib/generateSummary.ts index ae287b6..568d40f 100644 --- a/src/lib/generateSummary.ts +++ b/src/lib/generateSummary.ts @@ -1,302 +1,334 @@ /** - * Generates a human-readable summary of a C2PA manifest. - * e.g. "This photo was taken with a Sony camera and edited in Adobe Photoshop." - * Reads directly from crJSON manifest entry via getters. + * Build a one-line, human-readable summary of a C2PA manifest: + * "This is an image taken with a Pixel Camera, edited in Adobe Photoshop." + * + * The detection layer (what kind of asset is this? was it AI-generated? was + * it edited?) is sourced from the **signals rubric** + * (`public/rubrics/asset-rubric-signals-local.yml`) so the truth source is + * the same YAML the Conformance TF maintains. This file only handles the + * narrative composition: picking templates, joining clauses, getting + * grammar right, and pulling a few precision fields (camera make/model, + * editing-software names) that the rubric doesn't return. + * + * To regenerate the input `signals: ManifestSignalsResult`, run the signals + * rubric via `evaluatePerManifest()` (see `lib/rubrics/perManifest.ts`). */ import type { CrJsonManifestEntry, CrJsonIngredientItem } from './types' -import { getAssertionDataByLabel, getClaimInfo, getSignatureInfo, getAssertionsList } from './crjson' - -// IPTC digital source types -const SOURCE_TYPE = { - TRAINED_ALGORITHMIC: 'trainedAlgorithmicMedia', - COMPOSITE_AI: 'compositeWithTrainedAlgorithmicMedia', - DIGITAL_CAPTURE: 'digitalCapture', - DIGITAL_ART: 'digitalArt', - SCREEN_CAPTURE: 'screenCapture', - COMPUTATIONAL: 'computationalMedia', - COMPOSITE: 'compositeSynthetic', - MINOR_HUMAN_EDITS: 'minorHumanEdits', - ALGORITHMIC_MEDIA: 'algorithmicMedia', +import { + getAssertionDataByLabel, + getAssertionsList, + getClaimInfo, + getSignatureInfo, +} from './crjson' +import type { ManifestSignalsResult } from './rubrics/types' + +// Signal ids exposed by `asset-rubric-signals-local.yml`. Centralised here +// so a typo or upstream rename surfaces as a single point of change. +const INCEPTION = { + BLANK_CANVAS: 'inception:signal_blankCanvas', + CAPTURED_MEDIA: 'inception:signal_capturedMedia', + CAPTURED_MEDIA_STITCHED: 'inception:signal_capturedMediaStitched', + COMPOSITION_MAY_GENAI: 'inception:signal_compositionMayContainGenAI', + FULLY_GENAI: 'inception:signal_fullyGenAIMedia', + UNKNOWN_PROVENANCE: 'inception:signal_mediaUnknownProvenance', + NON_GENAI_DIGITAL_CREATION: 'inception:signal_nonGenAIDigitalCreation', + PARTLY_GENAI: 'inception:signal_partlyGenAICreation', + SCREEN_CAPTURE_MAY_GENAI: 'inception:signal_screenCaptureMayContainGenAI', } as const -// C2PA action types -const ACTION = { - CONVERTED: 'c2pa.converted', - EDITED: 'c2pa.edited', - FILTERED: 'c2pa.filtered', - COLOR_ADJUSTED: 'c2pa.color_adjustments', - RESIZED: 'c2pa.resized', - OPENED: 'c2pa.opened', - PLACED: 'c2pa.placed', - CREATED: 'c2pa.created', - PUBLISHED: 'c2pa.published', - TRANSCODED: 'c2pa.transcoded', - AI_GENERATED: 'c2pa.ai.generatedContent', - DREW: 'c2pa.drew', - CROPPED: 'c2pa.cropped', - REPACKAGED: 'c2pa.repackaged', +const TRANSFORMATION = { + EDITORIAL_AI: 'transformation:signal_editorialAI', + EDITORIAL_NON_AI: 'transformation:signal_editorialNonAI', + EDITORIAL_POSSIBLY_GENAI: 'transformation:signal_editorialPossiblyGenAI', + NON_EDITORIAL: 'transformation:signal_nonEditorial', } as const -function cleanName(name: string): string { - if (!name) return '' - // Strip version suffixes like "AppName/1.0" or "AppName 1.0.0" - return name.split('/')[0].trim() +export interface ManifestSummary { + /** The composed sentence, capitalised and terminated with a period. */ + sentence: string + /** Footnote-style facts (e.g. "Certificate issued by …"). */ + details: string[] } -function getSourceType(url: string | undefined): string { - if (!url) return '' - const parts = url.split('/') - return parts[parts.length - 1] || '' -} +/** + * Compose the summary. Pure function; all inputs explicit. + * + * `signals` carries the per-manifest result for THIS manifest from the + * signals rubric. When the caller can't load the rubric (e.g. offline / + * dev fallback), pass `null` and we'll do a minimal "{a/an} {media} from + * {signer}" fallback that never claims a property the rubric would. + */ +export function generateManifestSummary( + manifest: CrJsonManifestEntry | null | undefined, + signals: ManifestSignalsResult | null, + ingredients: CrJsonIngredientItem[], + mimeType: string, + usedITL: boolean = false, + isTrusted: boolean = true, +): ManifestSummary { + if (!manifest) return { sentence: '', details: [] } -function getMediaWord(mimeType: string): string { - if (mimeType.startsWith('image/')) return 'image' - if (mimeType.startsWith('video/')) return 'video' - if (mimeType.startsWith('audio/')) return 'audio file' - if (mimeType === 'application/pdf') return 'document' - return 'file' -} + const mediaWord = getMediaWord(mimeType) + const article = articleFor(mediaWord) + const signerName = getSignerName(manifest, usedITL) -interface AssertionData { - actions?: Array<{ - action?: string - digitalSourceType?: string - softwareAgent?: string | { name?: string } - description?: string - }> - digitalSourceType?: string - make?: string - model?: string - softwareAgent?: string | { name?: string } -} + const parts: string[] = [] + const details: string[] = [] -function getAssertionData(manifest: CrJsonManifestEntry, label: string): AssertionData | null { - const data = getAssertionDataByLabel(manifest, label) - return (data as AssertionData) ?? null + if (!isTrusted) { + parts.push(buildUntrustedOrigin(manifest, mediaWord, article)) + } else { + parts.push(buildOriginPhrase(manifest, signals, mediaWord, article, signerName)) + const mods = buildModificationPhrases(manifest, signals) + if (mods.length > 0) parts.push(joinAnd(mods)) + } + + // Footnote: signing certificate issuer. + const issuer = getSignatureInfo(manifest)?.issuer + if (issuer) details.push(`Certificate issued by ${issuer}`) + + // Footnote: ingredient count. + const n = ingredients?.length ?? 0 + if (n > 0) details.push(`Based on ${n} source asset${n > 1 ? 's' : ''}`) + + return { sentence: composeSentence(parts), details } } -function getAllActions(manifest: CrJsonManifestEntry): Array<{ action: string; digitalSourceType?: string; softwareAgent?: string }> { - const actionsAssertion = getAssertionData(manifest, 'c2pa.actions') - if (!actionsAssertion?.actions) return [] - return actionsAssertion.actions.map(a => ({ - action: a.action ?? '', - digitalSourceType: a.digitalSourceType, - softwareAgent: typeof a.softwareAgent === 'string' - ? a.softwareAgent - : a.softwareAgent?.name, - })) +// ── Origin phrase ───────────────────────────────────────────────────── + +function buildUntrustedOrigin( + manifest: CrJsonManifestEntry, + mediaWord: string, + article: string, +): string { + const claimGenerator = getClaimGeneratorName(manifest) + const commonName = getCommonName(manifest) + let phrase = `This is ${article} ${mediaWord}` + if (claimGenerator) phrase += ` from ${claimGenerator}` + if (commonName) phrase += ` signed by ${commonName}` + return phrase } -function getPrimaryDigitalSourceType(manifest: CrJsonManifestEntry): string { - // Check top-level assertion first - const actionsData = getAssertionData(manifest, 'c2pa.actions') - if (actionsData?.digitalSourceType) { - return getSourceType(actionsData.digitalSourceType) +function buildOriginPhrase( + manifest: CrJsonManifestEntry, + signals: ManifestSignalsResult | null, + mediaWord: string, + article: string, + signerName: string, +): string { + const has = makeHas(signals) + const editingSoftware = getEditingSoftware(manifest) + const creator = editingSoftware[0] || signerName + + // Fully GenAI overrides everything else — the asset *is* AI output. + if (has(INCEPTION.FULLY_GENAI)) { + return creator + ? `This is ${article} ${mediaWord} generated by ${creator}` + : `This is an AI-generated ${mediaWord}` } - // Check individual actions - const actions = getAllActions(manifest) - for (const action of actions) { - if (action.digitalSourceType) { - return getSourceType(action.digitalSourceType) + // Captured media (with optional camera precision from EXIF). + if (has(INCEPTION.CAPTURED_MEDIA) || has(INCEPTION.CAPTURED_MEDIA_STITCHED)) { + const camera = getCameraInfo(manifest) + if (camera) { + const stitched = has(INCEPTION.CAPTURED_MEDIA_STITCHED) + return stitched + ? `This is a stitched photo taken with a ${camera}` + : `This is a photo taken with a ${camera}` } + return `This is a captured ${mediaWord}` } - // Check creative work assertion - const creativeWork = getAssertionData(manifest, 'stds.schema.org/CreativeWork') - if (creativeWork?.digitalSourceType) { - return getSourceType(creativeWork.digitalSourceType as unknown as string) + // Screen capture (rubric framing: "may contain GenAI"); we keep it short. + if (has(INCEPTION.SCREEN_CAPTURE_MAY_GENAI)) { + return `This is a screen capture` } - return '' + // Hand-crafted digital art / blank canvas / non-GenAI digital creation. + if (has(INCEPTION.NON_GENAI_DIGITAL_CREATION) || has(INCEPTION.BLANK_CANVAS)) { + return creator + ? `This is a digital artwork created with ${creator}` + : `This is a digital artwork` + } + + // Composite with GenAI mixed in — the asset isn't fully synthetic but is + // partly so. Sentence stays neutral; the modification clause carries the + // "uses GenAI" detail. + if (has(INCEPTION.PARTLY_GENAI) || has(INCEPTION.COMPOSITION_MAY_GENAI)) { + return creator + ? `This is ${article} ${mediaWord} composed by ${creator}` + : `This is ${article} composed ${mediaWord}` + } + + // Generic fallback — no inception signal fired. We say what we can without + // claiming the asset is anything specific. + return creator + ? `This is ${article} ${mediaWord} from ${creator}` + : `This is ${article} ${mediaWord}` } -function getSoftwareAgentName(agent: string | { name?: string } | undefined): string { - if (!agent) return '' - if (typeof agent === 'string') return cleanName(agent) - return cleanName(agent.name ?? '') +// ── Modification phrases ────────────────────────────────────────────── + +function buildModificationPhrases( + manifest: CrJsonManifestEntry, + signals: ManifestSignalsResult | null, +): string[] { + const has = makeHas(signals) + const phrases: string[] = [] + + // GenAI usage takes precedence over editorial-non-AI: if both fire, the + // GenAI signal is the one we want to surface. + if (has(TRANSFORMATION.EDITORIAL_AI) || has(TRANSFORMATION.EDITORIAL_POSSIBLY_GENAI)) { + phrases.push('modified using generative AI') + } else if (has(TRANSFORMATION.EDITORIAL_NON_AI)) { + const tools = getEditingSoftware(manifest).slice(0, 2).join(' and ') + phrases.push(tools ? `edited in ${tools}` : 'edited') + } + + if (has(TRANSFORMATION.NON_EDITORIAL)) { + phrases.push('converted to a different format') + } + + return phrases } +// ── Signal lookup helper ────────────────────────────────────────────── + +/** + * Returns a closure `has(traitId)` that's true when the signals rubric + * fired that trait on this manifest. When `signals` is null (rubric not + * loaded), every check returns false — the generic fallback then takes + * over so we never make claims we can't back up. + */ +function makeHas(signals: ManifestSignalsResult | null) { + if (!signals) return () => false + // Cache the union once; `has()` is called many times per render. + const traits = new Set([ + ...signals.localInceptions.map((s) => s.trait), + ...signals.localTransformations.map((s) => s.trait), + ]) + return (id: string) => traits.has(id) +} + +// ── Manifest field extractors (precision the rubric doesn't carry) ──── + +interface AssertionData { + digitalSourceType?: string + make?: string + model?: string + softwareAgent?: string | { name?: string } + actions?: Array<{ + action?: string + softwareAgent?: string | { name?: string } + }> +} + +function getAssertionData(manifest: CrJsonManifestEntry, label: string): AssertionData | null { + return (getAssertionDataByLabel(manifest, label) as AssertionData | undefined) ?? null +} + +/** Names of every `softwareAgent` referenced from any action. Order-preserving, deduplicated. */ function getEditingSoftware(manifest: CrJsonManifestEntry): string[] { const tools = new Set() - const actions = getAllActions(manifest) - for (const action of actions) { - if (action.softwareAgent) { - const name = getSoftwareAgentName(action.softwareAgent) + // Collect from all assertions whose payload looks like an actions list. + for (const { data } of getAssertionsList(manifest)) { + const actions = (data as AssertionData)?.actions + if (!Array.isArray(actions)) continue + for (const a of actions) { + const name = softwareAgentName(a.softwareAgent) + if (name) tools.add(name) + } + } + // Also check the `c2pa.actions` shape directly in case the iterator missed it. + const actionsAssertion = getAssertionData(manifest, 'c2pa.actions') + if (actionsAssertion?.actions) { + for (const a of actionsAssertion.actions) { + const name = softwareAgentName(a.softwareAgent) if (name) tools.add(name) } } return [...tools] } +function softwareAgentName(agent: AssertionData['softwareAgent']): string { + if (!agent) return '' + return cleanAgentName(typeof agent === 'string' ? agent : (agent.name ?? '')) +} + +/** Strip `Tool/version` and `Tool 1.0.0` suffixes; trim whitespace. */ +function cleanAgentName(name: string): string { + if (!name) return '' + return name.split('/')[0].trim() +} + +/** + * Best-effort camera identification from any assertion that carries + * `make`/`model`. We scan all assertions because the field appears under + * various labels (`stds.exif`, `c2pa.exif`, etc.) and we don't want to + * hardcode a specific one. + */ function getCameraInfo(manifest: CrJsonManifestEntry): string { for (const { data } of getAssertionsList(manifest)) { const d = data as AssertionData - if (d?.make || d?.model) { - const make = d.make ?? '' - const model = d.model ?? '' - if (make && model) return `${make} ${model}` - if (make) return `${make} camera` - if (model) return model - } + if (!d?.make && !d?.model) continue + const make = d.make ?? '' + const model = d.model ?? '' + if (make && model) return `${make} ${model}` + if (make) return `${make} camera` + if (model) return model } return '' } +// ── Identity / signer extractors ────────────────────────────────────── + function getSignerName(manifest: CrJsonManifestEntry, usedITL: boolean): string { - const claimInfo = getClaimInfo(manifest) - const sigInfo = getSignatureInfo(manifest) + // When the ITL signed the cert, prefer the claim-generator's friendly + // name over the cert CN — the CN is often the ITL's own identity, not + // the asset's creator. if (usedITL) { - const generatorName = claimInfo?.claim_generator_info?.[0]?.name - if (generatorName) return cleanName(generatorName) + const generator = getClaimInfo(manifest)?.claim_generator_info?.[0]?.name + if (generator) return cleanAgentName(generator) } - return sigInfo?.common_name ?? '' + return getSignatureInfo(manifest)?.common_name ?? '' } function getClaimGeneratorName(manifest: CrJsonManifestEntry): string { - const claimInfo = getClaimInfo(manifest) - const generatorName = claimInfo?.claim_generator_info?.[0]?.name - if (generatorName) return cleanName(generatorName) - return '' + const name = getClaimInfo(manifest)?.claim_generator_info?.[0]?.name + return name ? cleanAgentName(name) : '' } function getCommonName(manifest: CrJsonManifestEntry): string { return getSignatureInfo(manifest)?.common_name ?? '' } -function hasAIActions(manifest: CrJsonManifestEntry): boolean { - const actions = getAllActions(manifest) - return actions.some(a => a.action === ACTION.AI_GENERATED) -} - -function getHumanReadableActions(manifest: CrJsonManifestEntry): string[] { - const actions = getAllActions(manifest) - const descriptions: string[] = [] +// ── Grammar / composition helpers ───────────────────────────────────── - const editActions = [ACTION.EDITED, ACTION.FILTERED, ACTION.COLOR_ADJUSTED, ACTION.CROPPED, ACTION.RESIZED, ACTION.DREW] - const hasEdit = actions.some(a => editActions.includes(a.action as typeof editActions[number])) - if (hasEdit) descriptions.push('edited') - - if (actions.some(a => a.action === ACTION.CONVERTED || a.action === ACTION.TRANSCODED || a.action === ACTION.REPACKAGED)) { - descriptions.push('converted') - } - - return descriptions +function getMediaWord(mimeType: string): string { + if (mimeType.startsWith('image/')) return 'image' + if (mimeType.startsWith('video/')) return 'video' + if (mimeType.startsWith('audio/')) return 'audio file' + if (mimeType === 'application/pdf') return 'document' + return 'file' } -export interface ManifestSummary { - sentence: string - details: string[] +/** Pick `a` vs `an` based on the next word's leading sound. */ +function articleFor(nextWord: string): string { + return /^[aeiou]/i.test(nextWord) ? 'an' : 'a' } -export function generateManifestSummary( - manifest: CrJsonManifestEntry | null | undefined, - ingredients: CrJsonIngredientItem[], - mimeType: string, - usedITL: boolean = false, - isTrusted: boolean = true, -): ManifestSummary { - if (!manifest) return { sentence: '', details: [] } - - const mediaWord = getMediaWord(mimeType) - const sourceType = getPrimaryDigitalSourceType(manifest) - const signerName = getSignerName(manifest, usedITL) - const cameraInfo = getCameraInfo(manifest) - const editingSoftware = getEditingSoftware(manifest) - const aiActions = hasAIActions(manifest) - const humanActions = getHumanReadableActions(manifest) - - const parts: string[] = [] - const details: string[] = [] - - if (!isTrusted) { - // For untrusted signatures: "This is a [media] from [claim_generator] signed by [common_name]" - const claimGenerator = getClaimGeneratorName(manifest) - const commonName = getCommonName(manifest) - const articleSuffix = mimeType.startsWith('image/') ? 'n' : '' - let originPhrase = `This is a${articleSuffix} ${mediaWord}` - if (claimGenerator) originPhrase += ` from ${claimGenerator}` - if (commonName) originPhrase += ` signed by ${commonName}` - parts.push(originPhrase) - } else { - // --- Determine the origin phrase --- - const isFullyAIGenerated = sourceType === SOURCE_TYPE.TRAINED_ALGORITHMIC || sourceType === SOURCE_TYPE.ALGORITHMIC_MEDIA - const hasAIComposite = sourceType === SOURCE_TYPE.COMPOSITE_AI || aiActions - const isCameraCapture = sourceType === SOURCE_TYPE.DIGITAL_CAPTURE || !!cameraInfo - - if (isFullyAIGenerated) { - // "This is an AI-generated image" - const creator = editingSoftware[0] || signerName - if (creator) { - parts.push(`This is a${mimeType.startsWith('image/') ? 'n' : ''} ${mediaWord} generated by ${creator}`) - } else { - parts.push(`This is an AI-generated ${mediaWord}`) - } - } else if (isCameraCapture) { - // "This is a photo from a Sony camera" - if (cameraInfo) { - parts.push(`This is a photo taken with a ${cameraInfo}`) - } else { - parts.push(`This is a captured ${mediaWord}`) - } - } else if (sourceType === SOURCE_TYPE.DIGITAL_ART) { - const creator = editingSoftware[0] || signerName - parts.push(creator ? `This is a digital artwork created with ${creator}` : `This is a digital artwork`) - } else if (sourceType === SOURCE_TYPE.SCREEN_CAPTURE) { - parts.push(`This is a screen capture`) - } else { - // Generic fallback - const creator = editingSoftware[0] || signerName - parts.push(creator ? `This is a${mimeType.startsWith('image/') ? 'n' : ''} ${mediaWord} from ${creator}` : `This is a${mimeType.startsWith('image/') ? 'n' : ''} ${mediaWord}`) - } - - // --- Determine modifications --- - const modifications: string[] = [] - - if (hasAIComposite) { - modifications.push('modified using generative AI') - } else if (humanActions.includes('edited') && editingSoftware.length > 0) { - const toolList = editingSoftware.slice(0, 2).join(' and ') - modifications.push(`edited in ${toolList}`) - } else if (humanActions.includes('edited')) { - modifications.push('edited') - } - - if (humanActions.includes('converted')) { - modifications.push('converted to a different format') - } - - if (modifications.length > 0) { - parts.push(modifications.join(' and ')) - } - - } - - // --- Certificate issuer (from crJSON manifest.signature) --- - const issuer = getSignatureInfo(manifest)?.issuer - if (issuer) { - details.push(`Certificate issued by ${issuer}`) - } - - // --- Ingredient provenance --- - const ingredientCount = ingredients?.length ?? 0 - if (ingredientCount > 0) { - details.push(`Based on ${ingredientCount} source asset${ingredientCount > 1 ? 's' : ''}`) - } - - // Combine parts into a sentence - let sentence = '' - if (parts.length === 1) { - sentence = parts[0] + '.' - } else if (parts.length === 2) { - sentence = parts[0] + ', ' + parts[1] + '.' - } else if (parts.length > 2) { - sentence = parts.slice(0, -1).join(', ') + ', and ' + parts[parts.length - 1] + '.' - } - - // Capitalize first letter - sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1) +function joinAnd(items: string[]): string { + if (items.length <= 1) return items[0] ?? '' + if (items.length === 2) return `${items[0]} and ${items[1]}` + return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}` +} - return { sentence, details } +function composeSentence(parts: string[]): string { + if (parts.length === 0) return '' + let s: string + if (parts.length === 1) s = parts[0] + else if (parts.length === 2) s = `${parts[0]}, ${parts[1]}` + else s = `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}` + return s.charAt(0).toUpperCase() + s.slice(1) + '.' } diff --git a/src/lib/profileEvaluator.ts b/src/lib/profileEvaluator.ts deleted file mode 100644 index fc83795..0000000 --- a/src/lib/profileEvaluator.ts +++ /dev/null @@ -1,181 +0,0 @@ -type ProfileEvaluatorModule = { - default: () => Promise - /** Returns a JSON string (report); parse with JSON.parse so the UI gets a plain object with .statements etc. */ - evaluate_profile_wasm: (profileYaml: string, indicatorsJson: string) => unknown -} - -export type EvaluateProfileResult = - | { success: true; result: unknown } - | { success: false; error: string; detail?: string } - -const base = - typeof import.meta.env?.BASE_URL === 'string' ? import.meta.env.BASE_URL : '/' -const profileEvaluatorModuleUrl = `${base}profile-evaluator/profile_evaluator_rs.js` - -/** URL the app uses to load the profile-evaluator WASM (for debugging). */ -export const PROFILE_EVALUATOR_SCRIPT_URL = profileEvaluatorModuleUrl - -const importModule = new Function('modulePath', 'return import(modulePath)') as ( - modulePath: string -) => Promise - -let evaluatorModulePromise: Promise | null = null -let evaluatorModuleLoadError: string | null = null - -/** Clear cached load state so the next verify or evaluate will try loading the WASM again. */ -export function resetProfileEvaluatorLoad(): void { - evaluatorModulePromise = null - evaluatorModuleLoadError = null -} - -async function loadProfileEvaluatorModule(): Promise { - if (evaluatorModuleLoadError) return null - if (!evaluatorModulePromise) { - evaluatorModulePromise = (async () => { - try { - // Try dynamic import directly; some servers don't send application/javascript for .js - // or HEAD may fail, so we don't rely on a prior HEAD check. - const module = await importModule(profileEvaluatorModuleUrl) - await module.default() - return module - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err) - evaluatorModuleLoadError = msg - evaluatorModulePromise = null - if (import.meta.env?.DEV) { - console.warn('[profile-evaluator] load failed:', msg, err) - } - return null - } - })() - } - return evaluatorModulePromise -} - -/** Minimal profile + indicators that produce a non-empty report (one statement). */ -const WASM_VERIFY_PROFILE = `--- -profile_metadata: - language: en ---- -- id: wasm_verify - title: WASM verification - expression: "1 + 1" - report_text: - en: "2 = {{ expr \\"1+1\\" }}" -` - -const WASM_VERIFY_INDICATORS = {} - -export type VerifyWasmResult = - | { ok: true; result: unknown; rawType: string } - | { ok: false; error: string; detail?: string } - -/** - * Verifies the profile-evaluator WASM loads and runs correctly by evaluating - * a minimal profile with known-good inputs. Call this to confirm the WASM is working. - */ -export async function verifyWasm(): Promise { - const module = await loadProfileEvaluatorModule() - if (!module) { - return { - ok: false, - error: 'WASM not loaded', - detail: - evaluatorModuleLoadError ?? - 'Profile evaluator module not available.', - } - } - try { - const raw = module.evaluate_profile_wasm( - WASM_VERIFY_PROFILE, - JSON.stringify(WASM_VERIFY_INDICATORS) - ) - // WASM returns a JSON string; parse so we get a plain object with .statements etc. - let report: unknown = raw - if (typeof raw === 'string') { - try { - report = JSON.parse(raw) as unknown - } catch (e) { - return { - ok: false, - error: 'WASM returned invalid JSON', - detail: e instanceof Error ? e.message : String(e), - } - } - } - const rawType = typeof raw - const hasKeys = - report != null && - typeof report === 'object' && - !Array.isArray(report) && - Object.keys(report as object).length > 0 - if (import.meta.env?.DEV) { - console.log('[profile-evaluator] verifyWasm:', { - rawType, - keys: report != null && typeof report === 'object' ? Object.keys(report as object) : [], - }) - } - if (!hasKeys) { - return { - ok: false, - error: 'WASM returned empty result', - detail: `Expected non-empty report from minimal profile. Parsed type: ${typeof report}, keys: ${report != null && typeof report === 'object' ? Object.keys(report as object).join(', ') || 'none' : 'n/a'}.`, - } - } - return { ok: true, result: report, rawType: typeof report } - } catch (err) { - const message = err instanceof Error ? err.message : String(err) - return { - ok: false, - error: 'WASM verification failed', - detail: message, - } - } -} - -/** - * Evaluate a YAML profile against crJSON indicators. - * If the profile-evaluator WASM module is not available, returns a structured - * result with success: false and an error message instead of throwing. - */ -export async function evaluateProfile( - profileYaml: string, - indicators: unknown -): Promise { - const evaluatorModule = await loadProfileEvaluatorModule() - if (!evaluatorModule) { - return { - success: false, - error: 'Profile evaluator WASM not available', - detail: - evaluatorModuleLoadError ?? - 'Run "npm run copy:profile-evaluator" to copy the profile evaluator from a sibling profile-evaluator-rs repo into public/profile-evaluator/.', - } - } - try { - // WASM returns a JSON string (serde_json::to_string); parse so the UI gets a plain object with .statements etc. - const raw = evaluatorModule.evaluate_profile_wasm( - profileYaml, - JSON.stringify(indicators) - ) - let result: unknown - if (typeof raw === 'string') { - try { - result = JSON.parse(raw) as unknown - } catch { - result = raw - } - } else { - result = raw - } - if (import.meta.env?.DEV) { - console.log('[profile-evaluator] evaluate_profile_wasm:', { - keys: result != null && typeof result === 'object' && !Array.isArray(result) ? Object.keys(result as object) : [], - }) - } - return { success: true, result } - } catch (err) { - const message = err instanceof Error ? err.message : String(err) - return { success: false, error: 'Profile evaluation failed', detail: message } - } -} diff --git a/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.conformance.json b/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.json b/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.json new file mode 100644 index 0000000..8d329c7 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.json @@ -0,0 +1,411 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4", + "assertions": { + "c2pa.hash.bmff.v3": { + "exclusions": [ + { + "xpath": "/ftyp" + }, + { + "xpath": "/uuid", + "data": [ + { + "offset": 8, + "value": [ + 216, + 254, + 195, + 214, + 27, + 14, + 72, + 60, + 146, + 151, + 88, + 40, + 135, + 126, + 196, + 129 + ] + } + ] + }, + { + "xpath": "/mfra" + }, + { + "xpath": "/free" + } + ], + "alg": "sha256", + "hash": "b64'Ux5Rz5ubwnuUIIoHhVRXdctAKpn6acNujT22vMBJBF0=", + "name": "BMFF file hash", + "pad": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'wrGfYTzqcn+suUUwhQhscgSRNwvMIsWIrETRWC2cXEE=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.dubbed", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.assertions/c2pa.hash.bmff.v3" + }, + { + "code": "assertion.bmffHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.assertions/c2pa.hash.bmff.v3" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature" + } + ], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a", + "hash": "b64'cykuU1M2Tb/0uyFDDEoHWeef1AiO1AK16PXvEzSgZyk=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature", + "hash": "b64'TBF0HEF8zMnOH8xrDUOfb0hEsCZ1VT8q3yKlpNuXTA8=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "2e1ebd20-c38f-182f-ca20-4783b606bc7a", + "signature": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "852130983:867906598" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'wrGfYTzqcn+suUUwhQhscgSRNwvMIsWIrETRWC2cXEE=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'OZ3pss9ZPcxyUinHZHQr7ozQagxiVk1/ky/wCu5Urcs=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3", + "hash": "b64'gowprNbZfHrW+1IgggMrOM/dlDD9BPHl5lD/j0Jvr0w=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "46c75c60ba9c323de2720b726b45dde36c8135", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L4" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Android", + "CN": "Pixel Recorder" + }, + "validity": { + "notBefore": "2026-03-02T09:19:20+00:00", + "notAfter": "2026-04-01T09:19:19+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-03-04T00:40:25+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3" + }, + { + "code": "assertion.bmffHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "BMFF hash valid" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:28:36.137+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:a2b680ae-e454-2302-cbf8-46b5d3e407b4/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a", + "assertions": { + "c2pa.hash.bmff.v3": { + "exclusions": [ + { + "xpath": "/ftyp" + }, + { + "xpath": "/uuid", + "data": [ + { + "offset": 8, + "value": [ + 216, + 254, + 195, + 214, + 27, + 14, + 72, + 60, + 146, + 151, + 88, + 40, + 135, + 126, + 196, + 129 + ] + } + ] + }, + { + "xpath": "/mfra" + }, + { + "xpath": "/free" + } + ], + "alg": "sha256", + "hash": "b64'lMaJ8VQCEl71zLB1jph2Da7lb/5JwhI6NFknB2Svp/M=", + "name": "BMFF file hash", + "pad": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "e71a9a36-803b-c1fb-a749-407b13ca758e", + "signature": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "852130983:867906598" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Qc/GfIFN7B3jbO7vQGR1KhL28apGvWum5Vgq4Kp44I8=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3", + "hash": "b64'kjLaiX3kOeck0AapbM4yfQcFbEJRDhdIcXfq+13KPHI=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "a7d0f59d0e0866958134ca36aec4b0925b17ce", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Android", + "CN": "Pixel Recorder" + }, + "validity": { + "notBefore": "2026-03-02T08:17:07+00:00", + "notAfter": "2026-04-01T08:17:06+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-03-03T23:33:50+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6b16b24d-4eba-60b5-0218-469a7ab9c73a", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:28:36.137+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.signals.json b/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.signals.json new file mode 100644 index 0000000..6868930 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/audio-mixed-with-genai.signals.json @@ -0,0 +1,47 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Pixel Recorder", + "O": "Google LLC", + "OU": "Android" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Pixel Recorder", + "O": "Google LLC", + "OU": "Android" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.conformance.json b/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.json b/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.json new file mode 100644 index 0000000..62589ea --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.json @@ -0,0 +1,404 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 7914, + "length": 11921 + } + ], + "alg": "sha256", + "hash": "b64'wX+xWKfr47WQ0+CoSAVEc+VByyifF1rBibNwvseFpio=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'SdSVV2KWCssld25s1HaJd1alYQ4hICNuMR4Yl9icL50=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.adjustedColor", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced" + }, + { + "action": "c2pa.edited", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.hash.data" + } + ], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "hash": "b64'bZqinbxvhgEUlckIKo+WujqWWh//1moGY1iCCAdQ1bw=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature", + "hash": "b64'6Xo+hlxnspzDe0IGVH2Zk7LmPHkpSpWS+1QvLrP68T8=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "f200bf63-2d3b-2ebd-524d-4634d42132ac", + "signature": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "790468618:790840964" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'SdSVV2KWCssld25s1HaJd1alYQ4hICNuMR4Yl9icL50=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'ruOYYdoCKZI4INDLlrmDnGtja4ieuNsFTYt/wVQOPh8=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xRyKQcOttsGaxH++s12AWm0Z5lXkpURXCdOl7gK29uc=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "e62fe52c0b746e6b3722272b4b5e6309a31ac7", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google Photos Android", + "CN": "Google Photos" + }, + "validity": { + "notBefore": "2025-08-05T18:21:27+00:00", + "notAfter": "2025-09-04T18:21:26+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-05T19:58:32+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T01:15:05.157+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:7781b3f0-c26c-f617-2565-4e8687c2b753/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 18044 + }, + { + "start": 18068, + "length": 5179 + }, + { + "start": 23865, + "length": 1159 + }, + { + "start": 25028, + "length": 65458 + }, + { + "start": 90490, + "length": 65458 + }, + { + "start": 155952, + "length": 65458 + }, + { + "start": 221414, + "length": 65458 + }, + { + "start": 286876, + "length": 65458 + }, + { + "start": 352338, + "length": 65458 + }, + { + "start": 417800, + "length": 65458 + }, + { + "start": 483262, + "length": 65458 + }, + { + "start": 548724, + "length": 65458 + }, + { + "start": 614186, + "length": 22222 + } + ], + "alg": "sha256", + "hash": "b64'AYgJaZosMByPAxgPMmQuZQvmDR40K1vS3coZ/ebDcZI=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "ce00b058-a1ef-62f0-6bfd-4a942dcd0bd1", + "signature": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:788932941" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'c5W2clbb1LBbL/Mg3JvhCSyg6/QX039JudWWOU2Iyec=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Axs3an5HFczmpjJiejh1Z5lKQi2MVSH8g1NVbjSNOjI=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "5855fdfb595875cbd50e958c611d3eecb78b1d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-02T05:24:43+00:00", + "notAfter": "2025-09-01T05:24:42+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-05T19:42:03+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T01:15:05.157+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.signals.json b/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.signals.json new file mode 100644 index 0000000..50c7eb1 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-ai-and-non-ai-edits.signals.json @@ -0,0 +1,51 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Photos", + "O": "Google LLC", + "OU": "Google Photos Android" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_editorialNonAI", + "reportText": "Contains Editorial Non-GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture-crop.conformance.json b/src/lib/rubrics/__fixtures__/capture-crop.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-crop.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture-crop.json b/src/lib/rubrics/__fixtures__/capture-crop.json new file mode 100644 index 0000000..b56f574 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-crop.json @@ -0,0 +1,404 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 4763, + "length": 11890 + } + ], + "alg": "sha256", + "hash": "b64'8AoLcIe2wJMm5abCNn3Mmwh4nmwf4DOBKd5gfdIDBU4=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'SdSVV2KWCssld25s1HaJd1alYQ4hICNuMR4Yl9icL50=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.cropped", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/humanEdits" + }, + { + "action": "c2pa.enhanced", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature" + }, + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.assertions/c2pa.hash.data" + } + ], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "hash": "b64'bZqinbxvhgEUlckIKo+WujqWWh//1moGY1iCCAdQ1bw=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature", + "hash": "b64'6Xo+hlxnspzDe0IGVH2Zk7LmPHkpSpWS+1QvLrP68T8=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "9ba79de5-f1f7-d817-8734-4b7e798b7a29", + "signature": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "790468618:790840964" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'SdSVV2KWCssld25s1HaJd1alYQ4hICNuMR4Yl9icL50=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'ZtyUPh7ZeuzRR7YSiLoiOIo+cgdVT07zweDvYrzRSKg=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'8G3Hf7mlkfTX9qcxqairnSL9D1urbTxXbTkyPt/hcEQ=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "d30a76ecceb8263ec1de469b2a4239440e1bfd", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google Photos Android", + "CN": "Google Photos" + }, + "validity": { + "notBefore": "2025-08-05T18:21:27+00:00", + "notAfter": "2025-09-04T18:21:26+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-05T19:55:33+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T03:48:58.674+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:afc49daf-eb53-dade-4c73-4b76c548bb3d/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 18044 + }, + { + "start": 18068, + "length": 5179 + }, + { + "start": 23865, + "length": 1159 + }, + { + "start": 25028, + "length": 65458 + }, + { + "start": 90490, + "length": 65458 + }, + { + "start": 155952, + "length": 65458 + }, + { + "start": 221414, + "length": 65458 + }, + { + "start": 286876, + "length": 65458 + }, + { + "start": 352338, + "length": 65458 + }, + { + "start": 417800, + "length": 65458 + }, + { + "start": 483262, + "length": 65458 + }, + { + "start": 548724, + "length": 65458 + }, + { + "start": 614186, + "length": 22222 + } + ], + "alg": "sha256", + "hash": "b64'AYgJaZosMByPAxgPMmQuZQvmDR40K1vS3coZ/ebDcZI=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "ce00b058-a1ef-62f0-6bfd-4a942dcd0bd1", + "signature": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:788932941" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'c5W2clbb1LBbL/Mg3JvhCSyg6/QX039JudWWOU2Iyec=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Axs3an5HFczmpjJiejh1Z5lKQi2MVSH8g1NVbjSNOjI=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "5855fdfb595875cbd50e958c611d3eecb78b1d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-02T05:24:43+00:00", + "notAfter": "2025-09-01T05:24:42+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-05T19:42:03+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:eecfbd16-c3ee-4ea0-4337-45682d534294", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T03:48:58.674+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture-crop.signals.json b/src/lib/rubrics/__fixtures__/capture-crop.signals.json new file mode 100644 index 0000000..6c0aa11 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-crop.signals.json @@ -0,0 +1,51 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Photos", + "O": "Google LLC", + "OU": "Google Photos Android" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialNonAI", + "reportText": "Contains Editorial Non-GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.conformance.json b/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.json b/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.json new file mode 100644 index 0000000..acd38c7 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.json @@ -0,0 +1,606 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 4764, + "length": 18727 + } + ], + "alg": "sha256", + "hash": "b64'oNGOk8dfwKamd2IWwKZn9FH0YZba1XAIGUz59OYeQ6A=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'YiYcZd+YmoiNmylw8GnsmqRBft5O4ZGLjFbTSG5s/Qw=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature" + } + ], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc", + "hash": "b64'U9oBBbmxrZ85LJBetk2ZcvOnDs6FcKnv/9i7JzdlZFE=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature", + "hash": "b64'fHh3sITKpfJNECRlQ01825Dw1FblPF/A5ezOwUV+J/k=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "21df8610-a42a-4191-bdfb-4ed314c77b98", + "signature": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "790468618:790840964" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'YiYcZd+YmoiNmylw8GnsmqRBft5O4ZGLjFbTSG5s/Qw=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'XobXR4QL3mEKoLL5uiPqLqwekt0Ao6uaC670WrMPW8U=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'sfZg6Om6IvtSYHHBqxH0E9RtyFgqH1t0hUtwlcvTalk=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "f7fee32e70be2f7ad7d966b403100df64bba6", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google Photos Android", + "CN": "Google Photos" + }, + "validity": { + "notBefore": "2025-08-05T18:21:27+00:00", + "notAfter": "2025-09-04T18:21:26+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-07T21:05:49+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T10:18:21.746+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:b91b82f2-8770-7ef0-5e2e-41c2b57a6481/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 4764, + "length": 11782 + } + ], + "alg": "sha256", + "hash": "b64'BrE3f7TufDAldkLNEVM8NMZGwbxup4DYECz8WGjGqdo=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'poiOA+IbJAQzIUeIRVgqGtxhhJ9uDjIE0RTfNgLoafc=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.cropped", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/humanEdits" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature" + }, + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.assertions/c2pa.hash.data" + } + ], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e", + "hash": "b64'9PL+68QiTQYM6s6Dq3WDnc036VdZhq6rinGTaNi/aCc=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature", + "hash": "b64'ikee1NXy21wArj5ZkPB1wX7Q6b0bcwksPyS+hPVGbaU=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "a85fa325-d9b8-cb54-0e4a-478878fe8318", + "signature": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "790468618:790840964" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'poiOA+IbJAQzIUeIRVgqGtxhhJ9uDjIE0RTfNgLoafc=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'/Us7UGaaQyLBUT/PEaw94tGctHVe/KidoHuVtox2Wik=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'rWL428S6sS+6F/nuu6KPx7of0Y1/HasU7RfdBn1dGOM=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "e8c55ffdb991d98b248d886135ad5ee225e75c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google Photos Android", + "CN": "Google Photos" + }, + "validity": { + "notBefore": "2025-08-05T18:21:27+00:00", + "notAfter": "2025-09-04T18:21:26+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-07T21:04:09+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T10:18:21.746+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:8edfb78e-661f-394a-f1b5-4c9946740ffc/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 30632 + }, + { + "start": 30656, + "length": 5181 + }, + { + "start": 36455, + "length": 1158 + }, + { + "start": 37617, + "length": 65458 + }, + { + "start": 103079, + "length": 65458 + }, + { + "start": 168541, + "length": 65458 + }, + { + "start": 234003, + "length": 65458 + }, + { + "start": 299465, + "length": 65458 + }, + { + "start": 364927, + "length": 65458 + }, + { + "start": 430389, + "length": 65458 + }, + { + "start": 495851, + "length": 65458 + }, + { + "start": 561313, + "length": 65458 + }, + { + "start": 626775, + "length": 12366 + } + ], + "alg": "sha256", + "hash": "b64'y6W4UFXTv4UdEoy/SKFl4A2kVVVWqkZPSvy7WQLPUfE=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "d0afb3cd-f150-50d1-29f2-408bb8c51c4c", + "signature": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:788932941" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'RYoCa7MQHz0xtftdEs4I27BLylOM3aGPWlB6g8mBoM4=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Axs3an5HFczmpjJiejh1Z5lKQi2MVSH8g1NVbjSNOjI=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "b6d014af6648c5633e710fa5cd80549b6d9f8c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-02T05:25:09+00:00", + "notAfter": "2025-09-01T05:25:08+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-07T21:00:08+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:f641bc5e-19fb-46c8-6b3c-4cbb47df508e", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T10:18:21.746+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.signals.json b/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.signals.json new file mode 100644 index 0000000..4b2ec4b --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-non-ai-then-ai-edits.signals.json @@ -0,0 +1,69 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Photos", + "O": "Google LLC", + "OU": "Google Photos Android" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Google Photos", + "O": "Google LLC", + "OU": "Google Photos Android" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialNonAI", + "reportText": "Contains Editorial Non-GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 2, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture-panorama.conformance.json b/src/lib/rubrics/__fixtures__/capture-panorama.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-panorama.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture-panorama.json b/src/lib/rubrics/__fixtures__/capture-panorama.json new file mode 100644 index 0000000..c2ece47 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-panorama.json @@ -0,0 +1,461 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 4920752 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'y0kpFXeDy86udKXF+kj3j8Gc7uRjUfLeOn3T1IjMeik=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 4920752, + "length": 83067 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'9hj61bEFiY955OMGx0bMnHus9uo+OXPYumvu9SlgPqs=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'AA==" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'q1SZYPsEms/e0tD3k3XGgBgFqPy7O5VVEVDZRCYeHuQ=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 1019 + }, + { + "start": 1043, + "length": 7840 + }, + { + "start": 9501, + "length": 1063 + }, + { + "start": 10568, + "length": 65458 + }, + { + "start": 76030, + "length": 65458 + }, + { + "start": 141492, + "length": 65458 + }, + { + "start": 206954, + "length": 65458 + }, + { + "start": 272416, + "length": 65458 + }, + { + "start": 337878, + "length": 65458 + }, + { + "start": 403340, + "length": 65458 + }, + { + "start": 468802, + "length": 65458 + }, + { + "start": 534264, + "length": 65458 + }, + { + "start": 599726, + "length": 65458 + }, + { + "start": 665188, + "length": 65458 + }, + { + "start": 730650, + "length": 65458 + }, + { + "start": 796112, + "length": 65458 + }, + { + "start": 861574, + "length": 65458 + }, + { + "start": 927036, + "length": 65458 + }, + { + "start": 992498, + "length": 65458 + }, + { + "start": 1057960, + "length": 65458 + }, + { + "start": 1123422, + "length": 65458 + }, + { + "start": 1188884, + "length": 65458 + }, + { + "start": 1254346, + "length": 65458 + }, + { + "start": 1319808, + "length": 65458 + }, + { + "start": 1385270, + "length": 65458 + }, + { + "start": 1450732, + "length": 65458 + }, + { + "start": 1516194, + "length": 65458 + }, + { + "start": 1581656, + "length": 65458 + }, + { + "start": 1647118, + "length": 37405 + } + ], + "alg": "sha256", + "hash": "b64'caEich5nUouL2TIbXXRBm71EEizfj/QpXjRB8YvtN6o=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 1019 + }, + { + "start": 1043, + "length": 7840 + }, + { + "start": 9501, + "length": 1063 + }, + { + "start": 10568, + "length": 65458 + }, + { + "start": 76030, + "length": 65458 + }, + { + "start": 141492, + "length": 65458 + }, + { + "start": 206954, + "length": 65458 + }, + { + "start": 272416, + "length": 65458 + }, + { + "start": 337878, + "length": 65458 + }, + { + "start": 403340, + "length": 65458 + }, + { + "start": 468802, + "length": 65458 + }, + { + "start": 534264, + "length": 65458 + }, + { + "start": 599726, + "length": 65458 + }, + { + "start": 665188, + "length": 65458 + }, + { + "start": 730650, + "length": 65458 + }, + { + "start": 796112, + "length": 65458 + }, + { + "start": 861574, + "length": 65458 + }, + { + "start": 927036, + "length": 65458 + }, + { + "start": 992498, + "length": 65458 + }, + { + "start": 1057960, + "length": 65458 + }, + { + "start": 1123422, + "length": 65458 + }, + { + "start": 1188884, + "length": 65458 + }, + { + "start": 1254346, + "length": 65458 + }, + { + "start": 1319808, + "length": 65458 + }, + { + "start": 1385270, + "length": 65458 + }, + { + "start": 1450732, + "length": 65458 + }, + { + "start": 1516194, + "length": 65458 + }, + { + "start": 1581656, + "length": 65458 + }, + { + "start": 1647118, + "length": 37405 + } + ], + "alg": "sha256", + "hash": "b64'NmSnOIEpWA7upBObXvx80VvNYbUPqJiwhGB3Di2BVaI=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "a766bf25-d150-664e-85b4-45cae2cb8737", + "signature": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'UMaw/K9C0CkbpdBnDbkS/GkIdIYYKkoP1iTge8Xieuo=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'kpFvIuZxaDgFDgnaGmwl1sIrgXstpMhfJzimgPPk6ic=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'2dgXjElqRv/Bpvv0Bu54W9pi4Ua+S2zIfVYmEIEtUn0=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'y0kpFXeDy86udKXF+kj3j8Gc7uRjUfLeOn3T1IjMeik=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'9hj61bEFiY955OMGx0bMnHus9uo+OXPYumvu9SlgPqs=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "b0e50c36040555550a8780d3d6c78494bf8652", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:09+00:00", + "notAfter": "2025-11-11T04:44:08+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:51:25+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.multi-asset", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data", + "explanation": "extra data hash exclusions found" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:07:04.965+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture-panorama.signals.json b/src/lib/rubrics/__fixtures__/capture-panorama.signals.json new file mode 100644 index 0000000..d76d713 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-panorama.signals.json @@ -0,0 +1,23 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMediaStitched", + "reportText": "Contains Stitched Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture-portrait.conformance.json b/src/lib/rubrics/__fixtures__/capture-portrait.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-portrait.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture-portrait.json b/src/lib/rubrics/__fixtures__/capture-portrait.json new file mode 100644 index 0000000..92b317d --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-portrait.json @@ -0,0 +1,429 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 1840566 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'sq/yH7T7YPpF4Tw3iUO5EvcxzWxYsGQ+QArzmHRwqms=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 1840566, + "length": 25229 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'FLtYI/jGjTx57ABp59b81x9EJgubOciL5B07/aw2Yrg=", + "pad": "b64'" + }, + "optional": true + }, + { + "location": { + "byteOffset": 1865795, + "length": 535559 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__2", + "alg": "", + "hash": "b64'8nbeFXMcUYIIncKIk9kP7dUsXUXEVRPbCQBccVAQVSc=", + "pad": "b64'" + }, + "optional": true + }, + { + "location": { + "byteOffset": 2401354, + "length": 27743 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__3", + "alg": "", + "hash": "b64'uGTMEEyNwJ8JKN2DAM1KwJNUgHWIS16m0p56PB9XzWo=", + "pad": "b64'" + }, + "optional": true + }, + { + "location": { + "byteOffset": 2429097, + "length": 152973 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__4", + "alg": "", + "hash": "b64'eX+8XKi22onQZz15PteglWztnvETwupWEcz47RfOMSw=", + "pad": "b64'" + }, + "optional": true + }, + { + "location": { + "byteOffset": 2582070, + "length": 38657 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__5", + "alg": "", + "hash": "b64'sL4Ohow86M6Zhcm9wT/KdVPM7uRhdv4M7+meZGiIdXI=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "pad2": "b64'" + }, + "c2pa.hash.data.part__5": { + "alg": "sha256", + "hash": "b64'CU4Y+1VLcDeuPxwQsmTKWmh7xIwnbO6OcPCprI+zyro=", + "pad": "b64'" + }, + "c2pa.hash.data.part__4": { + "alg": "sha256", + "hash": "b64'E3nHldMoJl5kf2N7zF5DRWTWub0HEQeOEpFMdvukn6o=", + "pad": "b64'" + }, + "c2pa.hash.data.part__3": { + "alg": "sha256", + "hash": "b64'nWm46e83OSz+n80HX/6R9rEFJRXvd21c/6OdR3SOp9Y=", + "pad": "b64'" + }, + "c2pa.hash.data.part__2": { + "alg": "sha256", + "hash": "b64'RvFJTlrDVgN5LAaXDb7FQJUFnifPU5ss0RehVy9cAOU=", + "pad": "b64'" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'uRKQAiX2b+J3Hph78B2kJh5xX1RDUg0O51XMo0Y2EAc=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 29198 + }, + { + "start": 29226, + "length": 1967 + }, + { + "start": 31197, + "length": 65458 + }, + { + "start": 96659, + "length": 65458 + }, + { + "start": 162121, + "length": 65458 + }, + { + "start": 227583, + "length": 65458 + }, + { + "start": 293045, + "length": 65458 + }, + { + "start": 358507, + "length": 65458 + }, + { + "start": 423969, + "length": 65458 + }, + { + "start": 489431, + "length": 6414 + }, + { + "start": 495845, + "length": 8758 + } + ], + "alg": "sha256", + "hash": "b64'xqZQt/h/XzUbTQfQqI/SrBya4sMZflHLvpqjC70YGF4=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 29198 + }, + { + "start": 29226, + "length": 1967 + }, + { + "start": 31197, + "length": 65458 + }, + { + "start": 96659, + "length": 65458 + }, + { + "start": 162121, + "length": 65458 + }, + { + "start": 227583, + "length": 65458 + }, + { + "start": 293045, + "length": 65458 + }, + { + "start": 358507, + "length": 65458 + }, + { + "start": 423969, + "length": 65458 + }, + { + "start": 489431, + "length": 6414 + }, + { + "start": 495845, + "length": 8758 + } + ], + "alg": "sha256", + "hash": "b64'Uuh/zYg3hMg6DVYtPnnZIv5T+FFmWpOPEvTRkXTHSSo=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + }, + { + "action": "c2pa.enhanced", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced" + } + ] + } + }, + "claim.v2": { + "instanceID": "94e39f14-9398-a760-12ba-4786acc996d5", + "signature": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'sq/yH7T7YPpF4Tw3iUO5EvcxzWxYsGQ+QArzmHRwqms=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'9VmeETMgnqWVgJ07KE5xb3Kf803CiF61cm8Zhg0arfE=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__3", + "hash": "b64'uGTMEEyNwJ8JKN2DAM1KwJNUgHWIS16m0p56PB9XzWo=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'FLtYI/jGjTx57ABp59b81x9EJgubOciL5B07/aw2Yrg=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'bUC4sr0ChDJqXp7xWCJWeLud8KNjA1lDgDu+iICcHoM=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'/JEsLSGOTGuYwWpYsw1HK60R7/a1BmbzABXsGa/T70Y=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__2", + "hash": "b64'8nbeFXMcUYIIncKIk9kP7dUsXUXEVRPbCQBccVAQVSc=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__4", + "hash": "b64'eX+8XKi22onQZz15PteglWztnvETwupWEcz47RfOMSw=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__5", + "hash": "b64'sL4Ohow86M6Zhcm9wT/KdVPM7uRhdv4M7+meZGiIdXI=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "7273227f8b2476fd6d0dd50359a52dd3798a91", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:05+00:00", + "notAfter": "2025-11-11T04:44:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:49:17+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__3", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__1", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.multi-asset", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__2", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__4", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__4" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__5", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data.part__5" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:6c1893e6-9432-6e9a-21c2-45c1ec9a3743/c2pa.assertions/c2pa.hash.data", + "explanation": "extra data hash exclusions found" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T04:19:43.487+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture-portrait.signals.json b/src/lib/rubrics/__fixtures__/capture-portrait.signals.json new file mode 100644 index 0000000..8290ea4 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-portrait.signals.json @@ -0,0 +1,29 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture-proResZoom.conformance.json b/src/lib/rubrics/__fixtures__/capture-proResZoom.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-proResZoom.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture-proResZoom.json b/src/lib/rubrics/__fixtures__/capture-proResZoom.json new file mode 100644 index 0000000..0d4d5d9 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-proResZoom.json @@ -0,0 +1,297 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 1704831 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'nw2mOhnKcRi/3aFehvn2XBlntS7sjewyNBDedz19J5o=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 1704831, + "length": 10126 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'kHH7LJ6rEN+koG5g5StX0DMpQhf070e24oNWBSznCE0=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "pad2": "b64'AA==" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'SsvkW/vqnouXM5fOxf+u67snRcsSjFjoruRY4vSHrEQ=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 26445 + }, + { + "start": 26469, + "length": 6617 + }, + { + "start": 33704, + "length": 1158 + }, + { + "start": 34866, + "length": 65458 + }, + { + "start": 100328, + "length": 65458 + }, + { + "start": 165790, + "length": 65458 + }, + { + "start": 231252, + "length": 65458 + }, + { + "start": 296714, + "length": 43457 + } + ], + "alg": "sha256", + "hash": "b64'+hrhGiD8mm7XKd+mrEJSXL+xRyFUHLMIR1IeaZwISQI=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 26445 + }, + { + "start": 26469, + "length": 6617 + }, + { + "start": 33704, + "length": 1158 + }, + { + "start": 34866, + "length": 65458 + }, + { + "start": 100328, + "length": 65458 + }, + { + "start": 165790, + "length": 65458 + }, + { + "start": 231252, + "length": 65458 + }, + { + "start": 296714, + "length": 43457 + } + ], + "alg": "sha256", + "hash": "b64'oce/fALFpj8y5udJryt4aUcZk7Gt5z4VgtBhCgX2KYA=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + }, + { + "action": "c2pa.edited", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "c366be8f-ca1e-bfc3-78b1-4437fd277124", + "signature": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'kHH7LJ6rEN+koG5g5StX0DMpQhf070e24oNWBSznCE0=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'nw2mOhnKcRi/3aFehvn2XBlntS7sjewyNBDedz19J5o=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'31P57YO8Fglg7y5JGMOz6KGxUB4cZQ+izmatHpKW/5Y=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'mEFJCECrQOUGTQtnIXFBr5bCdvh+Cltuc5f4fCoreGE=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'ulm9vPlZZ0rsQfz3WhB6KpmOtQAvAQ2iACfQlvbInuI=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "4f877db462f314cb377326d02e5cfb1039be47", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:05+00:00", + "notAfter": "2025-11-11T04:44:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:47:00+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part__1", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.multi-asset", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:b354b55a-3c74-6f40-50c5-4d1ce32f2693/c2pa.assertions/c2pa.hash.data", + "explanation": "extra data hash exclusions found" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T05:36:44.324+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture-proResZoom.signals.json b/src/lib/rubrics/__fixtures__/capture-proResZoom.signals.json new file mode 100644 index 0000000..51fb140 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture-proResZoom.signals.json @@ -0,0 +1,29 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture.conformance.json b/src/lib/rubrics/__fixtures__/capture.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture.json b/src/lib/rubrics/__fixtures__/capture.json new file mode 100644 index 0000000..a974d1e --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture.json @@ -0,0 +1,317 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 2201646 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'f+fgy1+OerCwgBUaf7B+3/YQZO0Rs3OCyV91hEo3hp0=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 2201646, + "length": 104882 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'6vBDDLKYgbDT6fD+g9+/myvdIv/2NFVpP6VUpIe99E0=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'AA==" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'YCKF9pJdtr0cJFTDlZ+m91kmHEJcP/g+MdiTKaczBck=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 29033 + }, + { + "start": 29057, + "length": 6687 + }, + { + "start": 36362, + "length": 1159 + }, + { + "start": 37525, + "length": 65458 + }, + { + "start": 102987, + "length": 65458 + }, + { + "start": 168449, + "length": 65458 + }, + { + "start": 233911, + "length": 65458 + }, + { + "start": 299373, + "length": 65458 + }, + { + "start": 364835, + "length": 65458 + }, + { + "start": 430297, + "length": 65458 + }, + { + "start": 495759, + "length": 29380 + } + ], + "alg": "sha256", + "hash": "b64'MC2WtOu48zBrMprYlxnm2hKHj2bKI9h64crE6HGfM8Y=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 29033 + }, + { + "start": 29057, + "length": 6687 + }, + { + "start": 36362, + "length": 1159 + }, + { + "start": 37525, + "length": 65458 + }, + { + "start": 102987, + "length": 65458 + }, + { + "start": 168449, + "length": 65458 + }, + { + "start": 233911, + "length": 65458 + }, + { + "start": 299373, + "length": 65458 + }, + { + "start": 364835, + "length": 65458 + }, + { + "start": 430297, + "length": 65458 + }, + { + "start": 495759, + "length": 29380 + } + ], + "alg": "sha256", + "hash": "b64'pHJmHV6VE0ZYSue93tkLM+/tRKhmegKkPjTby7eGNA0=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "66808d7a-4558-121c-751a-4ec0fcfafdb6", + "signature": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'6vBDDLKYgbDT6fD+g9+/myvdIv/2NFVpP6VUpIe99E0=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'f+fgy1+OerCwgBUaf7B+3/YQZO0Rs3OCyV91hEo3hp0=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Axs3an5HFczmpjJiejh1Z5lKQi2MVSH8g1NVbjSNOjI=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xOsMWdfp1/ZlFPCZgj8IFJ93ix2XCG8VutFmkihEjCw=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'8osZQ4n42hdQlS/GPQzfZcNgtWus+PhQ8ImQChIP3qk=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "81ce674ee783b652548ba056ef34993be9adee", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:05+00:00", + "notAfter": "2025-11-11T04:44:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:48:57+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data", + "explanation": "extra data hash exclusions found" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-01T15:54:04.585+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d209e393a529f8f54e06842aea3216a558b1caa3", + "shortCommit": "d209e39", + "date": "2026-03-31 12:02:12 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-31T16:07:04.350Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture.signals.json b/src/lib/rubrics/__fixtures__/capture.signals.json new file mode 100644 index 0000000..d51fed1 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture.signals.json @@ -0,0 +1,23 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture2genai.conformance.json b/src/lib/rubrics/__fixtures__/capture2genai.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture2genai.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture2genai.json b/src/lib/rubrics/__fixtures__/capture2genai.json new file mode 100644 index 0000000..c310280 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture2genai.json @@ -0,0 +1,688 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 33, + "length": 23121 + } + ], + "alg": "sha256", + "hash": "b64'R9PP+6cdxwGtKUTmslZ/vx6ILS8x4VdJsATtH5odtSo=", + "pad": "b64'AAAAAAAAAAAAAAAAAA==" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'/PqDQECJt0CU3rLGrvT9jSkaFqDuctFYfgoejBZTeZw=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "description": "Applied imperceptible SynthID watermark.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "dc:format": "image/jpeg", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051", + "hash": "b64'wE9Tl0MD4vycmSEplOdrfn/BX74Odzj9inxUmeVXlYU=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature", + "hash": "b64'xGf+jYOuDbGKLO2RS1ir4BBIpRCQ9mDO8T5D+qh3vNY=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "e1598e1b-3150-a3ee-bf46-4fc480f892a3", + "signature": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "894052442:894052442" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'/PqDQECJt0CU3rLGrvT9jSkaFqDuctFYfgoejBZTeZw=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'rVSfAMu2v85r/Y24M5UdBCv+RY01vXoLj14SIyAH3UY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xf6ozMV+ZsiCZo/jrr9Zch1rJsxHjtEYF0HTftOWo+A=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "9eaf156281a94902165b48ff58a96d24981ece", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 60032", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2026-02-17T15:17:12+00:00", + "notAfter": "2027-02-12T15:17:11+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-04-06T19:11:54+00:00", + "certificateInfo": { + "serialNumber": "a3e6ce9b0e2d6c0443cb71902c6d8f891dd17c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T8" + }, + "validity": { + "notBefore": "2025-09-08T13:48:53+00:00", + "notAfter": "2031-09-09T01:48:52+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T8" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T8" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "explanation": "signing cert not revoked: 3538769668273185232431161852199326737017347790" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:37:47.622+00:00" + } + }, + { + "label": "urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 937, + "length": 14913 + } + ], + "alg": "sha256", + "hash": "b64'peZi6rphpMN/NMY2e+arqniEe07mEfzeAiG5IHhESOo=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "description": "Opened by Google", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'reXvV5FU5mS4CI22lO/WmcO4rWqoOXuuplk7+Vsm5q4=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.transcoded", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'reXvV5FU5mS4CI22lO/WmcO4rWqoOXuuplk7+Vsm5q4=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "dc:format": "image/jpeg", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data" + } + ], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8", + "hash": "b64'KV5vVJVytCxnN0FjRa5AVtYT5Ue+RPHLRVVSJnJ7+QU=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "hash": "b64'65LVJKaY9a78wEoIcnWHX42CDXikTdwUvnf+krUXmeM=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "c641bb90-fb19-a22c-f32c-4d77f1dcb144", + "signature": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "893405627:893405627" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'reXvV5FU5mS4CI22lO/WmcO4rWqoOXuuplk7+Vsm5q4=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'+dr5nmes69piwnARv3qtFLW72prSBk0YZPoIR7wlM+E=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xab5iN2svJIfwvzQRJOc9RO5GaLIkZOMA/y8HTuWqkY=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "c2c6541a01c1e1f6150dd5605aea363d778f9f", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 90291", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-12-01T20:34:01+00:00", + "notAfter": "2026-11-26T20:34:00+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-04-06T19:11:16+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:37:47.624+00:00" + } + }, + { + "label": "urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 2201646 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'f+fgy1+OerCwgBUaf7B+3/YQZO0Rs3OCyV91hEo3hp0=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 2201646, + "length": 104882 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'6vBDDLKYgbDT6fD+g9+/myvdIv/2NFVpP6VUpIe99E0=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'AA==" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'YCKF9pJdtr0cJFTDlZ+m91kmHEJcP/g+MdiTKaczBck=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 29033 + }, + { + "start": 29057, + "length": 6687 + }, + { + "start": 36362, + "length": 1159 + }, + { + "start": 37525, + "length": 65458 + }, + { + "start": 102987, + "length": 65458 + }, + { + "start": 168449, + "length": 65458 + }, + { + "start": 233911, + "length": 65458 + }, + { + "start": 299373, + "length": 65458 + }, + { + "start": 364835, + "length": 65458 + }, + { + "start": 430297, + "length": 65458 + }, + { + "start": 495759, + "length": 29380 + } + ], + "alg": "sha256", + "hash": "b64'MC2WtOu48zBrMprYlxnm2hKHj2bKI9h64crE6HGfM8Y=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 29033 + }, + { + "start": 29057, + "length": 6687 + }, + { + "start": 36362, + "length": 1159 + }, + { + "start": 37525, + "length": 65458 + }, + { + "start": 102987, + "length": 65458 + }, + { + "start": 168449, + "length": 65458 + }, + { + "start": 233911, + "length": 65458 + }, + { + "start": 299373, + "length": 65458 + }, + { + "start": 364835, + "length": 65458 + }, + { + "start": 430297, + "length": 65458 + }, + { + "start": 495759, + "length": 29380 + } + ], + "alg": "sha256", + "hash": "b64'pHJmHV6VE0ZYSue93tkLM+/tRKhmegKkPjTby7eGNA0=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "66808d7a-4558-121c-751a-4ec0fcfafdb6", + "signature": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'6vBDDLKYgbDT6fD+g9+/myvdIv/2NFVpP6VUpIe99E0=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'f+fgy1+OerCwgBUaf7B+3/YQZO0Rs3OCyV91hEo3hp0=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Axs3an5HFczmpjJiejh1Z5lKQi2MVSH8g1NVbjSNOjI=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xOsMWdfp1/ZlFPCZgj8IFJ93ix2XCG8VutFmkihEjCw=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'8osZQ4n42hdQlS/GPQzfZcNgtWus+PhQ8ImQChIP3qk=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "81ce674ee783b652548ba056ef34993be9adee", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:05+00:00", + "notAfter": "2025-11-11T04:44:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:48:57+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:37:47.623+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d209e393a529f8f54e06842aea3216a558b1caa3", + "shortCommit": "d209e39", + "date": "2026-03-31 12:02:12 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-31T16:07:04.350Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture2genai.signals.json b/src/lib/rubrics/__fixtures__/capture2genai.signals.json new file mode 100644 index 0000000..4269420 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture2genai.signals.json @@ -0,0 +1,75 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 60032" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 90291" + }, + "mimeType": "image/jpeg", + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 2, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": "image/jpeg", + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/capture2genai2v.conformance.json b/src/lib/rubrics/__fixtures__/capture2genai2v.conformance.json new file mode 100644 index 0000000..548f09d --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture2genai2v.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/capture2genai2v.json b/src/lib/rubrics/__fixtures__/capture2genai2v.json new file mode 100644 index 0000000..74d9351 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture2genai2v.json @@ -0,0 +1,911 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a", + "assertions": { + "c2pa.hash.bmff.v3": { + "exclusions": [ + { + "xpath": "/ftyp" + }, + { + "xpath": "/uuid", + "data": [ + { + "offset": 8, + "value": [ + 216, + 254, + 195, + 214, + 27, + 14, + 72, + 60, + 146, + 151, + 88, + 40, + 135, + 126, + 196, + 129 + ] + } + ] + }, + { + "xpath": "/mfra" + }, + { + "xpath": "/free" + } + ], + "alg": "sha256", + "hash": "b64'C2Nl904mAB3FaKHin5t4W/GfLJWFUgoLNkWfXCzFPTQ=", + "name": "BMFF file hash", + "pad": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'ikvBZJWWN5l69j1+/GwgnCJ8DAvlHKSWiF0ZTnWKAUY=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "dc:format": "image/png", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature" + }, + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + }, + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef", + "hash": "b64'UXFZg3b/ZhKwExJPz48k7sDa3emEx/wHhv+Y764ZdaI=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "hash": "b64'Qnk6GR3JSOqQbQqWURc+EM364PPpYYBRMkHvZwtme6Y=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "1a0bb626-3c76-5405-4350-46515cf9f272", + "signature": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "893217790:893217790" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'ikvBZJWWN5l69j1+/GwgnCJ8DAvlHKSWiF0ZTnWKAUY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'x27kgF2xgmyWWRmpOMLIpAm1S58DsLxzGoPbXjInWCI=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3", + "hash": "b64'4UmF87wyeLR5jPmolakHR6m+b6TQ5GmTkROqEByB4r4=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-04-06T19:13:38+00:00", + "certificateInfo": { + "serialNumber": "6c26eeedd09cdcec7670d543e6da504e3a9c5e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T12" + }, + "validity": { + "notBefore": "2025-09-08T13:49:00+00:00", + "notAfter": "2031-09-09T01:48:59+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T12" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T12" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3" + }, + { + "code": "assertion.bmffHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "BMFF hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:a3218aaa-91ec-c95d-2fa8-47cca4f5c00a/c2pa.signature", + "explanation": "signing cert not revoked: 577753258235401352320643432677519988913335677" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:41:06.912+00:00" + } + }, + { + "label": "urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 33, + "length": 23121 + } + ], + "alg": "sha256", + "hash": "b64'R9PP+6cdxwGtKUTmslZ/vx6ILS8x4VdJsATtH5odtSo=", + "pad": "b64'AAAAAAAAAAAAAAAAAA==" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'/PqDQECJt0CU3rLGrvT9jSkaFqDuctFYfgoejBZTeZw=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "description": "Applied imperceptible SynthID watermark.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "dc:format": "image/jpeg", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051", + "hash": "b64'wE9Tl0MD4vycmSEplOdrfn/BX74Odzj9inxUmeVXlYU=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature", + "hash": "b64'xGf+jYOuDbGKLO2RS1ir4BBIpRCQ9mDO8T5D+qh3vNY=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "e1598e1b-3150-a3ee-bf46-4fc480f892a3", + "signature": "self#jumbf=/c2pa/urn:c2pa:763fbc99-427d-a47d-35dc-4fbdeff1f4ef/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "894052442:894052442" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'/PqDQECJt0CU3rLGrvT9jSkaFqDuctFYfgoejBZTeZw=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'rVSfAMu2v85r/Y24M5UdBCv+RY01vXoLj14SIyAH3UY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xf6ozMV+ZsiCZo/jrr9Zch1rJsxHjtEYF0HTftOWo+A=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "9eaf156281a94902165b48ff58a96d24981ece", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 60032", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2026-02-17T15:17:12+00:00", + "notAfter": "2027-02-12T15:17:11+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-04-06T19:11:54+00:00", + "certificateInfo": { + "serialNumber": "a3e6ce9b0e2d6c0443cb71902c6d8f891dd17c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T8" + }, + "validity": { + "notBefore": "2025-09-08T13:48:53+00:00", + "notAfter": "2031-09-09T01:48:52+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:41:06.915+00:00" + } + }, + { + "label": "urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 937, + "length": 14913 + } + ], + "alg": "sha256", + "hash": "b64'peZi6rphpMN/NMY2e+arqniEe07mEfzeAiG5IHhESOo=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "description": "Opened by Google", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'reXvV5FU5mS4CI22lO/WmcO4rWqoOXuuplk7+Vsm5q4=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.transcoded", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'reXvV5FU5mS4CI22lO/WmcO4rWqoOXuuplk7+Vsm5q4=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "dc:format": "image/jpeg", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.skipped", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature" + }, + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data" + } + ], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8", + "hash": "b64'KV5vVJVytCxnN0FjRa5AVtYT5Ue+RPHLRVVSJnJ7+QU=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "hash": "b64'65LVJKaY9a78wEoIcnWHX42CDXikTdwUvnf+krUXmeM=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "c641bb90-fb19-a22c-f32c-4d77f1dcb144", + "signature": "self#jumbf=/c2pa/urn:c2pa:1096b09b-3554-06fe-7328-478ac267f051/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "893405627:893405627" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'reXvV5FU5mS4CI22lO/WmcO4rWqoOXuuplk7+Vsm5q4=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'+dr5nmes69piwnARv3qtFLW72prSBk0YZPoIR7wlM+E=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xab5iN2svJIfwvzQRJOc9RO5GaLIkZOMA/y8HTuWqkY=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "c2c6541a01c1e1f6150dd5605aea363d778f9f", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 90291", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-12-01T20:34:01+00:00", + "notAfter": "2026-11-26T20:34:00+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-04-06T19:11:16+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:41:06.914+00:00" + } + }, + { + "label": "urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 2201646 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'f+fgy1+OerCwgBUaf7B+3/YQZO0Rs3OCyV91hEo3hp0=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 2201646, + "length": 104882 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'6vBDDLKYgbDT6fD+g9+/myvdIv/2NFVpP6VUpIe99E0=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'AA==" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'YCKF9pJdtr0cJFTDlZ+m91kmHEJcP/g+MdiTKaczBck=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 29033 + }, + { + "start": 29057, + "length": 6687 + }, + { + "start": 36362, + "length": 1159 + }, + { + "start": 37525, + "length": 65458 + }, + { + "start": 102987, + "length": 65458 + }, + { + "start": 168449, + "length": 65458 + }, + { + "start": 233911, + "length": 65458 + }, + { + "start": 299373, + "length": 65458 + }, + { + "start": 364835, + "length": 65458 + }, + { + "start": 430297, + "length": 65458 + }, + { + "start": 495759, + "length": 29380 + } + ], + "alg": "sha256", + "hash": "b64'MC2WtOu48zBrMprYlxnm2hKHj2bKI9h64crE6HGfM8Y=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 29033 + }, + { + "start": 29057, + "length": 6687 + }, + { + "start": 36362, + "length": 1159 + }, + { + "start": 37525, + "length": 65458 + }, + { + "start": 102987, + "length": 65458 + }, + { + "start": 168449, + "length": 65458 + }, + { + "start": 233911, + "length": 65458 + }, + { + "start": 299373, + "length": 65458 + }, + { + "start": 364835, + "length": 65458 + }, + { + "start": 430297, + "length": 65458 + }, + { + "start": 495759, + "length": 29380 + } + ], + "alg": "sha256", + "hash": "b64'pHJmHV6VE0ZYSue93tkLM+/tRKhmegKkPjTby7eGNA0=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "66808d7a-4558-121c-751a-4ec0fcfafdb6", + "signature": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'6vBDDLKYgbDT6fD+g9+/myvdIv/2NFVpP6VUpIe99E0=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'f+fgy1+OerCwgBUaf7B+3/YQZO0Rs3OCyV91hEo3hp0=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'Axs3an5HFczmpjJiejh1Z5lKQi2MVSH8g1NVbjSNOjI=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'xOsMWdfp1/ZlFPCZgj8IFJ93ix2XCG8VutFmkihEjCw=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:b8c1b157-d9b7-1fba-595f-4b5d5beb2ef8/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'8osZQ4n42hdQlS/GPQzfZcNgtWus+PhQ8ImQChIP3qk=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "81ce674ee783b652548ba056ef34993be9adee", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:05+00:00", + "notAfter": "2025-11-11T04:44:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:48:57+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-06T19:41:06.913+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d209e393a529f8f54e06842aea3216a558b1caa3", + "shortCommit": "d209e39", + "date": "2026-03-31 12:02:12 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-31T16:07:04.350Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/capture2genai2v.signals.json b/src/lib/rubrics/__fixtures__/capture2genai2v.signals.json new file mode 100644 index 0000000..83df0d3 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/capture2genai2v.signals.json @@ -0,0 +1,98 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 60032" + }, + "mimeType": "image/png", + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 2, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 90291" + }, + "mimeType": "image/jpeg", + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 3, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": "image/jpeg", + "localInceptions": [ + { + "trait": "inception:signal_capturedMedia", + "reportText": "Contains Captured Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/gathered-assertions-test.conformance.json b/src/lib/rubrics/__fixtures__/gathered-assertions-test.conformance.json new file mode 100644 index 0000000..0836d4a --- /dev/null +++ b/src/lib/rubrics/__fixtures__/gathered-assertions-test.conformance.json @@ -0,0 +1,72 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [ + { + "trait": "inception_action_position", + "reportText": "Inception action is not in the first actions assertion of created_assertions (or first item is not an actions assertion)" + } + ] + } +} diff --git a/src/lib/rubrics/__fixtures__/gathered-assertions-test.json b/src/lib/rubrics/__fixtures__/gathered-assertions-test.json new file mode 100644 index 0000000..d5ed2ac --- /dev/null +++ b/src/lib/rubrics/__fixtures__/gathered-assertions-test.json @@ -0,0 +1,462 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255", + "assertions": { + "c2pa.hash.multi-asset": { + "parts": [ + { + "location": { + "byteOffset": 0, + "length": 4920752 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part", + "alg": "", + "hash": "b64'y0kpFXeDy86udKXF+kj3j8Gc7uRjUfLeOn3T1IjMeik=", + "pad": "b64'" + }, + "optional": false + }, + { + "location": { + "byteOffset": 4920752, + "length": 83067 + }, + "hashAssertion": { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1", + "alg": "", + "hash": "b64'9hj61bEFiY955OMGx0bMnHus9uo+OXPYumvu9SlgPqs=", + "pad": "b64'" + }, + "optional": true + } + ], + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "pad2": "b64'AA==" + }, + "c2pa.hash.data.part__1": { + "alg": "sha256", + "hash": "b64'q1SZYPsEms/e0tD3k3XGgBgFqPy7O5VVEVDZRCYeHuQ=", + "pad": "b64'" + }, + "c2pa.hash.data.part": { + "exclusions": [ + { + "start": 6, + "length": 1019 + }, + { + "start": 1043, + "length": 7840 + }, + { + "start": 9501, + "length": 1063 + }, + { + "start": 10568, + "length": 65458 + }, + { + "start": 76030, + "length": 65458 + }, + { + "start": 141492, + "length": 65458 + }, + { + "start": 206954, + "length": 65458 + }, + { + "start": 272416, + "length": 65458 + }, + { + "start": 337878, + "length": 65458 + }, + { + "start": 403340, + "length": 65458 + }, + { + "start": 468802, + "length": 65458 + }, + { + "start": 534264, + "length": 65458 + }, + { + "start": 599726, + "length": 65458 + }, + { + "start": 665188, + "length": 65458 + }, + { + "start": 730650, + "length": 65458 + }, + { + "start": 796112, + "length": 65458 + }, + { + "start": 861574, + "length": 65458 + }, + { + "start": 927036, + "length": 65458 + }, + { + "start": 992498, + "length": 65458 + }, + { + "start": 1057960, + "length": 65458 + }, + { + "start": 1123422, + "length": 65458 + }, + { + "start": 1188884, + "length": 65458 + }, + { + "start": 1254346, + "length": 65458 + }, + { + "start": 1319808, + "length": 65458 + }, + { + "start": 1385270, + "length": 65458 + }, + { + "start": 1450732, + "length": 65458 + }, + { + "start": 1516194, + "length": 65458 + }, + { + "start": 1581656, + "length": 65458 + }, + { + "start": 1647118, + "length": 37405 + } + ], + "alg": "sha256", + "hash": "b64'caEich5nUouL2TIbXXRBm71EEizfj/QpXjRB8YvtN6o=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 6, + "length": 1019 + }, + { + "start": 1043, + "length": 7840 + }, + { + "start": 9501, + "length": 1063 + }, + { + "start": 10568, + "length": 65458 + }, + { + "start": 76030, + "length": 65458 + }, + { + "start": 141492, + "length": 65458 + }, + { + "start": 206954, + "length": 65458 + }, + { + "start": 272416, + "length": 65458 + }, + { + "start": 337878, + "length": 65458 + }, + { + "start": 403340, + "length": 65458 + }, + { + "start": 468802, + "length": 65458 + }, + { + "start": 534264, + "length": 65458 + }, + { + "start": 599726, + "length": 65458 + }, + { + "start": 665188, + "length": 65458 + }, + { + "start": 730650, + "length": 65458 + }, + { + "start": 796112, + "length": 65458 + }, + { + "start": 861574, + "length": 65458 + }, + { + "start": 927036, + "length": 65458 + }, + { + "start": 992498, + "length": 65458 + }, + { + "start": 1057960, + "length": 65458 + }, + { + "start": 1123422, + "length": 65458 + }, + { + "start": 1188884, + "length": 65458 + }, + { + "start": 1254346, + "length": 65458 + }, + { + "start": 1319808, + "length": 65458 + }, + { + "start": 1385270, + "length": 65458 + }, + { + "start": 1450732, + "length": 65458 + }, + { + "start": 1516194, + "length": 65458 + }, + { + "start": 1581656, + "length": 65458 + }, + { + "start": 1647118, + "length": 37405 + } + ], + "alg": "sha256", + "hash": "b64'NmSnOIEpWA7upBObXvx80VvNYbUPqJiwhGB3Di2BVaI=", + "pad": "b64'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "pad2": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Pixel Camera.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture" + } + ] + } + }, + "claim.v2": { + "instanceID": "a766bf25-d150-664e-85b4-45cae2cb8737", + "signature": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA SDK for Android", + "version": "781252796:793865596" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.multi-asset", + "hash": "b64'UMaw/K9C0CkbpdBnDbkS/GkIdIYYKkoP1iTge8Xieuo=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'kpFvIuZxaDgFDgnaGmwl1sIrgXstpMhfJzimgPPk6ic=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part", + "hash": "b64'y0kpFXeDy86udKXF+kj3j8Gc7uRjUfLeOn3T1IjMeik=" + }, + { + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1", + "hash": "b64'9hj61bEFiY955OMGx0bMnHus9uo+OXPYumvu9SlgPqs=" + } + ], + "gathered_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'2dgXjElqRv/Bpvv0Bu54W9pi4Ua+S2zIfVYmEIEtUn0=" + } + ] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "b0e50c36040555550a8780d3d6c78494bf8652", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Mobile A 1P ICA G3 L1" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Pixel Camera" + }, + "validity": { + "notBefore": "2025-08-14T04:44:09+00:00", + "notAfter": "2025-11-11T04:44:08+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-08-14T05:51:25+00:00", + "certificateInfo": { + "serialNumber": "801dd6ca0e3a76f622533f629162f7faa703c9", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Pixel Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google", + "CN": "Google Pixel Time Stamping Authority" + }, + "validity": { + "notBefore": "2025-05-12T23:43:02+00:00", + "notAfter": "2032-11-11T08:43:01+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "timestamp message digest matched: Google Pixel Time Stamping Authority" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "timestamp cert trusted: Google Pixel Time Stamping Authority" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.multi-asset", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.multi-asset" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1", + "explanation": "hashed uri matched: self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data.part__1" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "assertion.dataHash.additionalExclusionsPresent", + "url": "self#jumbf=/c2pa/urn:c2pa:bbc6882f-c24f-2263-4516-43bec19c9255/c2pa.assertions/c2pa.hash.data", + "explanation": "extra data hash exclusions found" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:07:04.965+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/gathered-assertions-test.signals.json b/src/lib/rubrics/__fixtures__/gathered-assertions-test.signals.json new file mode 100644 index 0000000..7caaf2f --- /dev/null +++ b/src/lib/rubrics/__fixtures__/gathered-assertions-test.signals.json @@ -0,0 +1,17 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Pixel Camera", + "O": "Google LLC" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/i2v.conformance.json b/src/lib/rubrics/__fixtures__/i2v.conformance.json new file mode 100644 index 0000000..548f09d --- /dev/null +++ b/src/lib/rubrics/__fixtures__/i2v.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/i2v.json b/src/lib/rubrics/__fixtures__/i2v.json new file mode 100644 index 0000000..0d8424b --- /dev/null +++ b/src/lib/rubrics/__fixtures__/i2v.json @@ -0,0 +1,720 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306", + "assertions": { + "c2pa.hash.bmff.v3": { + "exclusions": [ + { + "xpath": "/ftyp" + }, + { + "xpath": "/uuid", + "data": [ + { + "offset": 8, + "value": [ + 216, + 254, + 195, + 214, + 27, + 14, + 72, + 60, + 146, + 151, + 88, + 40, + 135, + 126, + 196, + 129 + ] + } + ] + }, + { + "xpath": "/mfra" + }, + { + "xpath": "/free" + } + ], + "alg": "sha256", + "hash": "b64'YEx/Eg1d410UxfiSSTShgM39jXS5ugtlEDohWb9UnMI=", + "name": "BMFF file hash", + "pad": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'uj+TmpMCaMsNTa+BJitjrdcLyuE/RFLbeLzZj9of4Xs=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.ingredient.v3__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.ingredient.v3__1" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + }, + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.assertions/c2pa.ingredient.v3__1", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a", + "hash": "b64'9VAK36wGeDJ7eu9bVSM86JhlyOicuxvosaKDPZgVC/I=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature", + "hash": "b64'LW2+3wPZtLvhIkyoklXN7z7Rzf0RJYPbKeR561ixHL4=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "969eb724-18c5-35af-10fe-4c454996914c", + "signature": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'uj+TmpMCaMsNTa+BJitjrdcLyuE/RFLbeLzZj9of4Xs=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0Bn/iyN1eY9vVC6c7lxQMk5SqpzSgVNwi9RKOHLL/pU=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3", + "hash": "b64'L7UWgRdwAjuuN17e1xJV45Tixov/I1Jr/qjI6qN8/zM=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:10:41+00:00", + "certificateInfo": { + "serialNumber": "a3e6ce9b0e2d6c0443cb71902c6d8f891dd17c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T8" + }, + "validity": { + "notBefore": "2025-09-08T13:48:53+00:00", + "notAfter": "2031-09-09T01:48:52+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T8" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T8" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3" + }, + { + "code": "assertion.bmffHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "BMFF hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:c4718f88-1bde-39ed-8835-4e22b2eff306/c2pa.signature", + "explanation": "signing cert not revoked: 577753258235401352320643432677519988913335677" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:57:28.395+00:00" + } + }, + { + "label": "urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 21740 + } + ], + "alg": "sha256", + "hash": "b64'nb9XcxAmI3NofSwWURx5u+vqwppddhG2wp/HY01yPqg=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'EtI585pwwqoFA5u2qm5PdcUNUz3WHQoiix5IsbIkAOA=", + "pad": "b64'" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1", + "hash": "b64'mulKvGeLbM8paw19fJ3RxpgNvOKtzp71QvueFDU94oM=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3__1": { + "relationship": "inputTo", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4", + "hash": "b64'ahx3mZGGK3oECYFF5MalzuoDm+iducP2oOOsw69URw0=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature", + "hash": "b64'YN/ARGwOeZA+LE+08Rjq1L3/1d3bAe+iP4R8hInkK3k=", + "pad": "b64'" + }, + "description": "Input ingredient 1" + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb", + "hash": "b64'FxNkI9Qros6EHELrFEhjxNz1q8jLsVEW8cMr1xalFvk=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature", + "hash": "b64'sHT3bdzHNKjHeqHiyooA9vzURrhHtWrfVZsK154PDGE=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "10d68faa-5694-07fc-c07f-45298c46f3cc", + "signature": "self#jumbf=/c2pa/urn:c2pa:e53c99d0-84bf-6456-386b-4672fd90758a/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'EtI585pwwqoFA5u2qm5PdcUNUz3WHQoiix5IsbIkAOA=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1", + "hash": "b64'mulKvGeLbM8paw19fJ3RxpgNvOKtzp71QvueFDU94oM=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'9gYeFgpNtbRaeYp92Sk3By3G9hB9jQX/33kbKxayhZY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'mZxadqlSheTnT0fjRm+7uE/aVeraO/bAm2SHKL+suQ8=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:06:05+00:00", + "certificateInfo": { + "serialNumber": "a3e6ce9b0e2d6c0443cb71902c6d8f891dd17c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T8" + }, + "validity": { + "notBefore": "2025-09-08T13:48:53+00:00", + "notAfter": "2031-09-09T01:48:52+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:57:28.398+00:00" + } + }, + { + "label": "urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 6128 + } + ], + "alg": "sha256", + "hash": "b64'3B6ij90z9RvPZUmgDWsQttUBzEDfj+o2YPd5zwjoe+0=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "2db38d15-bf81-686b-7f6c-4f6b7d0db517", + "signature": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0iRpWnXaCPtJwlIc8RruokSxpMzbiXQ7NBM1qAtHaWY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'+uRKtc3oxLa3+ai48OAAs7Z3qsqvNI8f2yxucSae4ZY=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:04:38+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:57:28.397+00:00" + } + }, + { + "label": "urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 6128 + } + ], + "alg": "sha256", + "hash": "b64'okruBQ3Osl7j9lTFRY5MKTubKfg3GdTII/rWRLnlTT0=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "e705950c-d6c4-be7c-e0a3-4f609e6a1257", + "signature": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0iRpWnXaCPtJwlIc8RruokSxpMzbiXQ7NBM1qAtHaWY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'AQCxRtrvDwr30htVmDk6eZDo6feD9JA4L6zELjk++CE=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:04:28+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:57:28.396+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/i2v.signals.json b/src/lib/rubrics/__fixtures__/i2v.signals.json new file mode 100644 index 0000000..0a84730 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/i2v.signals.json @@ -0,0 +1,92 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 2, + "relationship": "inputTo" + }, + { + "index": 3, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/ii2i.conformance.json b/src/lib/rubrics/__fixtures__/ii2i.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/ii2i.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/ii2i.json b/src/lib/rubrics/__fixtures__/ii2i.json new file mode 100644 index 0000000..e3b2056 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/ii2i.json @@ -0,0 +1,495 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 21740 + } + ], + "alg": "sha256", + "hash": "b64'eIhikxuoKRLfCK7Q/7NxQVjw0HYjFMs9Xrue4XR7two=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'EtI585pwwqoFA5u2qm5PdcUNUz3WHQoiix5IsbIkAOA=", + "pad": "b64'" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1", + "hash": "b64'mulKvGeLbM8paw19fJ3RxpgNvOKtzp71QvueFDU94oM=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3__1": { + "relationship": "inputTo", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4", + "hash": "b64'ahx3mZGGK3oECYFF5MalzuoDm+iducP2oOOsw69URw0=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature", + "hash": "b64'YN/ARGwOeZA+LE+08Rjq1L3/1d3bAe+iP4R8hInkK3k=", + "pad": "b64'" + }, + "description": "Input ingredient 1" + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb", + "hash": "b64'FxNkI9Qros6EHELrFEhjxNz1q8jLsVEW8cMr1xalFvk=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature", + "hash": "b64'sHT3bdzHNKjHeqHiyooA9vzURrhHtWrfVZsK154PDGE=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "46728428-8a10-649b-ab15-420ca0f83948", + "signature": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'EtI585pwwqoFA5u2qm5PdcUNUz3WHQoiix5IsbIkAOA=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1", + "hash": "b64'mulKvGeLbM8paw19fJ3RxpgNvOKtzp71QvueFDU94oM=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'9gYeFgpNtbRaeYp92Sk3By3G9hB9jQX/33kbKxayhZY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'u6jT9aJ9Fux42+0AOU+VjkkOoNgH3FqK95Zb1jbL4us=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:06:05+00:00", + "certificateInfo": { + "serialNumber": "a3e6ce9b0e2d6c0443cb71902c6d8f891dd17c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T8" + }, + "validity": { + "notBefore": "2025-09-08T13:48:53+00:00", + "notAfter": "2031-09-09T01:48:52+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T8" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T8" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.assertions/c2pa.ingredient.v3__1", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:548990cb-129c-8f6b-459b-4031bb7f6fc0/c2pa.signature", + "explanation": "signing cert not revoked: 577753258235401352320643432677519988913335677" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T03:50:45.071+00:00" + } + }, + { + "label": "urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 6128 + } + ], + "alg": "sha256", + "hash": "b64'3B6ij90z9RvPZUmgDWsQttUBzEDfj+o2YPd5zwjoe+0=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "2db38d15-bf81-686b-7f6c-4f6b7d0db517", + "signature": "self#jumbf=/c2pa/urn:c2pa:ddf3b76e-f307-5f71-9654-4bb0f19870f4/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0iRpWnXaCPtJwlIc8RruokSxpMzbiXQ7NBM1qAtHaWY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'+uRKtc3oxLa3+ai48OAAs7Z3qsqvNI8f2yxucSae4ZY=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:04:38+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T03:50:45.073+00:00" + } + }, + { + "label": "urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 6128 + } + ], + "alg": "sha256", + "hash": "b64'okruBQ3Osl7j9lTFRY5MKTubKfg3GdTII/rWRLnlTT0=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "e705950c-d6c4-be7c-e0a3-4f609e6a1257", + "signature": "self#jumbf=/c2pa/urn:c2pa:7c3a900e-47b9-c8b2-f19c-478288cca8fb/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "822815833:822815833" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0iRpWnXaCPtJwlIc8RruokSxpMzbiXQ7NBM1qAtHaWY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'AQCxRtrvDwr30htVmDk6eZDo6feD9JA4L6zELjk++CE=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-10-27T18:04:28+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T03:50:45.072+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/ii2i.signals.json b/src/lib/rubrics/__fixtures__/ii2i.signals.json new file mode 100644 index 0000000..8c1fde4 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/ii2i.signals.json @@ -0,0 +1,69 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "inputTo" + }, + { + "index": 2, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.conformance.json b/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.json b/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.json new file mode 100644 index 0000000..fc91ce8 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.json @@ -0,0 +1,205 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 33, + "length": 6199 + } + ], + "alg": "sha256", + "hash": "b64'0gHjbOEF1/KtHjn9F7p25EX2SISSPHcPjGj7TRLlFUk=", + "pad": "b64'AAAAAAAAAAAAAAAAAA==" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'vErZhOwuGNdB5FT/hTJX5fzvnBYUbEKaWLI4OeE4ExU=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "description": "Added visible watermark", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/composite" + }, + { + "action": "c2pa.converted", + "description": "Converted to .png" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf" + } + }, + "claim.v2": { + "instanceID": "b4b9809b-ddce-d6ae-d9fc-4c2f92d398c8", + "signature": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "891313448:891313448" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'vErZhOwuGNdB5FT/hTJX5fzvnBYUbEKaWLI4OeE4ExU=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'D8LOn5a2qXw5CU2TGZ3xlmqJM9RASLmEDsGCCFiB8O8=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'HTJmCCJ/1nRCQfkysFr2CjIv+93a9Lj/IAqOcwZkS0w=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "8fe1cc59a730ea52f84565289c7bc5aad9a6e1", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 67154", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2026-01-29T19:10:05+00:00", + "notAfter": "2027-01-24T19:10:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-04-01T06:44:25+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T9" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T9" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.signature", + "explanation": "signing cert not revoked: 3208676364496774357143389257185012302059513569" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-04-01T06:45:40.578+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [ + { + "code": "ingredient.unknownProvenance", + "url": "self#jumbf=/c2pa/urn:c2pa:5b7385c3-c9df-ab30-825d-451b1495bc58/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "no title: ingredient does not have provenance" + } + ], + "failure": [] + } + } + ] + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d209e393a529f8f54e06842aea3216a558b1caa3", + "shortCommit": "d209e39", + "date": "2026-03-31 12:02:12 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-31T16:07:04.350Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.signals.json b/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.signals.json new file mode 100644 index 0000000..7c5437e --- /dev/null +++ b/src/lib/rubrics/__fixtures__/r2i-unknownProvenance.signals.json @@ -0,0 +1,40 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 67154" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_mediaUnknownProvenance", + "reportText": "Contains Media of Unknown Provenance", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_editorialNonAI", + "reportText": "Contains Editorial Non-GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_editorialPossiblyGenAI", + "reportText": "Contains Editorial Transformations Possibly Using GenAI", + "multiple": false + }, + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/t2i-gemini.conformance.json b/src/lib/rubrics/__fixtures__/t2i-gemini.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-gemini.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/t2i-gemini.json b/src/lib/rubrics/__fixtures__/t2i-gemini.json new file mode 100644 index 0000000..c283fb7 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-gemini.json @@ -0,0 +1,568 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 33, + "length": 21965 + } + ], + "alg": "sha256", + "hash": "b64'+rGXWeBE35/Edb/yXQQCXFbUPVTaPPTTKVn0BoMjNU0=", + "pad": "b64'AAAAAAAAAAAAAAAAAA==" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'9SvNuTrbTqjrDJ8WRGeW1RxtpZJZk7jOmJYrLUx0law=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "description": "Added imperceptible SynthID watermark", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + }, + { + "action": "c2pa.edited", + "description": "Added visible watermark", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/composite" + }, + { + "action": "c2pa.converted", + "description": "Converted to .png" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47", + "hash": "b64'c26thwok08lNHdxte9fMIwOPdlfiT/xpibf25PrBJh4=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature", + "hash": "b64'JVOuj1sKjHV8/No+9Ib6sIUcwiNcVSeoQbl8TOd7dzM=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "8fcf9f18-6109-c2f5-f06f-45d88351b204", + "signature": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "876242061:876242061" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'9SvNuTrbTqjrDJ8WRGeW1RxtpZJZk7jOmJYrLUx0law=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'T+AIJvGeTmkzzZvfE4LalzhCDHnUGQRnjZwL0KlPtDg=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'hGLwM3W5Erk52/sHe//NZ8fgOIkAtrVk14m1dG9i+Iw=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "8fe1cc59a730ea52f84565289c7bc5aad9a6e1", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 67154", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2026-01-29T19:10:05+00:00", + "notAfter": "2027-01-24T19:10:04+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-03-04T01:35:19+00:00", + "certificateInfo": { + "serialNumber": "f043dfdd776347ea5f7c3ad45a70b5aee2455", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T10" + }, + "validity": { + "notBefore": "2025-09-08T13:48:57+00:00", + "notAfter": "2031-09-09T01:48:56+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T10" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T10" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.signature", + "explanation": "signing cert not revoked: 3208676364496774357143389257185012302059513569" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:51:17.190+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:066b9fa1-6c2f-2979-5767-4265a7091b56/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47", + "explanation": "ingredient hash matched" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature", + "explanation": "signing cert not revoked: 3538769668273185232431161852199326737017347790" + } + ], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 20, + "length": 13763 + } + ], + "alg": "sha256", + "hash": "b64'yRilZMc8UgKQ61TIWArfbGE3iLzaCTPtpwtO4idWzKU=", + "pad": "b64'AAAAAAAAAAAAAAAAAAA=" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'WeQCfBdSGx8QUYVPUetAeTjuFJr7Nb0Iil2P3YeRvsI=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "description": "Applied imperceptible SynthID watermark.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "dc:format": "image/jpeg", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d", + "hash": "b64'uDaidLyepRSYH/kfn97OAShDg2IC5805d1sWfirCpgw=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature", + "hash": "b64'GbT/lBL9C12zO2a/W8G3plqEAbvEBJRDqz6xopvsOUc=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "04f38790-bbf0-76c4-942a-4be47a16800b", + "signature": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "875320944:877502141" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'WeQCfBdSGx8QUYVPUetAeTjuFJr7Nb0Iil2P3YeRvsI=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'4irV6HAYABnKSvdtS+zIgo1p1u1SybldjlNz/HxIZAU=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'MCV86f5RmGhMOmSQKXP47qB0zzqiwCZo03fjZLWE8Ls=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "9eaf156281a94902165b48ff58a96d24981ece", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 60032", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2026-02-17T15:17:12+00:00", + "notAfter": "2027-02-12T15:17:11+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-03-04T01:35:16+00:00", + "certificateInfo": { + "serialNumber": "b2d5d51f21675e13bc4130aa88bc3be4e0ba1e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T9" + }, + "validity": { + "notBefore": "2025-09-08T13:48:55+00:00", + "notAfter": "2031-09-09T01:48:54+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47", + "explanation": "ingredient hash matched" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:c9d683be-dbbc-3000-ff87-4c8041d75d47/c2pa.signature", + "explanation": "signing cert not revoked: 3538769668273185232431161852199326737017347790" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:51:17.190+00:00" + } + }, + { + "label": "urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 20, + "length": 6028 + } + ], + "alg": "sha256", + "hash": "b64'jwPUX7kNiYzwM0ACsefmQ/8D0ylBj8mb4Gch3x0jvso=", + "pad": "b64'AAAAAAAAAAAAAAAAAAA=" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + }, + { + "action": "c2pa.edited", + "description": "Applied imperceptible SynthID watermark.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "89809671-03bb-f3d7-be93-411ef0d1b9bb", + "signature": "self#jumbf=/c2pa/urn:c2pa:03e0b8ea-59e3-7669-09eb-4449e66c5f3d/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "875320944:877502141" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'aCJRK8tzlB0JC+7AU+n6ttA6XAirm7RJDKP1WkTQ6Lk=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'LGba50dR/zcUwB9a8jSOSyVfq/ydrC9U7t8Z3ywwFQM=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "9eaf156281a94902165b48ff58a96d24981ece", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 60032", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2026-02-17T15:17:12+00:00", + "notAfter": "2027-02-12T15:17:11+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2026-03-04T01:34:18+00:00", + "certificateInfo": { + "serialNumber": "6c26eeedd09cdcec7670d543e6da504e3a9c5e", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T12" + }, + "validity": { + "notBefore": "2025-09-08T13:49:00+00:00", + "notAfter": "2031-09-09T01:48:59+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:51:17.191+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/t2i-gemini.signals.json b/src/lib/rubrics/__fixtures__/t2i-gemini.signals.json new file mode 100644 index 0000000..8b89dbe --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-gemini.signals.json @@ -0,0 +1,97 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 67154" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_editorialNonAI", + "reportText": "Contains Editorial Non-GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_editorialPossiblyGenAI", + "reportText": "Contains Editorial Transformations Possibly Using GenAI", + "multiple": false + }, + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 60032" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 2, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 60032" + }, + "mimeType": "image/jpeg", + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/t2i-openai.conformance.json b/src/lib/rubrics/__fixtures__/t2i-openai.conformance.json new file mode 100644 index 0000000..1494366 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-openai.conformance.json @@ -0,0 +1,72 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [ + { + "trait": "trusted_success", + "reportText": "Found trust failures: signingCredential.untrusted" + } + ] + } +} diff --git a/src/lib/rubrics/__fixtures__/t2i-openai.json b/src/lib/rubrics/__fixtures__/t2i-openai.json new file mode 100644 index 0000000..8469b1c --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-openai.json @@ -0,0 +1,330 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a", + "assertions": { + "c2pa.thumbnail.ingredient": { + "format": "image/jpeg", + "identifier": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.thumbnail.ingredient", + "hash": "b64'rjYkR3i4FhakkodD6cqv392O79bjOuGQhfbYPh/AN+w=" + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "dc:title": "image.png", + "dc:format": "png", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [], + "failure": [] + } + }, + "instanceID": "xmp:iid:8c614a03-e296-4ff9-ad28-dc43088ac657", + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c", + "alg": "sha256", + "hash": "b64'TqZVTHsaDs4JonPfiaVbML7udhg4TAbcScqdudc5dk4=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.signature", + "alg": "sha256", + "hash": "b64'7rpHoq0UDMcas1hAPjBtSFdRoKPeszatNICO4k8lipo=", + "pad": "b64'" + }, + "thumbnail": { + "url": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient", + "hash": "b64'rjYkR3i4FhakkodD6cqv392O79bjOuGQhfbYPh/AN+w=", + "pad": "b64'" + } + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'GegYoWtgLHvOMUDooV9zuEnhsrPVKVDnW4XLXf7xUYo=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 33, + "length": 71711 + } + ], + "name": "jumbf manifest", + "alg": "sha256", + "hash": "b64'wy0JUOnYycBpEP1AkiRsBYQj8oJxf6iUDbj7tE8nuFk=", + "pad": "b64'AAAAAAAAAAA=" + } + }, + "claim.v2": { + "instanceID": "xmp:iid:f4e73b28-f7ed-4f73-aa2b-6b5cccd1dbb4", + "signature": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.signature", + "claim_generator_info": { + "name": "ChatGPT", + "org.contentauth.c2pa_rs": "0.0.0" + }, + "alg": "sha256", + "dc:title": "image.png", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient", + "hash": "b64'rjYkR3i4FhakkodD6cqv392O79bjOuGQhfbYPh/AN+w=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'GegYoWtgLHvOMUDooV9zuEnhsrPVKVDnW4XLXf7xUYo=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'ocxu/8itRySQo/13V/3ve4jmsLQThuRYEHZT25PD5F8=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'4JnHtQ0bQQuSqf0yALwpoUOHR49mWGcf6/E9Sfg+LqQ=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "6c29a373fbdcc1d6bb48fc34ba5efa4004e0c446", + "issuer": { + "CN": "WebClaimSigningCA", + "OU": "Lens", + "O": "Truepic", + "C": "US" + }, + "subject": { + "C": "US", + "O": "OpenAI", + "OU": "Sora", + "CN": "Truepic Lens CLI in Sora" + }, + "validity": { + "notBefore": "2025-04-15T15:09:05+00:00", + "notAfter": "2026-04-15T15:09:04+00:00" + } + } + }, + "validationResults": { + "success": [ + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.thumbnail.ingredient", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.hash.data", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.hash.data", + "explanation": "data hash valid" + } + ], + "informational": [], + "failure": [ + { + "code": "signingCredential.untrusted", + "url": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.signature", + "explanation": "signing certificate untrusted" + } + ], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T05:51:57.503+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:e274ecd0-34ec-46b3-aa9d-d90dc10aa56a/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [ + { + "code": "signingCredential.untrusted", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.signature", + "explanation": "signing certificate untrusted" + } + ] + } + } + ] + }, + { + "label": "urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c", + "assertions": { + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "softwareAgent": { + "name": "GPT-4o" + }, + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + }, + { + "action": "c2pa.converted" + } + ] + }, + "c2pa.hash.data": { + "exclusions": [ + { + "start": 33, + "length": 14118 + } + ], + "name": "jumbf manifest", + "alg": "sha256", + "hash": "b64'wy0JUOnYycBpEP1AkiRsBYQj8oJxf6iUDbj7tE8nuFk=", + "pad": "b64'AAAAAAAAAAA=" + } + }, + "claim.v2": { + "instanceID": "xmp:iid:4a3c2a89-6c9c-4f46-bb0d-fd201c8d9957", + "signature": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.signature", + "claim_generator_info": { + "name": "ChatGPT", + "org.contentauth.c2pa_rs": "0.0.0" + }, + "alg": "sha256", + "dc:title": "image.png", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'j06jKi2a0YnaYj1JQTJA/fXaKbmaViQasY2WYTITPLU=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'hsxg8DbXsyiU7mFA+DF56T8dgofZbPNplpHuRg36ya4=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "6c29a373fbdcc1d6bb48fc34ba5efa4004e0c446", + "issuer": { + "CN": "WebClaimSigningCA", + "OU": "Lens", + "O": "Truepic", + "C": "US" + }, + "subject": { + "C": "US", + "O": "OpenAI", + "OU": "Sora", + "CN": "Truepic Lens CLI in Sora" + }, + "validity": { + "notBefore": "2025-04-15T15:09:05+00:00", + "notAfter": "2026-04-15T15:09:04+00:00" + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c", + "explanation": "ingredient hash matched" + } + ], + "informational": [], + "failure": [ + { + "code": "signingCredential.untrusted", + "url": "self#jumbf=/c2pa/urn:c2pa:adf57efd-c6bf-4ee2-9d3e-0f473a8ff16c/c2pa.signature", + "explanation": "signing certificate untrusted" + } + ], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T05:51:57.503+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/t2i-openai.signals.json b/src/lib/rubrics/__fixtures__/t2i-openai.signals.json new file mode 100644 index 0000000..40ed096 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-openai.signals.json @@ -0,0 +1,47 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Truepic Lens CLI in Sora", + "O": "OpenAI", + "OU": "Sora" + }, + "mimeType": "image/jpeg", + "localInceptions": [], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Truepic Lens CLI in Sora", + "O": "OpenAI", + "OU": "Sora" + }, + "mimeType": "png", + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [ + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/t2i-plus.conformance.json b/src/lib/rubrics/__fixtures__/t2i-plus.conformance.json new file mode 100644 index 0000000..c17a9eb --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-plus.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/t2i-plus.json b/src/lib/rubrics/__fixtures__/t2i-plus.json new file mode 100644 index 0000000..b606e87 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-plus.json @@ -0,0 +1,1378 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371", + "assertions": { + "c2pa.hash.boxes": { + "boxes": [ + { + "names": [ + "PNGh", + "IHDR", + "iTXt" + ], + "hash": "b64'UAn6NvKU888EjzhqrP0Gjnbqledw4/pu1jAbm3FGk3M=", + "pad": "b64'", + "pad2": "b64'" + }, + { + "names": [ + "C2PA" + ], + "hash": "b64'AA==", + "pad": "b64'", + "pad2": "b64'" + }, + { + "names": [ + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IDAT", + "IEND" + ], + "hash": "b64'um7jea+l3NiG0mjxptpK8gzGU1R7Fy+Wk2Yxb52HDkg=", + "pad": "b64'", + "pad2": "b64'" + } + ], + "alg": "sha256" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'zUGzL5QIbeApvXcsdkb8zXhKUYICckoRSlxUOurr4Qc=", + "pad": "b64'" + } + ] + } + }, + { + "action": "c2pa.edited", + "description": "Added imperceptible SynthID watermark", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + }, + { + "action": "c2pa.edited", + "description": "Added visible watermark", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/composite" + }, + { + "action": "c2pa.converted", + "description": "Converted to .png" + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "parentOf", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "ingredient.claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [], + "informational": [], + "failure": [] + } + } + ] + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09", + "hash": "b64'NhbmtLPvGKUK49B8JO64rPiuNUbup+kxl9ntq/Y98Ks=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature", + "hash": "b64'QcCN4Um3G4WElKSIORMyKdGlfTQ6C7b5cxF7el+IeVw=", + "pad": "b64'" + } + } + }, + "claim.v2": { + "instanceID": "4cfcb3b4-b210-4d7e-4555-4c6b853ccb69", + "signature": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "839267052:839267052" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'zUGzL5QIbeApvXcsdkb8zXhKUYICckoRSlxUOurr4Qc=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'bnUpvMC335K44qfKeOzD71ljKMP6IE7U63HHF0Ic2vg=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.boxes", + "hash": "b64'1vGD7QlmodKqQ26fqbRcJvp5msLSuq77ujvSP5/Xam4=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "861352a867db32ba70726ad50ed2e2b3b4ead2", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 67154", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-07-30T10:58:39+00:00", + "notAfter": "2026-01-26T10:58:38+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-12-05T15:16:03+00:00", + "certificateInfo": { + "serialNumber": "7b519970ffd75a959d0c40d74e86f1d7702483", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T11" + }, + "validity": { + "notBefore": "2025-09-08T13:48:59+00:00", + "notAfter": "2031-09-09T01:48:58+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T11" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T11" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.assertions/c2pa.hash.boxes", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.boxes" + }, + { + "code": "assertion.boxesHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.assertions/c2pa.hash.boxes", + "explanation": "boxes hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.signature", + "explanation": "signing cert not revoked: 2989983117039450197712507261014569595627170514" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T04:16:15.411+00:00" + }, + "ingredientDeltas": [ + { + "ingredientAssertionURI": "self#jumbf=/c2pa/urn:c2pa:840f88ed-cc8b-c4d9-ded4-4b41633a8371/c2pa.assertions/c2pa.ingredient.v3", + "validationDeltas": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09", + "explanation": "ingredient hash matched" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature", + "explanation": "signing cert not revoked: 2848987360631057089626022484736785301855189030" + } + ], + "failure": [] + } + } + ] + }, + { + "label": "urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 20, + "length": 13689 + } + ], + "alg": "sha256", + "hash": "b64'mA6cnDTpCb6ci4LvKrzhx5fsxWfqLH3aGQE5jLXIpk0=", + "pad": "b64'AAAAAAAAAAAAAAAAAAA=" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'ndFGLQ/R4urVZaXE/9RKp1TbDoOJ8y46lqugAG7EUZY=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "dc:format": "image/jpeg", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4", + "hash": "b64'UB/dPy1WQQka/4eQf0QBKZyKyuEEUGqBTWIodIwNdXE=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature", + "hash": "b64'0scUoK5Igplaqe8YzY3IWsFxVN7Y7oPRSFEPle/eGxY=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "d1eb9e0e-d0a1-cb9f-d729-4e9f3a28da29", + "signature": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "839419279:839419279" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'ndFGLQ/R4urVZaXE/9RKp1TbDoOJ8y46lqugAG7EUZY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'/5tLIovgZI7o1BGqV3OtWt2kVH6VOqO8HynYl1dAxBU=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'NBOukQ9msadngJnTCpPvC0qcDj3ysCPXfGS7iUiJLXQ=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "7fc0c55eb602ce830f9dfdd04ab00c459eb826", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 60032", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-10-30T22:34:47+00:00", + "notAfter": "2026-10-25T22:34:46+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-12-05T15:16:00+00:00", + "certificateInfo": { + "serialNumber": "7b519970ffd75a959d0c40d74e86f1d7702483", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T11" + }, + "validity": { + "notBefore": "2025-09-08T13:48:59+00:00", + "notAfter": "2031-09-09T01:48:58+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "ingredient.manifest.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09", + "explanation": "ingredient hash matched" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:964aabeb-447d-453a-f767-455d60eb8f09/c2pa.signature", + "explanation": "signing cert not revoked: 2848987360631057089626022484736785301855189030" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T04:16:15.411+00:00" + } + }, + { + "label": "urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 20, + "length": 5991 + } + ], + "alg": "sha256", + "hash": "b64'o1R7dQ1vZ9tUvnnxcKK5+6RHin56UBJQiIM3fADDeeM=", + "pad": "b64'AAAAAAAAAAAAAAAAAAA=" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "793da6ce-4f26-2ecd-100a-4956742768a7", + "signature": "self#jumbf=/c2pa/urn:c2pa:dfdfab00-b60a-9324-d754-4b43b6dfd0d4/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "839419279:839419279" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0iRpWnXaCPtJwlIc8RruokSxpMzbiXQ7NBM1qAtHaWY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'CGdetlWF4JnbDL2p2SGAC5kcoaBXTxm7R218V+UbCX8=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "7fc0c55eb602ce830f9dfdd04ab00c459eb826", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 60032", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-10-30T22:34:47+00:00", + "notAfter": "2026-10-25T22:34:46+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-12-05T15:15:34+00:00", + "certificateInfo": { + "serialNumber": "7b519970ffd75a959d0c40d74e86f1d7702483", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T11" + }, + "validity": { + "notBefore": "2025-09-08T13:48:59+00:00", + "notAfter": "2031-09-09T01:48:58+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T04:16:15.412+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/t2i-plus.signals.json b/src/lib/rubrics/__fixtures__/t2i-plus.signals.json new file mode 100644 index 0000000..e6a165a --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i-plus.signals.json @@ -0,0 +1,85 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 67154" + }, + "mimeType": null, + "localInceptions": [], + "localTransformations": [ + { + "trait": "transformation:signal_editorialAI", + "reportText": "Contains Editorial GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_editorialNonAI", + "reportText": "Contains Editorial Non-GenAI Transformations", + "multiple": false + }, + { + "trait": "transformation:signal_editorialPossiblyGenAI", + "reportText": "Contains Editorial Transformations Possibly Using GenAI", + "multiple": false + }, + { + "trait": "transformation:signal_nonEditorial", + "reportText": "Contains Non-editorial Transformations", + "multiple": false + } + ], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "parentOf" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 60032" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 2, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 60032" + }, + "mimeType": "image/jpeg", + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/__fixtures__/t2i2v.conformance.json b/src/lib/rubrics/__fixtures__/t2i2v.conformance.json new file mode 100644 index 0000000..548f09d --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i2v.conformance.json @@ -0,0 +1,71 @@ +{ + "rubricName": "C2PA Asset Conformance 0.1 Spec 2.2 Rubric", + "rubricVersion": "0.1.0", + "true": { + "validation": [ + { + "trait": "active_manifest_urn", + "reportText": "Active manifest URN uses standard 'urn:c2pa:' prefix" + }, + { + "trait": "inception_action_position", + "reportText": "Inception action is correctly positioned as the first item in the first created actions assertion" + }, + { + "trait": "ingredient_relationship_values", + "reportText": "All ingredient assertions have valid relationship values" + }, + { + "trait": "ingredient_v3_no_active_manifest", + "reportText": "Ingredient v3 assertions without activeManifest correctly omit validationResults" + }, + { + "trait": "no_deprecated_actions", + "reportText": "No deprecated actions used" + }, + { + "trait": "no_deprecated_assertions", + "reportText": "No deprecated standard assertions found" + }, + { + "trait": "no_unsupported_assertions", + "reportText": "All standard assertions are supported for Spec 2.2" + }, + { + "trait": "review_ratings_datasource", + "reportText": "No invalid co-occurrence of reviewRatings and human entry dataSource" + }, + { + "trait": "trusted_data_present", + "reportText": "Validation results are present for trust analysis" + }, + { + "trait": "trusted_success", + "reportText": "Asset is trusted" + }, + { + "trait": "valid_data_present", + "reportText": "Validation results are present for integrity analysis" + }, + { + "trait": "valid_success", + "reportText": "No validation mismatches found" + }, + { + "trait": "well_formed_data_present", + "reportText": "Validation results are present for structural analysis" + }, + { + "trait": "well_formed_success", + "reportText": "No structural failures found" + }, + { + "trait": "update_manifest_constraints", + "reportText": "Update manifest constraints are satisfied (or manifest is not an update manifest)" + } + ] + }, + "false": { + "validation": [] + } +} diff --git a/src/lib/rubrics/__fixtures__/t2i2v.json b/src/lib/rubrics/__fixtures__/t2i2v.json new file mode 100644 index 0000000..35eff86 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i2v.json @@ -0,0 +1,362 @@ +{ + "@context": { + "@vocab": "https://contentcredentials.org/crjson", + "extras": "https://contentcredentials.org/crjson/extras" + }, + "manifests": [ + { + "label": "urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589", + "assertions": { + "c2pa.hash.bmff.v3": { + "exclusions": [ + { + "xpath": "/ftyp" + }, + { + "xpath": "/uuid", + "data": [ + { + "offset": 8, + "value": [ + 216, + 254, + 195, + 214, + 27, + 14, + 72, + 60, + 146, + 151, + 88, + 40, + 135, + 126, + 196, + 129 + ] + } + ] + }, + { + "xpath": "/mfra" + }, + { + "xpath": "/free" + } + ], + "alg": "sha256", + "hash": "b64'prANMMdIUop24eAt6kCaVjGkzJ5acJFvBcGoyXEkdKI=", + "name": "BMFF file hash", + "pad": "b64'" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "parameters": { + "ingredients": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'NeLU3hO2vmRhi/O1zpAW26F/53xD7Te5IU3x+Gmsr3E=", + "pad": "b64'" + } + ] + } + } + ] + }, + "c2pa.ingredient.v3": { + "relationship": "inputTo", + "validationResults": { + "activeManifest": { + "success": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature" + }, + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.assertions/c2pa.hash.data" + }, + { + "code": "assertion.dataHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.assertions/c2pa.hash.data" + } + ], + "informational": [], + "failure": [] + } + }, + "activeManifest": { + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4", + "hash": "b64'wRQsDipgBSIvZUOAoaaiCEOGCCTYT0UY+I7cR4/Sj4w=", + "pad": "b64'" + }, + "claimSignature": { + "url": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature", + "hash": "b64'MahWxo5zYfzrJ1gEGe2Bxqwbms9tmVtVOLupytsssVc=", + "pad": "b64'" + }, + "description": "Input ingredient 0" + } + }, + "claim.v2": { + "instanceID": "960aae30-8023-b6eb-f87d-4e3d8d876c35", + "signature": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "808345081:808345081" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3", + "hash": "b64'NeLU3hO2vmRhi/O1zpAW26F/53xD7Te5IU3x+Gmsr3E=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'afITE4TsWIBtT0Nvmq8XZn10iG6iVW2K5b6TJyITqpg=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3", + "hash": "b64'sFZvHZUWOEK9eR9ay1CD+FvDYRfDhUbPtwIHswBxfeQ=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-09-19T01:47:40+00:00", + "certificateInfo": { + "serialNumber": "a3e6ce9b0e2d6c0443cb71902c6d8f891dd17c", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T8" + }, + "validity": { + "notBefore": "2025-09-08T13:48:53+00:00", + "notAfter": "2031-09-09T01:48:52+00:00" + } + } + } + }, + "validationResults": { + "success": [ + { + "code": "timeStamp.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "explanation": "timestamp message digest matched: Google Core Time Stamping Authority T8" + }, + { + "code": "timeStamp.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "explanation": "timestamp cert trusted: Google Core Time Stamping Authority T8" + }, + { + "code": "signingCredential.trusted", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "explanation": "signing certificate trusted, found in System trust anchors" + }, + { + "code": "claimSignature.insideValidity", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "claimSignature.validated", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "explanation": "claim signature valid" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.assertions/c2pa.ingredient.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.ingredient.v3" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.assertions/c2pa.actions.v2", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" + }, + { + "code": "assertion.hashedURI.match", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3" + }, + { + "code": "assertion.bmffHash.match", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.assertions/c2pa.hash.bmff.v3", + "explanation": "BMFF hash valid" + } + ], + "informational": [ + { + "code": "signingCredential.ocsp.notRevoked", + "url": "self#jumbf=/c2pa/urn:c2pa:386a8b21-42da-f4c2-42a6-42fa32886589/c2pa.signature", + "explanation": "signing cert not revoked: 577753258235401352320643432677519988913335677" + } + ], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:44:32.148+00:00" + } + }, + { + "label": "urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4", + "assertions": { + "c2pa.hash.data": { + "exclusions": [ + { + "start": 760, + "length": 6129 + } + ], + "alg": "sha256", + "hash": "b64'lMOR4Wd9FUupVgOw8wO1vxkx/2stATY0qqBH5GE4m2Y=", + "pad": "b64'AAAAAAAAAAAAAAAA" + }, + "c2pa.actions.v2": { + "actions": [ + { + "action": "c2pa.created", + "description": "Created by Google Generative AI.", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" + } + ] + } + }, + "claim.v2": { + "instanceID": "8fba80cd-afd2-d750-2ee6-49cc9a849d7d", + "signature": "self#jumbf=/c2pa/urn:c2pa:6594acf4-1f15-d522-eca9-417698dcbaf4/c2pa.signature", + "claim_generator_info": { + "name": "Google C2PA Core Generator Library", + "version": "808345081:808345081" + }, + "alg": "sha256", + "created_assertions": [ + { + "url": "self#jumbf=c2pa.assertions/c2pa.actions.v2", + "hash": "b64'0iRpWnXaCPtJwlIc8RruokSxpMzbiXQ7NBM1qAtHaWY=" + }, + { + "url": "self#jumbf=c2pa.assertions/c2pa.hash.data", + "hash": "b64'fKyMCh24Y4RuOdL5X2TmDSMsd4QQ4HkXmjgor4bZipw=" + } + ], + "gathered_assertions": [] + }, + "signature": { + "algorithm": "es256", + "certificateInfo": { + "serialNumber": "19e8483a6248fb45b185a31f672b844007ed7d", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Media Services 1P ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "OU": "Google System 98649", + "CN": "Google Media Processing Services" + }, + "validity": { + "notBefore": "2025-08-15T17:37:52+00:00", + "notAfter": "2026-08-10T17:37:51+00:00" + } + }, + "timeStampInfo": { + "timestamp": "2025-09-19T01:45:27+00:00", + "certificateInfo": { + "serialNumber": "7b519970ffd75a959d0c40d74e86f1d7702483", + "issuer": { + "C": "US", + "O": "Google LLC", + "CN": "Google C2PA Core Time-Stamping ICA G3" + }, + "subject": { + "C": "US", + "O": "Google LLC", + "CN": "Google Core Time Stamping Authority T11" + }, + "validity": { + "notBefore": "2025-09-08T13:48:59+00:00", + "notAfter": "2031-09-09T01:48:58+00:00" + } + } + } + }, + "validationResults": { + "success": [], + "informational": [], + "failure": [], + "specVersion": "2.3.0", + "validationTime": "2026-03-31T06:44:32.149+00:00" + } + } + ], + "jsonGenerator": { + "name": "c2pa-rs", + "version": "0.78.0" + }, + "usedITL": false, + "usedTestCerts": false, + "_conformanceToolVersion": { + "commit": "d291fb884e02665d169a38928392c2710f6f0953", + "shortCommit": "d291fb8", + "date": "2026-03-19 20:11:48 -0400", + "branch": "HEAD", + "generatedAt": "2026-03-23T18:42:00.448Z" + } +} \ No newline at end of file diff --git a/src/lib/rubrics/__fixtures__/t2i2v.signals.json b/src/lib/rubrics/__fixtures__/t2i2v.signals.json new file mode 100644 index 0000000..9ed4b76 --- /dev/null +++ b/src/lib/rubrics/__fixtures__/t2i2v.signals.json @@ -0,0 +1,47 @@ +{ + "rubricName": "C2PA Asset Signals Rubric (Local)", + "rubricVersion": "1.0.0", + "manifests": [ + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [ + { + "index": 1, + "relationship": "inputTo" + } + ] + }, + { + "assertedBy": { + "CN": "Google Media Processing Services", + "O": "Google LLC", + "OU": "Google System 98649" + }, + "mimeType": null, + "localInceptions": [ + { + "trait": "inception:signal_fullyGenAIMedia", + "reportText": "Contains Fully GenAI Media", + "multiple": false + } + ], + "localTransformations": [], + "allActionsIncluded": false, + "ingredients": [] + } + ] +} diff --git a/src/lib/rubrics/conformance.test.ts b/src/lib/rubrics/conformance.test.ts new file mode 100644 index 0000000..1671eb0 --- /dev/null +++ b/src/lib/rubrics/conformance.test.ts @@ -0,0 +1,46 @@ +/** + * Sanity checks on the 0.1 / Spec-2.2 conformance rubric as shipped. + * + * The *parity* assertions against `capture.conformance.json` (and every other + * fixture triple) live in `goldens.test.ts`, which runs the same logic across + * all upstream fixtures. This file keeps only the structural invariants of + * the rubric YAML itself — so if somebody edits the rubric in a way that + * drops every statement, or introduces a new category we weren't expecting, + * we'll notice here. + */ + +import { describe, expect, it } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' +import { parseRubricYaml } from './loader' + +const RUBRIC_PATH = path.resolve( + __dirname, + '../../../public/rubrics/asset-rubric-conformance0.1-spec2.2.yml', +) + +describe('conformance rubric · shape invariants (0.1 spec 2.2)', () => { + const rubric = parseRubricYaml( + fs.readFileSync(RUBRIC_PATH, 'utf8'), + 'asset-rubric-conformance0.1-spec2.2.yml', + ) + + it('ships a non-empty list of statements', () => { + expect(rubric.statements.length).toBeGreaterThan(0) + }) + + it('all statements are in the `validation:` category', () => { + const categories = new Set( + rubric.statements.map((s) => (s.id.includes(':') ? s.id.split(':', 1)[0] : 'general')), + ) + expect([...categories]).toEqual(['validation']) + }) + + it('every statement has a json-formula expression and reportText for "true" + "false"', () => { + for (const s of rubric.statements) { + expect(s.expression, `${s.id} missing expression`).toBeTruthy() + expect(s.reportText?.['true'], `${s.id} missing reportText.true`).toBeTruthy() + expect(s.reportText?.['false'], `${s.id} missing reportText.false`).toBeTruthy() + } + }) +}) diff --git a/src/lib/rubrics/context.ts b/src/lib/rubrics/context.ts new file mode 100644 index 0000000..42eaff1 --- /dev/null +++ b/src/lib/rubrics/context.ts @@ -0,0 +1,46 @@ +/** + * Build the json-formula evaluation context from a crJSON report. + * + * The Python reference evaluator expects `validationResults` to live at + * `manifests[i].validationResults` (so expressions like + * `manifests[0].validationResults.failure[?...]` work). c2pa-rs sometimes + * emits it at the document root instead; we normalize by mirroring the + * root-level validation into `manifests[0]` if it isn't already present. + * + * Otherwise the context is pass-through: our crJSON already has + * `manifest.assertions` as a label-keyed object and `manifest['claim.v2']` + * as a dotted key, which is exactly what the rubric expressions assume. + */ + +import type { CrJson, CrJsonManifestEntry } from '../crjson' + +/** + * Evaluation context — a normalized crJSON ready for json-formula. + * + * Typed loosely as `Record` because json-formula doesn't care + * about our nominal types and the rubric expressions reach into arbitrary + * fields on assertions / claim / signature. + */ +export type EvalContext = Record + +export function buildEvalContext(report: CrJson): EvalContext { + // Shallow-clone so we don't mutate the caller's report. + const ctx: Record = { ...report } + + const rootValidation = (report as Record).validationResults + + // If validationResults is at document-root, mirror it into each manifest + // that lacks its own per-manifest validationResults. The Python reference + // rubrics reference `manifests[0].validationResults`, so per-manifest is + // the canonical shape for evaluation. + if (Array.isArray(report.manifests) && rootValidation != null) { + ctx.manifests = report.manifests.map((m: CrJsonManifestEntry) => { + if (m && typeof m === 'object' && m.validationResults == null) { + return { ...m, validationResults: rootValidation } + } + return m + }) + } + + return ctx +} diff --git a/src/lib/rubrics/engine.test.ts b/src/lib/rubrics/engine.test.ts new file mode 100644 index 0000000..44fda57 --- /dev/null +++ b/src/lib/rubrics/engine.test.ts @@ -0,0 +1,104 @@ +/** + * Tests for the json-formula engine wrapper. + * + * Most engine behavior is exercised end-to-end by the goldens (and by + * `evaluate.test.ts` / `perManifest.test.ts`). This file pins down the + * pieces that aren't easy to demonstrate via real rubrics — specifically + * the bare-keyword normalizer and named-expression invocation with $argN. + */ +import { describe, expect, it } from 'vitest' +import { createEngine, normalizeExpression } from './engine' + +describe('normalizeExpression', () => { + it('rewrites bare true / false / null to function calls', () => { + expect(normalizeExpression('contains(arr, true)')).toBe('contains(arr, true())') + expect(normalizeExpression('x == false')).toBe('x == false()') + expect(normalizeExpression('y != null')).toBe('y != null()') + }) + + it('leaves string literals alone', () => { + expect(normalizeExpression('"true is here"')).toBe('"true is here"') + expect(normalizeExpression("'true.field'")).toBe("'true.field'") + expect(normalizeExpression('`true`')).toBe('`true`') + }) + + it("doesn't touch identifiers that contain a keyword", () => { + expect(normalizeExpression('is_true')).toBe('is_true') + expect(normalizeExpression('truely == falsehood')).toBe('truely == falsehood') + expect(normalizeExpression('null_check')).toBe('null_check') + }) + + it('leaves already-called keyword functions alone', () => { + expect(normalizeExpression('contains(arr, true())')).toBe('contains(arr, true())') + expect(normalizeExpression('false() && null()')).toBe('false() && null()') + }) + + it('handles whitespace between keyword and parens', () => { + expect(normalizeExpression('true ()')).toBe('true ()') + expect(normalizeExpression('true\n()')).toBe('true\n()') + }) + + it('handles the real upstream pattern from no_unsupported_assertions', () => { + const expr = 'contains(startsWith(@, $allowed), true)' + expect(normalizeExpression(expr)).toBe('contains(startsWith(@, $allowed), true())') + }) +}) + +describe('createEngine', () => { + it('returns null/true/false coerced correctly even when used as bare keywords', () => { + const engine = createEngine({ name: 'test' }) + expect(engine.search('null', {})).toBe(null) + expect(engine.search('true', {})).toBe(true) + expect(engine.search('false', {})).toBe(false) + }) + + it('exposes variables via $name globals', () => { + const engine = createEngine({ + name: 'test', + variables: { $codes: ['a', 'b', 'c'] }, + }) + expect(engine.search('contains($codes, "b")', {})).toBe(true) + }) + + it('registers named expressions as zero-arg functions', () => { + const engine = createEngine({ + name: 'test', + expressions: { _firstFailure: 'manifests[0].validationResults.failure[0].code' }, + }) + const data = { + manifests: [{ validationResults: { failure: [{ code: 'boom' }] } }], + } + expect(engine.search('_firstFailure()', data)).toBe('boom') + }) + + it('passes positional args to parameterised _expressions via $argN injection', () => { + const engine = createEngine({ + name: 'test', + expressions: { + _hasCode: 'manifests[0].validationResults.failure[?code == $arg0].code', + }, + }) + const data = { + manifests: [{ validationResults: { failure: [{ code: 'boom' }, { code: 'bam' }] } }], + } + expect(engine.search('_hasCode("boom")', data)).toEqual(['boom']) + expect(engine.search('_hasCode("nope")', data)).toEqual([]) + }) + + it('isolates $argN injection across nested calls (save/restore)', () => { + // Two named expressions: outer calls inner; both reference $arg0. The + // engine must restore outer's $arg0 after inner returns. + const engine = createEngine({ + name: 'test', + expressions: { + _inner: '$arg0', + _outer: '[$arg0, _inner("inner-val"), $arg0]', + }, + }) + expect(engine.search('_outer("outer-val")', {})).toEqual([ + 'outer-val', + 'inner-val', + 'outer-val', + ]) + }) +}) diff --git a/src/lib/rubrics/engine.ts b/src/lib/rubrics/engine.ts new file mode 100644 index 0000000..627ab0d --- /dev/null +++ b/src/lib/rubrics/engine.ts @@ -0,0 +1,254 @@ +/** + * json-formula engine wrapper. + * + * Mirrors the Python reference evaluator at + * `../../c2pa/conformance/asset-rubrics/c2pa_conformance_rubric_evaluator.py::create_json_formula_engine` + * so the same rubric YAMLs evaluate identically in the browser. + * + * Rubrics now carry two extra metadata blocks alongside `rubric_metadata`: + * + * - `variables:` → plain `$name: value` globals, passed as the `globals` + * argument on every `search()` call. + * + * - `expressions:` → `_name: ""` named expressions, registered as + * custom functions. They can reference `$argN` positional parameters, + * which we inject into the interpreter's `globals` at call time and + * restore afterwards so nested calls don't leak state. + * + * Keep this file free of evaluator-specific logic (pass/fail, coercion, + * reportText). Those stay in `evaluate.ts` / `perManifest.ts`. + */ +import JsonFormula, { + dataTypes, + type CustomFunctionEntry, + type Interpreter, + type JsonFormulaAst, +} from '@adobe/json-formula' +import type { RubricMetadata } from './types' + +/** Thin façade exposing just the methods the evaluators call. */ +export interface RubricEngine { + /** Evaluate an expression string against `data`. */ + search(expression: string, data: unknown): unknown + /** The resolved `$name` globals — pulled from rubric metadata. */ + readonly variables: Record +} + +/** Build an engine configured for one rubric's variables + named expressions. */ +export function createEngine(metadata: RubricMetadata): RubricEngine { + const variables: Record = { ...(metadata.variables ?? {}) } + const expressions: Record = { ...(metadata.expressions ?? {}) } + + // Determine the widest `$argN` fingerprint across all named expressions so + // the parser will accept those identifiers when compiling any of them. + const maxArity = Object.values(expressions).reduce( + (acc, expr) => Math.max(acc, argCount(expr)), + 0, + ) + const argNames = Array.from({ length: maxArity }, (_, i) => `$arg${i}`) + + // Pre-compile each named expression once. Parsing happens now; execution + // happens every time the expression is invoked (possibly many times per + // statement via nested `_name()` calls). + const compileHelper = new JsonFormula({}, null, []) + const allowedGlobals = [ + ...Object.keys(variables), + ...Object.keys(expressions), + ...argNames, + ] + + const compiled = new Map() + for (const [name, exprStr] of Object.entries(expressions)) { + const arity = argCount(exprStr) + try { + compiled.set(name, { + ast: compileHelper.compile(normalizeExpression(exprStr), allowedGlobals), + arity, + }) + } catch (err) { + compiled.set(name, { + ast: null, + arity, + error: err instanceof Error ? err.message : String(err), + }) + } + } + + const customFunctions: Record = {} + for (const [name, entry] of compiled) { + customFunctions[name] = { + _signature: makeSignature(entry.arity), + _func: makeExpressionFn(name, entry), + } + } + + const engine = new JsonFormula(customFunctions, null, []) + + return { + variables, + search(expression: string, data: unknown): unknown { + return engine.search(normalizeExpression(expression), data, variables) + }, + } +} + +/** + * Rewrite bare `true` / `false` / `null` keywords to their zero-arg function + * form (`true()`, etc.). + * + * Why: `@adobe/json-formula` 2.0 registers `true`/`false`/`null` as zero-arg + * functions but does NOT auto-invoke them when written without parens — bare + * `true` is parsed as a field access (current value's `true` property), + * which yields `null` and breaks `contains([...], true)`. The reference + * Python `json-formula` package tolerates the bare form, so upstream rubrics + * are written that way (e.g. the `no_unsupported_assertions` rule: + * `contains(startsWith(@, $allowed), true)`). Normalizing here keeps the + * pre-built YAMLs unmodified relative to upstream while still evaluating + * correctly in the browser. + * + * The walk is string-aware: it skips over `"..."` (string literals), + * `'...'` (quoted identifiers), and `` `...` `` (JSON literals) so a + * keyword inside a string is never rewritten. It also leaves identifiers + * like `is_true` or `truely` alone (word-boundary check), and skips any + * keyword already followed by `(`. + */ +export function normalizeExpression(expr: string): string { + if (typeof expr !== 'string' || expr.length === 0) return expr + const KEYWORDS = new Set(['true', 'false', 'null']) + // Characters that count as "word" for the purpose of identifier detection. + const isWordChar = (ch: string | undefined) => + ch != null && /[A-Za-z0-9_$]/.test(ch) + + let out = '' + let i = 0 + while (i < expr.length) { + const ch = expr[i] + + // Skip string-like spans untouched: " ' and `. + if (ch === '"' || ch === "'" || ch === '`') { + const quote = ch + out += ch + i += 1 + while (i < expr.length && expr[i] !== quote) { + if (expr[i] === '\\' && i + 1 < expr.length) { + out += expr[i] + expr[i + 1] + i += 2 + } else { + out += expr[i] + i += 1 + } + } + if (i < expr.length) { + out += expr[i] // closing quote + i += 1 + } + continue + } + + // Try to match one of the bare keywords at this position. They must: + // - not be preceded by a word char (so we don't touch `is_true`), + // - not be followed by a word char (so we don't touch `truely`), + // - not be already followed by `(` (so we don't touch `true()`). + const prev = i > 0 ? expr[i - 1] : undefined + if (!isWordChar(prev)) { + let matched: string | undefined + for (const kw of KEYWORDS) { + if ( + expr.startsWith(kw, i) && + !isWordChar(expr[i + kw.length]) + ) { + matched = kw + break + } + } + if (matched) { + // Look past whitespace for an opening `(` — if present, it's already + // a function call and we leave it alone. + let j = i + matched.length + while (j < expr.length && /\s/.test(expr[j])) j += 1 + if (expr[j] !== '(') { + out += `${matched}()` + i += matched.length + continue + } + } + } + + out += ch + i += 1 + } + return out +} + +/** Count the highest `$argN` index referenced in an expression, +1. Zero if none. */ +function argCount(expr: string | undefined): number { + if (!expr) return 0 + const re = /\$arg(\d+)/g + let max = -1 + for (const m of expr.matchAll(re)) { + const n = Number(m[1]) + if (Number.isFinite(n) && n > max) max = n + } + return max + 1 +} + +/** Build an `_signature` list accepting exactly `arity` positional args (any type). */ +function makeSignature(arity: number) { + if (arity === 0) return [] + return Array.from({ length: arity }, () => ({ types: [dataTypes.TYPE_ANY] })) +} + +/** + * Build the `_func` for a named expression. Zero-arity forms just re-evaluate + * the compiled AST against the caller's data. Parameterised forms inject the + * caller-provided values as `$arg0`, `$arg1`, ... into the interpreter's + * `globals`, evaluate, then restore the prior values — matching the Python + * reference's save/restore dance exactly. + */ +function makeExpressionFn( + name: string, + entry: { ast: JsonFormulaAst | null; arity: number; error?: string }, +): CustomFunctionEntry['_func'] { + if (entry.arity === 0) { + return (_args, data, interpreter) => { + if (entry.ast == null) { + throw new Error(`Expression '${name}' failed to compile: ${entry.error ?? 'unknown error'}`) + } + return interpreter.search(entry.ast, data) + } + } + return (args, data, interpreter) => { + if (entry.ast == null) { + throw new Error(`Expression '${name}' failed to compile: ${entry.error ?? 'unknown error'}`) + } + return withInjectedGlobals( + interpreter, + Object.fromEntries(args.map((v, i) => [`$arg${i}`, v])), + () => interpreter.search(entry.ast as JsonFormulaAst, data), + ) + } +} + +/** Run `fn` with extra entries merged into `interpreter.globals`, restoring on exit. */ +function withInjectedGlobals( + interpreter: Interpreter, + extra: Record, + fn: () => T, +): T { + const prior = new Map() + for (const k of Object.keys(extra)) { + prior.set(k, { + had: Object.prototype.hasOwnProperty.call(interpreter.globals, k), + value: interpreter.globals[k], + }) + interpreter.globals[k] = extra[k] + } + try { + return fn() + } finally { + for (const [k, snap] of prior) { + if (snap.had) interpreter.globals[k] = snap.value + else delete interpreter.globals[k] + } + } +} diff --git a/src/lib/rubrics/evaluate.test.ts b/src/lib/rubrics/evaluate.test.ts new file mode 100644 index 0000000..a57ccda --- /dev/null +++ b/src/lib/rubrics/evaluate.test.ts @@ -0,0 +1,147 @@ +/** + * Golden test: verify the TS evaluator produces the same pass/fail outcomes + * as the Python reference evaluator on a known-good input. + * + * Fixtures are copied from `/Users/andyp/Desktop/Projects/c2pa/conformance/asset-rubrics/test/`: + * - capture.json — input crJSON for a clean captured-media asset + * - capture.conformance.json — expected output from the full conformance rubric + * + * The integrity rubric's six statements all appear under "true" in + * capture.conformance.json, so we assert that our evaluator reports all six + * as passed. + */ + +import { describe, expect, it } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' +import type { CrJson } from '../crjson' +import { parseRubricYaml } from './loader' +import { evaluateRubric } from './evaluate' + +const FIXTURE_DIR = path.resolve(__dirname, '__fixtures__') +const RUBRIC_PATH = path.resolve(__dirname, '../../../public/rubrics/asset-rubric-integrity.yml') + +function loadFixture(filename: string): T { + const raw = fs.readFileSync(path.join(FIXTURE_DIR, filename), 'utf8') + return JSON.parse(raw) as T +} + +describe('integrity rubric · golden parity with Python reference', () => { + const input = loadFixture('capture.json') + const rubric = parseRubricYaml(fs.readFileSync(RUBRIC_PATH, 'utf8'), 'asset-rubric-integrity.yml') + + it('loads all 6 integrity statements', () => { + expect(rubric.statements.map((s) => s.id)).toEqual([ + 'validation:well_formed_data_present', + 'validation:well_formed_success', + 'validation:valid_data_present', + 'validation:valid_success', + 'validation:trusted_data_present', + 'validation:trusted_success', + ]) + }) + + it('all statements pass on capture.json (clean captured-media asset)', () => { + const result = evaluateRubric(rubric, input, { rubricId: 'asset-integrity' }) + + const failed = result.statements.filter((s) => s.passed !== true) + // If this fails, print which statements regressed and the raw value so + // we can see exactly where the TS port diverges from the Python reference. + if (failed.length > 0) { + console.error( + 'Regressions:\n' + + failed + .map( + (s) => + ` - ${s.id}: passed=${s.passed} raw=${JSON.stringify(s.rawValue)} error=${s.error ?? ''}`, + ) + .join('\n'), + ) + } + expect(failed).toEqual([]) + expect(result.overallPassed).toBe(true) + }) + + it('selects the "true" reportText for passing statements', () => { + const result = evaluateRubric(rubric, input, { rubricId: 'asset-integrity' }) + const wellFormed = result.statements.find((s) => s.id === 'validation:well_formed_success') + expect(wellFormed?.message).toBe('No structural failures found') + }) + + it('sets category from the id prefix', () => { + const result = evaluateRubric(rubric, input, { rubricId: 'asset-integrity' }) + expect(result.statements.every((s) => s.category === 'validation')).toBe(true) + }) +}) + +describe('coercion rules', () => { + it('failIfMatched inverts a non-empty list to a failure with matches', () => { + const rubric = parseRubricYaml( + [ + 'rubric_metadata:', + ' name: test', + ' version: 1.0.0', + '---', + '- id: test:absence', + ' failIfMatched: true', + ' expression: |-', + ' failures[?contains(["boom"], code)].code', + ' reportText:', + " 'true':", + ' en: No boom', + " 'false':", + " en: 'Boom found: {{matches}}'", + ].join('\n'), + 'inline', + ) + const ctx = { failures: [{ code: 'boom' }, { code: 'other' }] } as unknown as CrJson + const result = evaluateRubric(rubric, ctx, { rubricId: 'test' }) + expect(result.statements[0].passed).toBe(false) + expect(result.statements[0].message).toBe('Boom found: boom') + }) + + it('failIfMatched on an empty list passes', () => { + const rubric = parseRubricYaml( + [ + 'rubric_metadata:', + ' name: test', + '---', + '- id: test:absence', + ' failIfMatched: true', + ' expression: |-', + ' failures[?code == "boom"].code', + ' reportText:', + " 'true':", + ' en: No boom', + " 'false':", + " en: 'Boom: {{matches}}'", + ].join('\n'), + 'inline', + ) + const ctx = { failures: [] } as unknown as CrJson + const result = evaluateRubric(rubric, ctx, { rubricId: 'test' }) + expect(result.statements[0].passed).toBe(true) + expect(result.statements[0].message).toBe('No boom') + }) + + it('records an error when the expression is invalid', () => { + const rubric = parseRubricYaml( + [ + 'rubric_metadata:', + ' name: test', + '---', + '- id: test:bad', + ' expression: |-', + ' ))) not valid jmespath', + ' reportText:', + " 'true': { en: ok }", + " 'false': { en: bad }", + ].join('\n'), + 'inline', + ) + const ctx = {} as CrJson + const result = evaluateRubric(rubric, ctx, { rubricId: 'test' }) + expect(result.statements[0].passed).toBe(null) + expect(result.statements[0].error).toBeTruthy() + }) +}) diff --git a/src/lib/rubrics/evaluate.ts b/src/lib/rubrics/evaluate.ts new file mode 100644 index 0000000..07b0923 --- /dev/null +++ b/src/lib/rubrics/evaluate.ts @@ -0,0 +1,164 @@ +/** + * Rubric evaluator — runs one or more statements against a crJSON context. + * + * Mirrors the coercion and reportText rules from the Python reference at + * `c2pa_conformance_rubric_evaluator.py`. Key rules: + * + * - json-formula result coercion (normal case): + * list → passed = list.length > 0 + * bool → passed = val + * number → passed = val > 0 + * null → passed = false + * other → passed = true + * + * - failIfMatched: + * list & non-empty → passed = false, matches = list + * otherwise → passed = true + * + * - reportText[passed ? 'true' : 'false'][locale] is selected; falls back + * to reportText['default'][locale]. `{{matches}}` is replaced with the + * comma-joined matches if they are strings. + * + * - Any thrown error produces passed = null and an `error` field. + */ + +import type { CrJson } from '../crjson' +import { buildEvalContext } from './context' +import { createEngine, type RubricEngine } from './engine' +import type { Rubric, RubricResult, RubricStatement, StatementResult } from './types' + +const DEFAULT_LOCALE = 'en' + +export function evaluateRubric( + rubric: Rubric, + report: CrJson, + options: { rubricId: string; locale?: string } = { rubricId: 'unknown' }, +): RubricResult { + const context = buildEvalContext(report) + const locale = options.locale ?? rubric.metadata.language ?? DEFAULT_LOCALE + + // One engine per evaluation — it closes over the rubric's variables and + // named expressions. Cheap enough that per-call is fine; statements reuse it. + const engine = createEngine(rubric.metadata) + + const statements = rubric.statements.map((s) => evaluateStatement(s, context, locale, engine)) + + const overallPassed = statements.every((s) => s.passed === true) + + return { + rubricId: options.rubricId, + rubricName: rubric.metadata.name, + rubricVersion: rubric.metadata.version, + overallPassed, + statements, + evaluatedAt: new Date(), + } +} + +export function evaluateStatement( + stmt: RubricStatement, + context: unknown, + locale: string = DEFAULT_LOCALE, + engine?: RubricEngine, +): StatementResult { + const category = stmt.id.includes(':') ? stmt.id.split(':', 1)[0] : 'general' + // Allow ad-hoc single-statement evaluation without a rubric: fall back to a + // barebones engine that has no variables or named expressions. Unit tests + // and older callers rely on this. + const evalEngine = engine ?? createEngine({ name: 'ad-hoc' }) + + let rawValue: unknown = undefined + let error: string | undefined + + try { + rawValue = evalEngine.search(stmt.expression.trim(), context) + } catch (e) { + error = e instanceof Error ? e.message : String(e) + return { + id: stmt.id, + category, + description: stmt.description, + passed: null, + rawValue: null, + error, + message: `Error: ${error}`, + } + } + + const { passed, matches } = coerce(rawValue, stmt.failIfMatched === true) + const message = pickReportText(stmt, passed, locale, matches) + + return { + id: stmt.id, + category, + description: stmt.description, + passed, + rawValue, + message, + } +} + +/** + * Coerce a raw JMESPath result to a boolean outcome, following the Python + * reference rules. Also returns `matches` when the raw value is a list that + * carries match information (used for `{{matches}}` substitution). + */ +function coerce(val: unknown, failIfMatched: boolean): { passed: boolean; matches?: unknown[] } { + if (failIfMatched) { + if (Array.isArray(val) && val.length > 0) { + return { passed: false, matches: val } + } + return { passed: true } + } + + if (Array.isArray(val)) { + // Non-failIfMatched list: truthy iff non-empty. If entries look like + // match records (have `label` or `signature`), expose them as matches. + if (val.length > 0) { + const first = val[0] + if (first && typeof first === 'object' && !Array.isArray(first) && ('label' in first || 'signature' in first)) { + return { passed: true, matches: val } + } + return { passed: true } + } + return { passed: false } + } + + if (typeof val === 'boolean') return { passed: val } + if (typeof val === 'number') return { passed: val > 0 } + if (val == null) return { passed: false } + return { passed: true } +} + +function pickReportText( + stmt: RubricStatement, + passed: boolean, + locale: string, + matches: unknown[] | undefined, +): string { + const dict = stmt.reportText + if (!dict) return '' + + const key = passed ? 'true' : 'false' + const chosen = dict[key] ?? dict.default + if (!chosen) return '' + + // `chosen` may be either a locale-keyed object or a bare string in old rubrics. + let text: string + if (typeof chosen === 'string') { + text = chosen + } else { + text = chosen[locale] ?? chosen[DEFAULT_LOCALE] ?? '' + } + + // Substitute `{{matches}}` if present and matches are strings. + if (text.includes('{{matches}}')) { + if (Array.isArray(matches) && matches.every((m) => typeof m === 'string')) { + text = text.replace('{{matches}}', (matches as string[]).join(', ')) + } else { + text = text.replace('{{matches}}', '') + } + } + + return text +} diff --git a/src/lib/rubrics/goldens.test.ts b/src/lib/rubrics/goldens.test.ts new file mode 100644 index 0000000..ec9dc0d --- /dev/null +++ b/src/lib/rubrics/goldens.test.ts @@ -0,0 +1,201 @@ +/** + * Parameterized golden-parity test: for every fixture triple copied from + * upstream (`.json` + `.conformance.json` + `.signals.json`), + * verify that our TS evaluators produce results that agree with the Python + * reference on every trait the fixture covers. + * + * Fixtures are stored under `__fixtures__/` and were generated by + * `c2pa_conformance_rubric_evaluator.py` (0.1 / Spec-2.2 rubric) and + * `c2pa_signals_rubric_evaluator.py` (signals-local rubric) respectively. + * + * Assertion shape notes: + * - Conformance: subset-match. Every trait in the fixture's + * `true` / `false` buckets must resolve to the same outcome and + * reportText on our side. Rubric statements added after the fixture + * was generated are ignored (forward-compatible) — that's fine + * because the upstream rubric has grown past its fixtures (see e.g. + * `thumbnail_location` which post-dates every fixture). + * - Signals: exact-match, field by field, in fixture order. + */ + +import { describe, expect, it } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' +import type { CrJson } from '../crjson' +import { parseRubricYaml } from './loader' +import { evaluateRubric } from './evaluate' +import { evaluatePerManifest } from './perManifest' +import type { RubricResult } from './types' + +const FIXTURE_DIR = path.resolve(__dirname, '__fixtures__') +const CONFORMANCE_RUBRIC_PATH = path.resolve( + __dirname, + '../../../public/rubrics/asset-rubric-conformance0.1-spec2.2.yml', +) +const SIGNALS_RUBRIC_PATH = path.resolve( + __dirname, + '../../../public/rubrics/asset-rubric-signals-local.yml', +) + +// Parse the rubrics once so we're not paying YAML parse cost per fixture. +const conformanceRubric = parseRubricYaml( + fs.readFileSync(CONFORMANCE_RUBRIC_PATH, 'utf8'), + 'asset-rubric-conformance0.1-spec2.2.yml', +) +const signalsRubric = parseRubricYaml( + fs.readFileSync(SIGNALS_RUBRIC_PATH, 'utf8'), + 'asset-rubric-signals-local.yml', +) + +// Discover fixtures by looking for `.json` files that also have +// companion `.conformance.json` and `.signals.json` siblings. This keeps +// the test list self-maintaining when new fixtures land in the directory. +function discoverFixtureNames(): string[] { + const all = fs.readdirSync(FIXTURE_DIR) + const baseInputs = all.filter( + (f) => f.endsWith('.json') && !f.endsWith('.conformance.json') && !f.endsWith('.signals.json'), + ) + return baseInputs + .map((f) => f.replace(/\.json$/, '')) + .filter( + (name) => + all.includes(`${name}.conformance.json`) && all.includes(`${name}.signals.json`), + ) + .sort() +} + +const FIXTURE_NAMES = discoverFixtureNames() + +// ── Conformance fixture parity ──────────────────────────────────────── + +interface ConformanceFixture { + rubricName: string + rubricVersion: string + true: Record> + false: Record> +} + +/** trait → { outcome, reportText } lookup, flattened across all categories. */ +function traitIndex( + result: RubricResult, +): Map { + const out = new Map() + for (const s of result.statements) { + const trait = s.id.includes(':') ? s.id.split(':').slice(1).join(':') : s.id + const outcome: 'true' | 'false' | 'error' = + s.passed === true ? 'true' : s.passed === false ? 'false' : 'error' + out.set(`${s.category}:${trait}`, { outcome, reportText: s.message }) + } + return out +} + +describe('conformance rubric · parameterized golden parity (0.1 spec 2.2)', () => { + it(`discovered at least the original capture fixture`, () => { + // Cheap sanity check so a broken discover doesn't silently skip all tests. + expect(FIXTURE_NAMES).toContain('capture') + expect(FIXTURE_NAMES.length).toBeGreaterThanOrEqual(15) + }) + + for (const name of FIXTURE_NAMES) { + it(`${name} · every fixture trait matches our evaluator`, () => { + const input = JSON.parse( + fs.readFileSync(path.join(FIXTURE_DIR, `${name}.json`), 'utf8'), + ) as CrJson + const expected = JSON.parse( + fs.readFileSync(path.join(FIXTURE_DIR, `${name}.conformance.json`), 'utf8'), + ) as ConformanceFixture + + const result = evaluateRubric(conformanceRubric, input, { + rubricId: 'asset-conformance-0.1-spec2.2', + }) + expect(result.rubricName).toBe(expected.rubricName) + expect(result.rubricVersion).toBe(expected.rubricVersion) + + const index = traitIndex(result) + + const mismatches: string[] = [] + const check = ( + wantOutcome: 'true' | 'false', + buckets: ConformanceFixture['true'], + ) => { + for (const [category, traits] of Object.entries(buckets)) { + for (const { trait, reportText } of traits) { + const got = index.get(`${category}:${trait}`) + if (!got) { + mismatches.push(` missing: ${category}:${trait}`) + continue + } + if (got.outcome !== wantOutcome) { + mismatches.push( + ` ${category}:${trait} → outcome expected=${wantOutcome} got=${got.outcome}`, + ) + } + if (got.reportText !== reportText) { + mismatches.push( + ` ${category}:${trait} → reportText diverged\n want: ${reportText}\n got: ${got.reportText}`, + ) + } + } + } + } + check('true', expected.true) + check('false', expected.false) + + expect(mismatches, `parity diffs for ${name}:\n${mismatches.join('\n')}`).toEqual([]) + }) + } +}) + +// ── Signals fixture parity ──────────────────────────────────────────── + +interface SignalsFixture { + rubricName: string + rubricVersion: string + manifests: Array<{ + assertedBy: { CN: string; O: string; OU?: string } + mimeType: string | null + localInceptions: Array<{ trait: string; reportText: string; multiple: boolean }> + localTransformations: Array<{ trait: string; reportText: string; multiple: boolean }> + allActionsIncluded: boolean + ingredients: Array<{ index: number; relationship?: string }> + }> +} + +describe('signals rubric · parameterized golden parity (local)', () => { + for (const name of FIXTURE_NAMES) { + it(`${name} · per-manifest signals match the reference exactly`, () => { + const input = JSON.parse( + fs.readFileSync(path.join(FIXTURE_DIR, `${name}.json`), 'utf8'), + ) as CrJson + const expected = JSON.parse( + fs.readFileSync(path.join(FIXTURE_DIR, `${name}.signals.json`), 'utf8'), + ) as SignalsFixture + + const result = evaluatePerManifest(signalsRubric, input, { + rubricId: 'asset-signals-local', + }) + + expect(result.rubricName).toBe(expected.rubricName) + expect(result.rubricVersion).toBe(expected.rubricVersion) + expect(result.manifests).toHaveLength(expected.manifests.length) + + for (let i = 0; i < expected.manifests.length; i++) { + const want = expected.manifests[i] + const got = result.manifests[i] + expect(got.assertedBy, `${name} manifest[${i}] assertedBy`).toEqual(want.assertedBy) + expect(got.mimeType, `${name} manifest[${i}] mimeType`).toEqual(want.mimeType) + expect(got.allActionsIncluded, `${name} manifest[${i}] allActionsIncluded`).toEqual( + want.allActionsIncluded, + ) + expect(got.localInceptions, `${name} manifest[${i}] localInceptions`).toEqual( + want.localInceptions, + ) + expect( + got.localTransformations, + `${name} manifest[${i}] localTransformations`, + ).toEqual(want.localTransformations) + expect(got.ingredients, `${name} manifest[${i}] ingredients`).toEqual(want.ingredients) + } + }) + } +}) diff --git a/src/lib/rubrics/json-formula.d.ts b/src/lib/rubrics/json-formula.d.ts new file mode 100644 index 0000000..aa2554e --- /dev/null +++ b/src/lib/rubrics/json-formula.d.ts @@ -0,0 +1,92 @@ +/** + * Ambient types for `@adobe/json-formula` 2.x. + * + * The published package has no `.d.ts` files — this shim covers the shape we + * actually consume in `engine.ts`. It is intentionally narrow. + */ +declare module '@adobe/json-formula' { + /** Opaque AST returned by `compile()`. Only valid input for `run()`. */ + export type JsonFormulaAst = unknown + + /** The interpreter instance passed as the 3rd arg to custom `_func`s. */ + export interface Interpreter { + /** Mutable map of `$name → value`. Used to inject `$argN` at call time. */ + globals: Record + /** Evaluate a compiled AST against a data value. */ + search(ast: JsonFormulaAst, data: unknown): unknown + } + + /** A parameter slot's type constraints — we pass `[]` or a list of `TYPE_ANY`. */ + export interface FunctionSignatureSlot { + types: number[] + optional?: boolean + variadic?: boolean + } + + export interface CustomFunctionEntry { + /** Called with (resolvedArgs, data, interpreter). */ + _func: ( + args: unknown[], + data: unknown, + interpreter: Interpreter, + ) => unknown + _signature: FunctionSignatureSlot[] + } + + export default class JsonFormula { + constructor( + customFunctions?: Record, + stringToNumber?: ((s: string) => number) | null, + debug?: unknown[], + ) + + /** Compile + run in one shot. */ + search( + expression: string, + json: unknown, + globals?: Record, + language?: string, + ): unknown + + /** Parse an expression once; reuse the AST with `run()`. */ + compile( + expression: string, + allowedGlobalNames?: string[], + ): JsonFormulaAst + + /** Evaluate a previously compiled AST. */ + run( + ast: JsonFormulaAst, + json: unknown, + language?: string, + globals?: Record, + ): unknown + } + + /** Enum of type constants used in `_signature` slots. `TYPE_ANY` is 1. */ + export const dataTypes: { + TYPE_NUMBER: 0 + TYPE_ANY: 1 + TYPE_STRING: 2 + TYPE_ARRAY: 3 + TYPE_OBJECT: 4 + TYPE_BOOLEAN: 5 + TYPE_EXPREF: 6 + TYPE_NULL: 7 + TYPE_ARRAY_NUMBER: 8 + TYPE_ARRAY_STRING: 9 + TYPE_ARRAY_ARRAY: 10 + TYPE_EMPTY_ARRAY: 11 + } + + /** One-shot convenience — compiles + runs. */ + export function jsonFormula( + json: unknown, + globals: Record, + expression: string, + customFunctions?: Record, + stringToNumber?: ((s: string) => number) | null, + debug?: unknown[], + language?: string, + ): unknown +} diff --git a/src/lib/rubrics/loader.ts b/src/lib/rubrics/loader.ts new file mode 100644 index 0000000..44fb468 --- /dev/null +++ b/src/lib/rubrics/loader.ts @@ -0,0 +1,180 @@ +/** + * Rubric loader — fetches rubric manifests and multi-document YAML files. + * + * Rubric YAML shape (matches the Python reference): + * + * rubric_metadata: + * name: ... + * version: ... + * language: en + * variables: # optional — shared $globals + * $well_formed_error_codes: [...] + * expressions: # optional — named reusable expressions + * _validationResults: |- + * (manifests[0].validationResults || {...}) + * --- + * - id: ... + * expression: ... + * reportText: { 'true': { en: ... }, 'false': { en: ... } } + * - id: ... + * ... + * + * The first document is the metadata (plus `variables` / `expressions` at the + * same top level). The second (and any subsequent) document is a list of + * statements; statements from all later docs are concatenated. + */ + +import { parseAllDocuments } from 'yaml' +import type { Rubric, RubricIndexEntry, RubricMetadata, RubricStatement } from './types' + +// All rubric assets live under this base URL (honors Vite's BASE_URL for GH Pages). +const RUBRICS_BASE = `${import.meta.env.BASE_URL}rubrics/` + +/** Fetch the index of available rubrics. */ +export async function loadRubricIndex(): Promise { + const url = `${RUBRICS_BASE}index.json` + const res = await fetch(url) + if (!res.ok) { + throw new Error(`Failed to load rubric index (${res.status}): ${url}`) + } + const json = (await res.json()) as { rubrics?: RubricIndexEntry[] } + return json.rubrics ?? [] +} + +/** Fetch and parse a single rubric YAML file. */ +export async function loadRubric(filename: string): Promise { + const url = `${RUBRICS_BASE}${filename}` + const res = await fetch(url) + if (!res.ok) { + throw new Error(`Failed to load rubric (${res.status}): ${url}`) + } + const yamlText = await res.text() + return parseRubricYaml(yamlText, filename) +} + +/** + * Parse the multi-document YAML into a Rubric. Exported for use in tests. + * Accepts a single YAML string (possibly containing multiple `---` docs). + */ +export function parseRubricYaml(yamlText: string, filenameForError = ''): Rubric { + const docs = parseAllDocuments(yamlText) + + if (docs.length === 0) { + throw new Error(`Empty rubric file: ${filenameForError}`) + } + + // Doc 0 — metadata. Accepts either { rubric_metadata: {...} } or just {...}. + const firstDoc = docs[0].toJSON() + const metadata = extractMetadata(firstDoc, filenameForError) + + // Docs 1..N — statements. Each doc is expected to be a list. + const statements: RubricStatement[] = [] + for (let i = 1; i < docs.length; i++) { + const raw = docs[i].toJSON() + if (raw == null) continue // Empty trailing document — skip. + if (!Array.isArray(raw)) { + throw new Error( + `Rubric ${filenameForError} document ${i} is not a list of statements`, + ) + } + for (const item of raw) { + const stmt = normalizeStatement(item, filenameForError) + if (stmt) statements.push(stmt) + } + } + + // Edge case: some rubrics put everything (metadata + statements) in a single + // document as { rubric_metadata, statements: [...] } — support that too. + if (statements.length === 0 && firstDoc && typeof firstDoc === 'object' && 'statements' in firstDoc) { + const inlineList = (firstDoc as { statements?: unknown }).statements + if (Array.isArray(inlineList)) { + for (const item of inlineList) { + const stmt = normalizeStatement(item, filenameForError) + if (stmt) statements.push(stmt) + } + } + } + + if (statements.length === 0) { + throw new Error(`Rubric ${filenameForError} contains no statements`) + } + + return { metadata, statements } +} + +function extractMetadata(doc: unknown, filenameForError: string): RubricMetadata { + if (doc && typeof doc === 'object') { + const obj = doc as Record + // `variables` and `expressions` sit at the top level of doc 0, as + // siblings of `rubric_metadata`. Pull them out once here so both the + // wrapped and unwrapped shapes get the same treatment. + const variables = coerceStringKeyedObject(obj.variables) + const expressions = coerceStringExpressions(obj.expressions) + + // Preferred: wrapped under `rubric_metadata`. + const wrapped = obj.rubric_metadata + if (wrapped && typeof wrapped === 'object') { + return coerceMetadata(wrapped as Record, variables, expressions) + } + // Fallback: metadata fields directly at the top level of doc 0. + if ('name' in obj) { + return coerceMetadata(obj, variables, expressions) + } + } + throw new Error(`Rubric ${filenameForError} is missing rubric_metadata`) +} + +function coerceMetadata( + obj: Record, + variables: Record | undefined, + expressions: Record | undefined, +): RubricMetadata { + return { + name: String(obj.name ?? 'Unnamed Rubric'), + issuer: obj.issuer != null ? String(obj.issuer) : undefined, + date: obj.date != null ? String(obj.date) : undefined, + version: obj.version != null ? String(obj.version) : undefined, + language: obj.language != null ? String(obj.language) : undefined, + variables, + expressions, + } +} + +/** Accept any object with string keys. Returns `undefined` for null/arrays/non-objects. */ +function coerceStringKeyedObject(raw: unknown): Record | undefined { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined + return raw as Record +} + +/** Expressions must be `{ [name]: string }`; drop non-string values. */ +function coerceStringExpressions(raw: unknown): Record | undefined { + const obj = coerceStringKeyedObject(raw) + if (!obj) return undefined + const out: Record = {} + for (const [k, v] of Object.entries(obj)) { + if (typeof v === 'string') out[k] = v + } + return Object.keys(out).length > 0 ? out : undefined +} + +function normalizeStatement(item: unknown, filenameForError: string): RubricStatement | null { + if (!item || typeof item !== 'object') return null + const obj = item as Record + const id = typeof obj.id === 'string' ? obj.id : null + const expression = typeof obj.expression === 'string' ? obj.expression : null + if (!id || !expression) { + // Malformed entry — skip rather than crash the whole file. Warn in dev. + console.warn( + `[rubrics] Skipping malformed statement in ${filenameForError}:`, + obj, + ) + return null + } + return { + id, + description: typeof obj.description === 'string' ? obj.description : undefined, + expression, + failIfMatched: obj.failIfMatched === true, + reportText: (obj.reportText ?? undefined) as RubricStatement['reportText'], + } +} diff --git a/src/lib/rubrics/perManifest.test.ts b/src/lib/rubrics/perManifest.test.ts new file mode 100644 index 0000000..c80cfa1 --- /dev/null +++ b/src/lib/rubrics/perManifest.test.ts @@ -0,0 +1,115 @@ +/** + * Unit-level behavior tests for the per-manifest signals evaluator. + * + * The *parity* assertions against `capture.signals.json` (and all other + * upstream fixtures) live in `goldens.test.ts`, which iterates the full + * fixture set. This file exercises pieces that aren't easily demonstrated + * by upstream fixtures — coercion edge cases, ingredient back-fill, and + * the strict "at least one actions assertion" rule for `allActionsIncluded`. + */ + +import { describe, expect, it } from 'vitest' +import type { CrJson } from '../crjson' +import { parseRubricYaml } from './loader' +import { evaluatePerManifest } from './perManifest' + +describe('per-manifest evaluator · local unit behavior', () => { + it('emits only truthy signals and groups by id prefix', () => { + const rubric = parseRubricYaml( + [ + 'rubric_metadata:', + ' name: mini', + '---', + '- id: inception:always_true', + ' expression: "`true`"', + " reportText: { 'true': { en: yep } }", + '- id: transformation:always_false', + ' expression: "`false`"', + " reportText: { 'true': { en: nope } }", + '- id: inception:list_signal', + ' expression: |-', + " assertions.'c2pa.actions'.actions[?action == \"c2pa.created\"].action", + " reportText: { 'true': { en: created } }", + ].join('\n'), + 'inline', + ) + + const report: CrJson = { + manifests: [ + { + label: 'urn:c2pa:test-1', + assertions: { + 'c2pa.actions': { + actions: [{ action: 'c2pa.created' }, { action: 'c2pa.created' }], + }, + }, + }, + ], + } as unknown as CrJson + + const result = evaluatePerManifest(rubric, report, { rubricId: 'mini' }) + expect(result.manifests).toHaveLength(1) + const m = result.manifests[0] + expect(m.localInceptions.map((s) => s.trait)).toEqual([ + 'inception:always_true', + 'inception:list_signal', + ]) + // two 'c2pa.created' → list length 2 → multiple = true + expect(m.localInceptions.find((s) => s.trait === 'inception:list_signal')?.multiple).toBe(true) + expect(m.localTransformations).toEqual([]) + }) + + it('resolves parent mimeType via child ingredient back-fill', () => { + const rubric = parseRubricYaml( + ['rubric_metadata:', ' name: empty', '---', '- id: x:never', " expression: \"`false`\"", " reportText: { 'true': { en: n } }"].join('\n'), + 'inline', + ) + const report: CrJson = { + manifests: [ + { + label: 'urn:c2pa:parent', + assertions: {}, + }, + { + label: 'urn:c2pa:child', + assertions: { + 'c2pa.ingredient.v3': { + 'dc:format': 'image/jpeg', + relationship: 'parentOf', + activeManifest: { url: 'self#jumbf=/c2pa/urn:c2pa:parent/c2pa.assertions/x' }, + }, + }, + }, + ], + } as unknown as CrJson + + const result = evaluatePerManifest(rubric, report, { rubricId: 'empty' }) + expect(result.manifests[0].mimeType).toBe('image/jpeg') + expect(result.manifests[1].ingredients).toEqual([{ index: 0, relationship: 'parentOf' }]) + }) + + it('allActionsIncluded requires at least one actions assertion', () => { + const rubric = parseRubricYaml( + ['rubric_metadata:', ' name: e', '---', '- id: x:n', " expression: \"`false`\"", " reportText: { 'true': { en: n } }"].join('\n'), + 'inline', + ) + const report: CrJson = { + manifests: [ + { label: 'urn:c2pa:none', assertions: {} }, + { + label: 'urn:c2pa:yes', + assertions: { 'c2pa.actions.v2': { actions: [], allActionsIncluded: true } }, + }, + { + label: 'urn:c2pa:partial', + assertions: { 'c2pa.actions.v2': { actions: [], allActionsIncluded: false } }, + }, + ], + } as unknown as CrJson + + const result = evaluatePerManifest(rubric, report, { rubricId: 'e' }) + expect(result.manifests[0].allActionsIncluded).toBe(false) // no actions → false + expect(result.manifests[1].allActionsIncluded).toBe(true) + expect(result.manifests[2].allActionsIncluded).toBe(false) + }) +}) diff --git a/src/lib/rubrics/perManifest.ts b/src/lib/rubrics/perManifest.ts new file mode 100644 index 0000000..3bcf050 --- /dev/null +++ b/src/lib/rubrics/perManifest.ts @@ -0,0 +1,304 @@ +/** + * Per-manifest rubric evaluator — used for "signals" rubrics where every + * statement is a local predicate applied to a single manifest, and only + * *truthy* outcomes are reported. + * + * This is a direct port of the Python reference at + * `c2pa_signals_rubric_evaluator.py` (in asset-rubrics/). The algorithm: + * + * 1. Build a label → index mapping over `report.manifests`. + * 2. For every manifest, run each statement's json-formula expression + * with that manifest as the root. Only record signals where the + * result coerces to true. + * 3. Build the "mimeType" and ingredient-DAG metadata per manifest, so + * the UI can show ingredient edges, assertedBy, and allActionsIncluded. + * + * We intentionally do NOT share coerce() with `evaluate.ts` here: the + * document-mode evaluator exposes pass/fail + matches (including support + * for `failIfMatched`), while this mode is strictly "truthy emit, falsy drop". + * Diverging intents → diverging code; keeping them separate prevents surprise + * coupling when either evaluation mode grows. + */ + +import type { CrJson, CrJsonManifestEntry } from '../crjson' +import { createEngine, type RubricEngine } from './engine' +import type { + AssertedBy, + IngredientEdge, + ManifestSignalsResult, + Rubric, + RubricStatement, + SignalHit, + SignalsRubricResult, +} from './types' + +const DEFAULT_LOCALE = 'en' + +export function evaluatePerManifest( + rubric: Rubric, + report: CrJson, + options: { rubricId: string; locale?: string } = { rubricId: 'unknown' }, +): SignalsRubricResult { + const locale = options.locale ?? rubric.metadata.language ?? DEFAULT_LOCALE + const manifests = Array.isArray(report.manifests) ? report.manifests : [] + + // One engine for the whole evaluation: its custom `_expression()` functions + // are pure (they re-evaluate their AST against whatever `data` is passed), + // so we can safely reuse a single instance across all manifests. + const engine = createEngine(rubric.metadata) + + // label → string index, mirroring the Python reference. + const indexMapping = buildIndexMapping(manifests) + + // Derive per-manifest mime types, resolving parent fills from child + // ingredient assertions (same pass as the Python DAG builder). + const mimeTypes = resolveMimeTypes(manifests, indexMapping) + + const manifestResults: ManifestSignalsResult[] = manifests.map((manifest, idx) => { + const signals = evaluateManifestSignals(manifest, rubric.statements, locale, engine) + + const localInceptions: SignalHit[] = [] + const localTransformations: SignalHit[] = [] + for (const s of signals) { + if (s.trait.startsWith('inception:')) localInceptions.push(s) + else if (s.trait.startsWith('transformation:')) localTransformations.push(s) + } + + return { + assertedBy: extractAssertedBy(manifest), + mimeType: mimeTypes[idx] ?? null, + localInceptions, + localTransformations, + allActionsIncluded: computeAllActionsIncluded(manifest), + ingredients: extractIngredients(manifest, indexMapping), + } + }) + + return { + rubricId: options.rubricId, + rubricName: rubric.metadata.name, + rubricVersion: rubric.metadata.version, + mode: 'per-manifest', + manifests: manifestResults, + evaluatedAt: new Date(), + } +} + +// ── Signal evaluation ──────────────────────────────────────────────── + +function evaluateManifestSignals( + manifest: CrJsonManifestEntry, + statements: RubricStatement[], + locale: string, + engine: RubricEngine, +): SignalHit[] { + const hits: SignalHit[] = [] + for (const stmt of statements) { + if (!stmt.expression || !stmt.id) continue + + let val: unknown + try { + val = engine.search(stmt.expression.trim(), manifest) + } catch (e) { + // Match Python: log and skip; don't let one bad expression kill the run. + // eslint-disable-next-line no-console + console.warn(`[rubrics] Error evaluating ${stmt.id}:`, e) + continue + } + + const { truthy, multiple } = coerceTruthy(val) + if (!truthy) continue + + hits.push({ + trait: stmt.id, + reportText: pickTrueText(stmt, locale) ?? stmt.id, + multiple, + }) + } + return hits +} + +/** + * Truthy-only coercion (mirror of the Python reference for signals mode): + * list → truthy if non-empty; multiple if length > 1 + * bool → as-is + * number → truthy if > 0 + * null → falsy + * other → truthy + */ +function coerceTruthy(val: unknown): { truthy: boolean; multiple: boolean } { + if (Array.isArray(val)) return { truthy: val.length > 0, multiple: val.length > 1 } + if (typeof val === 'boolean') return { truthy: val, multiple: false } + if (typeof val === 'number') return { truthy: val > 0, multiple: false } + if (val == null) return { truthy: false, multiple: false } + return { truthy: true, multiple: false } +} + +function pickTrueText(stmt: RubricStatement, locale: string): string | undefined { + const dict = stmt.reportText + if (!dict) return undefined + const chosen = dict['true'] ?? dict.default + if (!chosen) return undefined + if (typeof chosen === 'string') return chosen + return chosen[locale] ?? chosen[DEFAULT_LOCALE] +} + +// ── Metadata extraction ────────────────────────────────────────────── + +function extractAssertedBy(manifest: CrJsonManifestEntry): AssertedBy { + const sig = (manifest.signature ?? {}) as Record + const certInfo = (sig.certificateInfo ?? sig.certificate_info ?? {}) as Record + const subject = (certInfo.subject ?? {}) as Record + + const CN = typeof subject.CN === 'string' ? subject.CN : 'Unknown CN' + const O = typeof subject.O === 'string' ? subject.O : 'Unknown O' + const OU = typeof subject.OU === 'string' ? subject.OU : undefined + + const out: AssertedBy = { CN, O } + if (OU) out.OU = OU + return out +} + +/** + * Mirror the Python reference: `allActionsIncluded` is true iff at least + * one actions assertion exists and every such assertion has + * `allActionsIncluded === true`. If no actions assertions are present, + * the value is false (not vacuously true). + */ +function computeAllActionsIncluded(manifest: CrJsonManifestEntry): boolean { + const assertions = (manifest.assertions ?? {}) as Record + let actionsFound = false + let allIncluded = true + for (const value of Object.values(assertions)) { + if (!value || typeof value !== 'object') continue + const obj = value as Record + if (!('actions' in obj)) continue + actionsFound = true + if (obj.allActionsIncluded !== true) { + allIncluded = false + break + } + } + return actionsFound && allIncluded +} + +function extractIngredients( + manifest: CrJsonManifestEntry, + indexMapping: Map, +): IngredientEdge[] { + const assertions = (manifest.assertions ?? {}) as Record + const edges: IngredientEdge[] = [] + + for (const [key, rawValue] of Object.entries(assertions)) { + if (!key.startsWith('c2pa.ingredient')) continue + if (!rawValue || typeof rawValue !== 'object') continue + const value = rawValue as Record + + const manifestRef = (value.c2pa_manifest ?? value.activeManifest ?? {}) as Record + const url = typeof manifestRef.url === 'string' ? manifestRef.url : undefined + const relationship = typeof value.relationship === 'string' ? value.relationship : undefined + + if (!url) continue + + const parentUrn = parseParentUrn(url) + const parentIdx = indexMapping.get(parentUrn) + if (parentIdx == null) { + // Parent not present in this bundle — mirror Python's warning-only behavior. + // eslint-disable-next-line no-console + console.warn(`[rubrics] Could not map parent URN ${parentUrn} to index`) + continue + } + edges.push({ index: parentIdx, relationship }) + } + return edges +} + +/** + * Extract the manifest label from a JUMBF URL. + * Handles both new-style (urn:c2pa:UUID) and old-style (self#jumbf=/c2pa/