From fd37054c2e2fe368c00fad613535a29fc00cc86d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 02:19:28 +0000 Subject: [PATCH 1/8] =?UTF-8?q?feat(layer-101):=20Content=20Verification?= =?UTF-8?q?=20System=20=E2=80=94=20sign,=20chain=20of=20custody,=20humanit?= =?UTF-8?q?y=20score?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements ECDSA P-256 in-browser signing with a hash-linked chain of edit steps (lite C2PA), plus a heuristic humanity score so creators can tell whether a draft reads polished or unmistakably human. Inspired by the IG post on cryptographic provenance for content in the AI era. - contentProvenance.js: ensureIdentity / signContent / appendEdit / verifyManifest / humanityScore. SubtleCrypto-backed, FNV fallback for display-only environments. Manifest schema brainsnn-prov/1. - ContentProvenancePanel.jsx: Sign / Verify / Humanity / Log tabs. Identity card with handle + fingerprint. Manifest copy-to-clipboard. Smoke-tested: legitimate verify passes, tampered payload caught, 2-step chain verifies end-to-end, prevHash tamper caught. --- .ai-memory/MEMORY.md | 23 + brainsnn-r3f-app/src/App.jsx | 5 + .../src/components/ContentProvenancePanel.jsx | 500 ++++++++++++++++++ .../src/utils/contentProvenance.js | 497 +++++++++++++++++ brainsnn-r3f-app/src/utils/layerCatalog.js | 1 + 5 files changed, 1026 insertions(+) create mode 100644 brainsnn-r3f-app/src/components/ContentProvenancePanel.jsx create mode 100644 brainsnn-r3f-app/src/utils/contentProvenance.js diff --git a/.ai-memory/MEMORY.md b/.ai-memory/MEMORY.md index cfe0846..7d66aeb 100644 --- a/.ai-memory/MEMORY.md +++ b/.ai-memory/MEMORY.md @@ -697,3 +697,26 @@ Club Penguin-style AI debate arena live at https://penguinwalk.co per-user immunity / streak / scan-count / receipt stats, and lists the four ways to navigate (⌘K / Shift-? / Role Tour / Layer Explorer). The "you made it here" panel. +101. Content Verification System — sign humanity, verify chain of custody + - ECDSA P-256 keypair via Web Crypto, stored locally, exportable + via Layer 57. Manifest format `brainsnn-prov/1` carries the pub + key + handle + a hash-linked chain of signed steps (origin → edit + → edit). Each step canonicalizes its fields, SHA-256s them, and + signs that with the creator's private key + - signContent() creates an origin step with optional device / + location / note context. appendEdit() adds a chained edit step + whose prevHash references the previous step's hash — a + lite-C2PA chain of custody for caption / photo updates + - verifyManifest({ manifest, payload }) walks every step, re-derives + its hash, verifies the signature against the manifest's pub key, + checks prevHash linkage, and confirms the latest payloadHash + matches the supplied payload. Returns + { ok, reason, fingerprint, handle, steps } + - humanityScore(text) — heuristic 0-100 over five axes (cadence + variance, candid markers, contractions, irregularities, AI + boilerplate). Tier ladder: AI-perfect / leans polished / mixed + signals / reads human / unmistakably human. Per-axis evidence + so creators see WHY a draft reads polished + - ContentProvenancePanel: Sign / Verify / Humanity / Log tabs. + Identity card with handle + fingerprint. Manifest copy-to-clipboard + for sharing. Recent-manifest log capped at 25 in localStorage diff --git a/brainsnn-r3f-app/src/App.jsx b/brainsnn-r3f-app/src/App.jsx index afb3b3f..a76823d 100644 --- a/brainsnn-r3f-app/src/App.jsx +++ b/brainsnn-r3f-app/src/App.jsx @@ -81,6 +81,7 @@ import HotkeyMap from './components/HotkeyMap'; import ThemePanel from './components/ThemePanel'; import CommunityPackPanel from './components/CommunityPackPanel'; import MilestonePanel from './components/MilestonePanel'; +import ContentProvenancePanel from './components/ContentProvenancePanel'; import { registerServiceWorker } from './utils/pwa'; import { registerTheme } from './utils/theme'; import DreamModePanel from './components/DreamModePanel'; @@ -779,6 +780,10 @@ export default function App() { + + + + diff --git a/brainsnn-r3f-app/src/components/ContentProvenancePanel.jsx b/brainsnn-r3f-app/src/components/ContentProvenancePanel.jsx new file mode 100644 index 0000000..7b4b1d5 --- /dev/null +++ b/brainsnn-r3f-app/src/components/ContentProvenancePanel.jsx @@ -0,0 +1,500 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { + isCryptoAvailable, + ensureIdentity, + loadIdentity, + loadHandle, + setHandle, + fingerprintFor, + signContent, + appendEdit, + verifyManifest, + recentManifestLog, + logManifest, + clearManifestLog, + manifestToShareString, + tryParseManifest, + humanityScore, +} from '../utils/contentProvenance'; + +/** + * Layer 101 — Content Verification System panel. + * + * Three tabs: + * - Sign: capture context, sign payload, output a manifest + * - Verify: paste a manifest + payload, get pass/fail + chain view + * - Humanity: run the heuristic humanity score on a draft + * + * The whole thing runs in-browser — keys never leave the device. + */ +export default function ContentProvenancePanel() { + const supported = isCryptoAvailable(); + const [tab, setTab] = useState('sign'); + const [identity, setIdentity] = useState(() => loadIdentity()); + const [handle, setHandleState] = useState(() => loadHandle()); + const [fp, setFp] = useState(''); + + useEffect(() => { + let alive = true; + if (identity?.pub) { + fingerprintFor(identity.pub).then((f) => { if (alive) setFp(f); }); + } + return () => { alive = false; }; + }, [identity]); + + async function handleGenerate() { + if (!supported) return; + const r = await ensureIdentity({ handle }); + if (r.ok) { + setIdentity(loadIdentity()); + setFp(r.identity.fingerprint); + } + } + + function handleHandleChange(e) { + const v = e.target.value; + setHandleState(v); + setHandle(v); + } + + return ( +
+
Layer 101 · content verification
+

Sign your humanity

+

+ AI-perfect imagery is now cheap. The signal that cuts through is + a verifiable chain of custody — who captured it, on what device, + and which edits followed. Sign your content with a local keypair. + Anyone can verify your post hasn't been swapped for a fake. +

+ + + +
+ + + + +
+ + {tab === 'sign' && } + {tab === 'verify' && } + {tab === 'humanity' && } + {tab === 'log' && } +
+ ); +} + +function Tab({ id, tab, setTab, label }) { + const on = tab === id; + return ( + + ); +} + +function IdentityCard({ supported, identity, fingerprint, handle, onHandle, onGenerate }) { + if (!supported) { + return ( +
+ Web Crypto unavailable. +

+ Signing needs SubtleCrypto, which requires HTTPS or localhost. + The Humanity tab still works. +

+
+ ); + } + + const hasIdentity = !!identity?.priv && !!identity?.pub; + const tone = hasIdentity ? '#5ee69a' : '#fdab43'; + + return ( +
+
+ {hasIdentity ? 'Creator key ready' : 'No creator key yet'} + + {hasIdentity ? 'ECDSA P-256 · stored locally' : 'click to generate'} + +
+
+ + +
+ {fingerprint && ( +

+ fp · {fingerprint.slice(0, 16)}… +

+ )} +
+ ); +} + +function SignTab({ supported, identity }) { + const [payload, setPayload] = useState(''); + const [device, setDevice] = useState(''); + const [location, setLocation] = useState(''); + const [note, setNote] = useState(''); + const [manifest, setManifest] = useState(null); + const [editNote, setEditNote] = useState(''); + const [err, setErr] = useState(''); + + const ready = supported && identity?.priv; + + async function doSign() { + setErr(''); + if (!ready) { setErr('Generate a key first'); return; } + if (!payload.trim()) { setErr('Paste content to sign'); return; } + const r = await signContent({ + payload, + payloadKind: 'text', + device, + location, + note, + }); + if (!r.ok) { setErr(r.reason || 'sign failed'); return; } + setManifest(r.manifest); + logManifest({ manifest: r.manifest, payloadExcerpt: payload }); + } + + async function doAppendEdit() { + setErr(''); + if (!manifest) return; + const r = await appendEdit({ manifest, payload, note: editNote }); + if (!r.ok) { setErr(r.reason || 'edit failed'); return; } + setManifest(r.manifest); + setEditNote(''); + logManifest({ manifest: r.manifest, payloadExcerpt: payload }); + } + + function copyManifest() { + if (!manifest) return; + try { navigator.clipboard.writeText(manifestToShareString(manifest)); } catch { /* noop */ } + } + + return ( +
+