From 3db50e0479b47f054a65b1a392b2c8053aa998c2 Mon Sep 17 00:00:00 2001 From: midiakiasat Date: Mon, 1 Jun 2026 20:09:09 +0200 Subject: [PATCH] Add TRUTHFRAMER public independent verification console --- ...BLIC_INDEPENDENT_VERIFICATION_WITNESS.json | 51 ++++ ...RUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs | 227 ++++++++++++++++++ docs/verifier/index.html | 46 ++++ package.json | 9 +- ...dependent-verification-witness-v1.7.0.json | 51 ++++ ...public-independent-verification-witness.js | 57 +++++ ...ndependent-verification-witness-network.js | 61 +++++ ...public-independent-verification-witness.js | 62 +++++ ...BLIC_INDEPENDENT_VERIFICATION_WITNESS.json | 51 ++++ ...RUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs | 227 ++++++++++++++++++ verifier/index.html | 46 ++++ 11 files changed, 885 insertions(+), 3 deletions(-) create mode 100644 docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json create mode 100644 docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs create mode 100644 docs/verifier/index.html create mode 100644 reports/current/public-independent-verification-witness-v1.7.0.json create mode 100755 scripts/generate-public-independent-verification-witness.js create mode 100755 scripts/verify-public-independent-verification-witness-network.js create mode 100755 scripts/verify-public-independent-verification-witness.js create mode 100644 verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json create mode 100644 verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs create mode 100644 verifier/index.html diff --git a/docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json b/docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json new file mode 100644 index 0000000..35b3460 --- /dev/null +++ b/docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json @@ -0,0 +1,51 @@ +{ + "protocol": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS", + "version": "v1.7.0", + "status": "PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_BOUND", + "closure_claim": "A third party can verify TRUTHFRAMER public truth-frame continuity from public URLs only, without private source, local repository state, maintainer terminal output, or hidden context.", + "public_console_path": "/verifier/", + "public_verifier_script": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs", + "public_checked_url_count": 2, + "verifier": { + "protocol": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER", + "version": "v1.7.0", + "status": "PUBLIC_INDEPENDENT_VERIFICATION_PASS", + "public_inputs": { + "public_verification_capsule_url": "https://truthframer.github.io/truthframer-platform/capsule/TRUTHFRAMER_PUBLIC_VERIFICATION_CAPSULE.json", + "public_capsule_consumption_receipt_url": "https://truthframer.github.io/truthframer-platform/receipt/TRUTHFRAMER_PUBLIC_CAPSULE_CONSUMPTION_RECEIPT.json" + }, + "public_object_digests": { + "capsule_raw_bytes_sha256": "8f1b6dc9cf953c91f1f300ee28d4b66378059d44c464989d00aa385f3c3163e4", + "capsule_canonical_json_sha256": "7d252db39d7f98ca1f239d3ecac2abaca0cfcad050c471e6efe1a1b7ec00f028", + "receipt_raw_bytes_sha256": "94ee2f308b5a3c3415534b9439b0ad394dd338d41e8d6eb8eebb9563c7b1df48", + "receipt_canonical_json_sha256": "ae2b0b1e7867d76ffb2df7ff97ebc6135e5b7b3eed5857b99a5d2161b75945f2" + }, + "observed_public_claims": { + "capsule_sha256": "957b92e87d6e6ca0f341702e367cb454347b66eba143e4046e3f0e4a8489fe17", + "receipt_sha256": "ae2b0b1e7867d76ffb2df7ff97ebc6135e5b7b3eed5857b99a5d2161b75945f2", + "receipt_sha256_basis": "computed_public_receipt_canonical_json_sha256", + "receipt_capsule_sha256": "957b92e87d6e6ca0f341702e367cb454347b66eba143e4046e3f0e4a8489fe17", + "capsule_fetched_from_public_url": true, + "capsule_sha256_match": true, + "capsule_sha256_match_basis": "expected_sha256_found_inside_public_capsule_json", + "capsule_artifacts_live": true, + "release_tag_bound": true, + "no_private_source_required": true + }, + "assertions": { + "capsule_public_fetch_ok": true, + "receipt_public_fetch_ok": true, + "capsule_declares_sha256": true, + "receipt_has_public_sha256_basis": true, + "receipt_capsule_sha256_matches_capsule": true, + "receipt_claims_capsule_public_fetch": true, + "receipt_claims_capsule_sha256_match": true, + "receipt_claims_capsule_artifacts_live": true, + "receipt_claims_release_tag_bound": true, + "receipt_claims_no_private_source_required": true, + "receipt_sha_basis_is_public_capsule_internal_digest": true + }, + "verifier_result_sha256": "3de5138348ac2a3227ec2f60c3c062afeddf209a837664b8881f7f020529b2f1" + }, + "witness_sha256": "132a366bc7aee1df8fddd0ebf87fc2d21d107afd915b51a489aaed13c12c2323" +} diff --git a/docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs b/docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs new file mode 100644 index 0000000..0960b04 --- /dev/null +++ b/docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs @@ -0,0 +1,227 @@ +const DEFAULTS = Object.freeze({ + version: "v1.7.0", + capsuleUrl: "https://truthframer.github.io/truthframer-platform/capsule/TRUTHFRAMER_PUBLIC_VERIFICATION_CAPSULE.json", + receiptUrl: "https://truthframer.github.io/truthframer-platform/receipt/TRUTHFRAMER_PUBLIC_CAPSULE_CONSUMPTION_RECEIPT.json" +}); + +export function stableStringify(value) { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"; + const keys = Object.keys(value).sort(); + return "{" + keys.map((key) => JSON.stringify(key) + ":" + stableStringify(value[key])).join(",") + "}"; +} + +export async function sha256Hex(input) { + const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input; + + if (globalThis.crypto && globalThis.crypto.subtle) { + const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join(""); + } + + const { createHash } = await import("node:crypto"); + return createHash("sha256").update(Buffer.from(bytes)).digest("hex"); +} + +function normalizeKey(key) { + return String(key).replace(/[^a-zA-Z0-9]/g, "").toUpperCase(); +} + +function isSha256(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/i.test(value); +} + +function asString(value) { + if (value === null || value === undefined) return ""; + return String(value); +} + +function asBool(value) { + if (value === true) return true; + if (value === false) return false; + if (typeof value === "string") return value.toLowerCase() === "true"; + return false; +} + +function walk(value, path = [], out = []) { + if (!value || typeof value !== "object") return out; + + if (Array.isArray(value)) { + value.forEach((item, index) => walk(item, path.concat(String(index)), out)); + return out; + } + + for (const [key, child] of Object.entries(value)) { + const itemPath = path.concat(key); + out.push({ + key, + normalizedKey: normalizeKey(key), + normalizedPath: itemPath.map(normalizeKey).join("."), + value: child + }); + walk(child, itemPath, out); + } + + return out; +} + +function firstBoolBySemanticKey(root, names) { + const wanted = new Set(names.map(normalizeKey)); + const hit = walk(root).find((entry) => wanted.has(entry.normalizedKey)); + return hit ? asBool(hit.value) : false; +} + +function firstStringBySemanticKey(root, names) { + const wanted = new Set(names.map(normalizeKey)); + const hit = walk(root).find((entry) => wanted.has(entry.normalizedKey)); + return hit ? asString(hit.value) : ""; +} + +function shaCandidates(root, predicate) { + return walk(root) + .filter((entry) => isSha256(entry.value)) + .filter(predicate) + .map((entry) => ({ + key: entry.key, + path: entry.normalizedPath, + value: String(entry.value).toLowerCase() + })); +} + +function preferredSha(root, predicate) { + const hits = shaCandidates(root, predicate); + if (!hits.length) return ""; + const exact = hits.find((hit) => hit.path.split(".").length <= 3); + return (exact || hits[0]).value; +} + +function capsuleShaFromCapsule(capsuleJson) { + return preferredSha(capsuleJson, (entry) => { + const k = entry.normalizedKey; + const p = entry.normalizedPath; + return ( + k.endsWith("CAPSULESHA256") && + !p.includes("RAWBYTES") && + !p.includes("CANONICALJSON") && + !p.includes("WITHOUTSHAFIELDS") && + !p.includes("RECEIPT") + ); + }); +} + +function capsuleShaFromReceipt(receiptJson) { + return preferredSha(receiptJson, (entry) => { + const k = entry.normalizedKey; + const p = entry.normalizedPath; + return ( + k.endsWith("CAPSULESHA256") && + !p.includes("RAWBYTES") && + !p.includes("CANONICALJSON") && + !p.includes("WITHOUTSHAFIELDS") && + !p.includes("RECEIPT") + ); + }); +} + +function receiptShaFromReceipt(receiptJson) { + return preferredSha(receiptJson, (entry) => { + const k = entry.normalizedKey; + const p = entry.normalizedPath; + return ( + k.endsWith("RECEIPTSHA256") || + k.endsWith("CONSUMPTIONRECEIPTSHA256") || + p.endsWith("RECEIPTSHA256") || + p.endsWith("CONSUMPTIONRECEIPTSHA256") + ); + }); +} + +async function fetchPublicJson(url) { + const response = await fetch(url, { cache: "no-store" }); + const text = await response.text(); + + if (!response.ok) { + throw new Error(`PUBLIC_FETCH_FAILED url=${url} status=${response.status}`); + } + + return { + url, + status: response.status, + text, + json: JSON.parse(text), + raw_bytes_sha256: await sha256Hex(text), + canonical_json_sha256: await sha256Hex(stableStringify(JSON.parse(text))) + }; +} + +export async function verifyTruthframerPublicIndependently(config = {}) { + const capsuleUrl = config.capsuleUrl || DEFAULTS.capsuleUrl; + const receiptUrl = config.receiptUrl || DEFAULTS.receiptUrl; + + const capsule = await fetchPublicJson(capsuleUrl); + const receipt = await fetchPublicJson(receiptUrl); + + const capsuleDeclaredSha = capsuleShaFromCapsule(capsule.json); + const receiptCapsuleSha = capsuleShaFromReceipt(receipt.json); + const receiptDeclaredSha = receiptShaFromReceipt(receipt.json); + const receiptObservedSha = receiptDeclaredSha || receipt.canonical_json_sha256; + + const claims = { + capsule_sha256: capsuleDeclaredSha, + receipt_sha256: receiptObservedSha, + receipt_sha256_basis: receiptDeclaredSha ? "declared_inside_public_receipt_json" : "computed_public_receipt_canonical_json_sha256", + receipt_capsule_sha256: receiptCapsuleSha, + capsule_fetched_from_public_url: firstBoolBySemanticKey(receipt.json, ["CAPSULE_FETCHED_FROM_PUBLIC_URL"]), + capsule_sha256_match: firstBoolBySemanticKey(receipt.json, ["CAPSULE_SHA256_MATCH"]), + capsule_sha256_match_basis: firstStringBySemanticKey(receipt.json, ["CAPSULE_SHA256_MATCH_BASIS"]), + capsule_artifacts_live: firstBoolBySemanticKey(receipt.json, ["CAPSULE_ARTIFACTS_LIVE"]), + release_tag_bound: firstBoolBySemanticKey(receipt.json, ["RELEASE_TAG_BOUND"]), + no_private_source_required: firstBoolBySemanticKey(receipt.json, ["NO_PRIVATE_SOURCE_REQUIRED"]) + }; + + const assertions = { + capsule_public_fetch_ok: capsule.status === 200, + receipt_public_fetch_ok: receipt.status === 200, + capsule_declares_sha256: isSha256(claims.capsule_sha256), + receipt_has_public_sha256_basis: isSha256(claims.receipt_sha256), + receipt_capsule_sha256_matches_capsule: claims.receipt_capsule_sha256 === claims.capsule_sha256, + receipt_claims_capsule_public_fetch: claims.capsule_fetched_from_public_url === true, + receipt_claims_capsule_sha256_match: claims.capsule_sha256_match === true, + receipt_claims_capsule_artifacts_live: claims.capsule_artifacts_live === true, + receipt_claims_release_tag_bound: claims.release_tag_bound === true, + receipt_claims_no_private_source_required: claims.no_private_source_required === true, + receipt_sha_basis_is_public_capsule_internal_digest: + claims.capsule_sha256_match_basis === "expected_sha256_found_inside_public_capsule_json" + }; + + const pass = Object.values(assertions).every(Boolean); + + const result = { + protocol: "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER", + version: DEFAULTS.version, + status: pass ? "PUBLIC_INDEPENDENT_VERIFICATION_PASS" : "PUBLIC_INDEPENDENT_VERIFICATION_FAIL", + public_inputs: { + public_verification_capsule_url: capsuleUrl, + public_capsule_consumption_receipt_url: receiptUrl + }, + public_object_digests: { + capsule_raw_bytes_sha256: capsule.raw_bytes_sha256, + capsule_canonical_json_sha256: capsule.canonical_json_sha256, + receipt_raw_bytes_sha256: receipt.raw_bytes_sha256, + receipt_canonical_json_sha256: receipt.canonical_json_sha256 + }, + observed_public_claims: claims, + assertions + }; + + result.verifier_result_sha256 = await sha256Hex(stableStringify(result)); + return result; +} + +if (typeof window !== "undefined") { + window.TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER = { + verifyTruthframerPublicIndependently, + stableStringify, + sha256Hex + }; +} diff --git a/docs/verifier/index.html b/docs/verifier/index.html new file mode 100644 index 0000000..419af68 --- /dev/null +++ b/docs/verifier/index.html @@ -0,0 +1,46 @@ + + + + + TRUTHFRAMER Public Independent Verification Console + + + + +
+

TRUTHFRAMER Public Independent Verification Console

+

+ This page verifies the public capsule and public consumption receipt from public URLs only. + No private source, local repository state, maintainer terminal output, or hidden context is required. +

+

VERIFYING...

+

+
+ + + diff --git a/package.json b/package.json index 6077ee3..021fe0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "truthframer-platform", - "version": "1.6.0", + "version": "1.7.0", "private": true, "description": "The object spine for TRUTHFRAMER: replayable visual truth frames for market reality.", "scripts": { @@ -8,7 +8,7 @@ "verify": "node scripts/verify-truthframer.js", "verify:render": "node scripts/verify-render.js", "verify:public": "node scripts/verify-public-surface.js", - "verify:all": "npm run verify && npm run verify:render && npm run verify:public && npm run verify:registry && npm run verify:readme && npm run verify:tf000002 && npm run verify:tf000003 && npm run verify:tf000004 && npm run verify:root && npm run verify:audit && npm run verify:hardening && npm run verify:network-seal && npm run verify:verification-index && npm run verify:index-seal && npm run verify:verification-atlas && npm run verify:atlas-seal && npm run verify:stack-closure && npm run verify:stack-closure-seal && npm run verify:continuity-sentinel && npm run verify:continuity-sentinel-seal && npm run verify:release-closure && npm run verify:release-closure-seal && npm run verify:root-finality && npm run verify:root-finality-seal && npm run verify:privacy-perimeter && npm run verify:legal-privacy-perimeter && npm run verify:legal-privacy-perimeter-seal && npm run verify:legal-privacy-network && npm run verify:distribution-egress && npm run verify:distribution-egress-firewall && npm run verify:distribution-egress-firewall-seal && npm run verify:distribution-egress-network && npm run verify:source-provenance && npm run verify:source-provenance-seal && npm run verify:source-provenance-network && npm run verify:verification-purity && npm run verify:verification-immutability && npm run verify:verification-immutability-seal && npm run verify:verification-immutability-network && npm run verify:truth-frame-spine && npm run verify:truth-frame-spine-network && npm run verify:truth-frame-admission && npm run verify:truth-frame-admission-network && npm run verify:truth-frame-admission-refusal && npm run verify:truth-frame-admission-refusal-network && npm run verify:truth-frame-admission-closure && npm run verify:truth-frame-admission-closure-network && npm run verify:public-verification-capsule && npm run verify:public-verification-capsule-network && npm run verify:public-capsule-consumption-receipt && npm run verify:public-capsule-consumption-receipt-network", + "verify:all": "npm run verify && npm run verify:render && npm run verify:public && npm run verify:registry && npm run verify:readme && npm run verify:tf000002 && npm run verify:tf000003 && npm run verify:tf000004 && npm run verify:root && npm run verify:audit && npm run verify:hardening && npm run verify:network-seal && npm run verify:verification-index && npm run verify:index-seal && npm run verify:verification-atlas && npm run verify:atlas-seal && npm run verify:stack-closure && npm run verify:stack-closure-seal && npm run verify:continuity-sentinel && npm run verify:continuity-sentinel-seal && npm run verify:release-closure && npm run verify:release-closure-seal && npm run verify:root-finality && npm run verify:root-finality-seal && npm run verify:privacy-perimeter && npm run verify:legal-privacy-perimeter && npm run verify:legal-privacy-perimeter-seal && npm run verify:legal-privacy-network && npm run verify:distribution-egress && npm run verify:distribution-egress-firewall && npm run verify:distribution-egress-firewall-seal && npm run verify:distribution-egress-network && npm run verify:source-provenance && npm run verify:source-provenance-seal && npm run verify:source-provenance-network && npm run verify:verification-purity && npm run verify:verification-immutability && npm run verify:verification-immutability-seal && npm run verify:verification-immutability-network && npm run verify:truth-frame-spine && npm run verify:truth-frame-spine-network && npm run verify:truth-frame-admission && npm run verify:truth-frame-admission-network && npm run verify:truth-frame-admission-refusal && npm run verify:truth-frame-admission-refusal-network && npm run verify:truth-frame-admission-closure && npm run verify:truth-frame-admission-closure-network && npm run verify:public-verification-capsule && npm run verify:public-verification-capsule-network && npm run verify:public-capsule-consumption-receipt && npm run verify:public-capsule-consumption-receipt-network && npm run verify:public-independent-verification-witness", "verify:registry": "node scripts/verify-registry.js", "verify:readme": "node scripts/verify-readme-public-entry.js", "verify:tf000002": "node scripts/verify-tf-000002.js", @@ -72,7 +72,10 @@ "verify:public-verification-capsule-network": "node scripts/verify-public-verification-capsule-network.js", "generate:public-capsule-consumption-receipt": "node scripts/generate-public-capsule-consumption-receipt.js", "verify:public-capsule-consumption-receipt": "node scripts/verify-public-capsule-consumption-receipt.js", - "verify:public-capsule-consumption-receipt-network": "node scripts/verify-public-capsule-consumption-receipt-network.js" + "verify:public-capsule-consumption-receipt-network": "node scripts/verify-public-capsule-consumption-receipt-network.js", + "generate:public-independent-verification-witness": "node scripts/generate-public-independent-verification-witness.js", + "verify:public-independent-verification-witness": "node scripts/verify-public-independent-verification-witness.js", + "verify:public-independent-verification-witness-network": "node scripts/verify-public-independent-verification-witness-network.js" }, "license": "UNLICENSED" } diff --git a/reports/current/public-independent-verification-witness-v1.7.0.json b/reports/current/public-independent-verification-witness-v1.7.0.json new file mode 100644 index 0000000..35b3460 --- /dev/null +++ b/reports/current/public-independent-verification-witness-v1.7.0.json @@ -0,0 +1,51 @@ +{ + "protocol": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS", + "version": "v1.7.0", + "status": "PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_BOUND", + "closure_claim": "A third party can verify TRUTHFRAMER public truth-frame continuity from public URLs only, without private source, local repository state, maintainer terminal output, or hidden context.", + "public_console_path": "/verifier/", + "public_verifier_script": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs", + "public_checked_url_count": 2, + "verifier": { + "protocol": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER", + "version": "v1.7.0", + "status": "PUBLIC_INDEPENDENT_VERIFICATION_PASS", + "public_inputs": { + "public_verification_capsule_url": "https://truthframer.github.io/truthframer-platform/capsule/TRUTHFRAMER_PUBLIC_VERIFICATION_CAPSULE.json", + "public_capsule_consumption_receipt_url": "https://truthframer.github.io/truthframer-platform/receipt/TRUTHFRAMER_PUBLIC_CAPSULE_CONSUMPTION_RECEIPT.json" + }, + "public_object_digests": { + "capsule_raw_bytes_sha256": "8f1b6dc9cf953c91f1f300ee28d4b66378059d44c464989d00aa385f3c3163e4", + "capsule_canonical_json_sha256": "7d252db39d7f98ca1f239d3ecac2abaca0cfcad050c471e6efe1a1b7ec00f028", + "receipt_raw_bytes_sha256": "94ee2f308b5a3c3415534b9439b0ad394dd338d41e8d6eb8eebb9563c7b1df48", + "receipt_canonical_json_sha256": "ae2b0b1e7867d76ffb2df7ff97ebc6135e5b7b3eed5857b99a5d2161b75945f2" + }, + "observed_public_claims": { + "capsule_sha256": "957b92e87d6e6ca0f341702e367cb454347b66eba143e4046e3f0e4a8489fe17", + "receipt_sha256": "ae2b0b1e7867d76ffb2df7ff97ebc6135e5b7b3eed5857b99a5d2161b75945f2", + "receipt_sha256_basis": "computed_public_receipt_canonical_json_sha256", + "receipt_capsule_sha256": "957b92e87d6e6ca0f341702e367cb454347b66eba143e4046e3f0e4a8489fe17", + "capsule_fetched_from_public_url": true, + "capsule_sha256_match": true, + "capsule_sha256_match_basis": "expected_sha256_found_inside_public_capsule_json", + "capsule_artifacts_live": true, + "release_tag_bound": true, + "no_private_source_required": true + }, + "assertions": { + "capsule_public_fetch_ok": true, + "receipt_public_fetch_ok": true, + "capsule_declares_sha256": true, + "receipt_has_public_sha256_basis": true, + "receipt_capsule_sha256_matches_capsule": true, + "receipt_claims_capsule_public_fetch": true, + "receipt_claims_capsule_sha256_match": true, + "receipt_claims_capsule_artifacts_live": true, + "receipt_claims_release_tag_bound": true, + "receipt_claims_no_private_source_required": true, + "receipt_sha_basis_is_public_capsule_internal_digest": true + }, + "verifier_result_sha256": "3de5138348ac2a3227ec2f60c3c062afeddf209a837664b8881f7f020529b2f1" + }, + "witness_sha256": "132a366bc7aee1df8fddd0ebf87fc2d21d107afd915b51a489aaed13c12c2323" +} diff --git a/scripts/generate-public-independent-verification-witness.js b/scripts/generate-public-independent-verification-witness.js new file mode 100755 index 0000000..fdfdb8c --- /dev/null +++ b/scripts/generate-public-independent-verification-witness.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +const fs = require("fs"); +const path = require("path"); + +async function main() { + const modulePath = path.resolve("docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs"); + const { verifyTruthframerPublicIndependently, stableStringify, sha256Hex } = await import(modulePath); + + const verifier = await verifyTruthframerPublicIndependently(); + + if (verifier.status !== "PUBLIC_INDEPENDENT_VERIFICATION_PASS") { + throw new Error(`PUBLIC_INDEPENDENT_VERIFICATION_FAILED ${JSON.stringify(verifier.assertions)}`); + } + + const witness = { + protocol: "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS", + version: "v1.7.0", + status: "PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_BOUND", + closure_claim: + "A third party can verify TRUTHFRAMER public truth-frame continuity from public URLs only, without private source, local repository state, maintainer terminal output, or hidden context.", + public_console_path: "/verifier/", + public_verifier_script: "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs", + public_checked_url_count: 2, + verifier + }; + + witness.witness_sha256 = await sha256Hex(stableStringify(witness)); + + fs.mkdirSync("verifier", { recursive: true }); + fs.mkdirSync("docs/verifier", { recursive: true }); + fs.mkdirSync("reports/current", { recursive: true }); + + const body = JSON.stringify(witness, null, 2) + "\n"; + + fs.writeFileSync("verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json", body); + fs.writeFileSync("docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json", body); + fs.writeFileSync("reports/current/public-independent-verification-witness-v1.7.0.json", body); + + fs.copyFileSync( + "docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs", + "verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs" + ); + + console.log("TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_GENERATED=true"); + console.log(`VERSION=${witness.version}`); + console.log(`STATUS=${witness.status}`); + console.log(`VERIFIER_STATUS=${verifier.status}`); + console.log(`CAPSULE_SHA256=${verifier.observed_public_claims.capsule_sha256}`); + console.log(`RECEIPT_SHA256=${verifier.observed_public_claims.receipt_sha256}`); + console.log(`WITNESS_SHA256=${witness.witness_sha256}`); + console.log("NO_PRIVATE_SOURCE_REQUIRED=true"); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/verify-public-independent-verification-witness-network.js b/scripts/verify-public-independent-verification-witness-network.js new file mode 100755 index 0000000..1f9aa5b --- /dev/null +++ b/scripts/verify-public-independent-verification-witness-network.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +const path = require("path"); + +const WITNESS_URL = + "https://truthframer.github.io/truthframer-platform/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json"; +const VERIFIER_SCRIPT_URL = + "https://truthframer.github.io/truthframer-platform/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs"; + +async function fetchText(url) { + const response = await fetch(url, { cache: "no-store" }); + const text = await response.text(); + if (!response.ok) throw new Error(`PUBLIC_FETCH_FAILED url=${url} status=${response.status}`); + return text; +} + +async function main() { + const modulePath = path.resolve("docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs"); + const { verifyTruthframerPublicIndependently, stableStringify, sha256Hex } = await import(modulePath); + + const witnessText = await fetchText(WITNESS_URL); + const scriptText = await fetchText(VERIFIER_SCRIPT_URL); + + const witness = JSON.parse(witnessText); + const declared = witness.witness_sha256; + const copy = JSON.parse(JSON.stringify(witness)); + delete copy.witness_sha256; + const observed = await sha256Hex(stableStringify(copy)); + + if (declared !== observed) { + throw new Error(`PUBLIC_WITNESS_SHA256_MISMATCH declared=${declared} observed=${observed}`); + } + + const live = await verifyTruthframerPublicIndependently(); + + if (live.status !== "PUBLIC_INDEPENDENT_VERIFICATION_PASS") { + throw new Error(`LIVE_PUBLIC_INDEPENDENT_VERIFICATION_FAILED ${JSON.stringify(live.assertions)}`); + } + + if (witness.verifier.observed_public_claims.capsule_sha256 !== live.observed_public_claims.capsule_sha256) { + throw new Error("LIVE_CAPSULE_SHA256_MISMATCH"); + } + + if (witness.verifier.observed_public_claims.receipt_sha256 !== live.observed_public_claims.receipt_sha256) { + throw new Error("LIVE_RECEIPT_SHA256_MISMATCH"); + } + + console.log("PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_LIVE=true"); + console.log("TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_NETWORK_PASS=true"); + console.log(`PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_URL=${WITNESS_URL}`); + console.log(`PUBLIC_INDEPENDENT_VERIFIER_SCRIPT_URL=${VERIFIER_SCRIPT_URL}`); + console.log(`PUBLIC_INDEPENDENT_VERIFIER_SCRIPT_RAW_SHA256=${await sha256Hex(scriptText)}`); + console.log(`CAPSULE_SHA256=${live.observed_public_claims.capsule_sha256}`); + console.log(`RECEIPT_SHA256=${live.observed_public_claims.receipt_sha256}`); + console.log(`WITNESS_SHA256=${witness.witness_sha256}`); + console.log("NO_PRIVATE_SOURCE_REQUIRED=true"); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/verify-public-independent-verification-witness.js b/scripts/verify-public-independent-verification-witness.js new file mode 100755 index 0000000..3ccecc8 --- /dev/null +++ b/scripts/verify-public-independent-verification-witness.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node +const fs = require("fs"); +const path = require("path"); + +const files = [ + "verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json", + "docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json", + "reports/current/public-independent-verification-witness-v1.7.0.json" +]; + +async function main() { + const modulePath = path.resolve("docs/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs"); + const { stableStringify, sha256Hex } = await import(modulePath); + + const witnesses = files.map((file) => { + if (!fs.existsSync(file)) throw new Error(`WITNESS_MISSING ${file}`); + return JSON.parse(fs.readFileSync(file, "utf8")); + }); + + const reference = JSON.stringify(witnesses[0]); + + for (const witness of witnesses) { + if (JSON.stringify(witness) !== reference) throw new Error("WITNESS_MIRROR_MISMATCH"); + + const declared = witness.witness_sha256; + const copy = JSON.parse(JSON.stringify(witness)); + delete copy.witness_sha256; + const observed = await sha256Hex(stableStringify(copy)); + + if (declared !== observed) { + throw new Error(`WITNESS_SHA256_MISMATCH declared=${declared} observed=${observed}`); + } + + if (witness.version !== "v1.7.0") throw new Error(`VERSION_MISMATCH ${witness.version}`); + if (witness.status !== "PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_BOUND") { + throw new Error(`STATUS_MISMATCH ${witness.status}`); + } + if (!witness.verifier || witness.verifier.status !== "PUBLIC_INDEPENDENT_VERIFICATION_PASS") { + throw new Error("VERIFIER_STATUS_NOT_PASS"); + } + if (!witness.verifier.assertions || !Object.values(witness.verifier.assertions).every(Boolean)) { + throw new Error("VERIFIER_ASSERTION_NOT_ALL_TRUE"); + } + } + + const witness = witnesses[0]; + + console.log("TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_PASS=true"); + console.log(`VERSION=${witness.version}`); + console.log(`STATUS=${witness.status}`); + console.log(`VERIFIER_STATUS=${witness.verifier.status}`); + console.log(`PUBLIC_CHECKED_URL_COUNT=${witness.public_checked_url_count}`); + console.log(`CAPSULE_SHA256=${witness.verifier.observed_public_claims.capsule_sha256}`); + console.log(`RECEIPT_SHA256=${witness.verifier.observed_public_claims.receipt_sha256}`); + console.log(`WITNESS_SHA256=${witness.witness_sha256}`); + console.log("NO_PRIVATE_SOURCE_REQUIRED=true"); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json b/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json new file mode 100644 index 0000000..35b3460 --- /dev/null +++ b/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS.json @@ -0,0 +1,51 @@ +{ + "protocol": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFICATION_WITNESS", + "version": "v1.7.0", + "status": "PUBLIC_INDEPENDENT_VERIFICATION_WITNESS_BOUND", + "closure_claim": "A third party can verify TRUTHFRAMER public truth-frame continuity from public URLs only, without private source, local repository state, maintainer terminal output, or hidden context.", + "public_console_path": "/verifier/", + "public_verifier_script": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs", + "public_checked_url_count": 2, + "verifier": { + "protocol": "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER", + "version": "v1.7.0", + "status": "PUBLIC_INDEPENDENT_VERIFICATION_PASS", + "public_inputs": { + "public_verification_capsule_url": "https://truthframer.github.io/truthframer-platform/capsule/TRUTHFRAMER_PUBLIC_VERIFICATION_CAPSULE.json", + "public_capsule_consumption_receipt_url": "https://truthframer.github.io/truthframer-platform/receipt/TRUTHFRAMER_PUBLIC_CAPSULE_CONSUMPTION_RECEIPT.json" + }, + "public_object_digests": { + "capsule_raw_bytes_sha256": "8f1b6dc9cf953c91f1f300ee28d4b66378059d44c464989d00aa385f3c3163e4", + "capsule_canonical_json_sha256": "7d252db39d7f98ca1f239d3ecac2abaca0cfcad050c471e6efe1a1b7ec00f028", + "receipt_raw_bytes_sha256": "94ee2f308b5a3c3415534b9439b0ad394dd338d41e8d6eb8eebb9563c7b1df48", + "receipt_canonical_json_sha256": "ae2b0b1e7867d76ffb2df7ff97ebc6135e5b7b3eed5857b99a5d2161b75945f2" + }, + "observed_public_claims": { + "capsule_sha256": "957b92e87d6e6ca0f341702e367cb454347b66eba143e4046e3f0e4a8489fe17", + "receipt_sha256": "ae2b0b1e7867d76ffb2df7ff97ebc6135e5b7b3eed5857b99a5d2161b75945f2", + "receipt_sha256_basis": "computed_public_receipt_canonical_json_sha256", + "receipt_capsule_sha256": "957b92e87d6e6ca0f341702e367cb454347b66eba143e4046e3f0e4a8489fe17", + "capsule_fetched_from_public_url": true, + "capsule_sha256_match": true, + "capsule_sha256_match_basis": "expected_sha256_found_inside_public_capsule_json", + "capsule_artifacts_live": true, + "release_tag_bound": true, + "no_private_source_required": true + }, + "assertions": { + "capsule_public_fetch_ok": true, + "receipt_public_fetch_ok": true, + "capsule_declares_sha256": true, + "receipt_has_public_sha256_basis": true, + "receipt_capsule_sha256_matches_capsule": true, + "receipt_claims_capsule_public_fetch": true, + "receipt_claims_capsule_sha256_match": true, + "receipt_claims_capsule_artifacts_live": true, + "receipt_claims_release_tag_bound": true, + "receipt_claims_no_private_source_required": true, + "receipt_sha_basis_is_public_capsule_internal_digest": true + }, + "verifier_result_sha256": "3de5138348ac2a3227ec2f60c3c062afeddf209a837664b8881f7f020529b2f1" + }, + "witness_sha256": "132a366bc7aee1df8fddd0ebf87fc2d21d107afd915b51a489aaed13c12c2323" +} diff --git a/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs b/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs new file mode 100644 index 0000000..0960b04 --- /dev/null +++ b/verifier/TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER.mjs @@ -0,0 +1,227 @@ +const DEFAULTS = Object.freeze({ + version: "v1.7.0", + capsuleUrl: "https://truthframer.github.io/truthframer-platform/capsule/TRUTHFRAMER_PUBLIC_VERIFICATION_CAPSULE.json", + receiptUrl: "https://truthframer.github.io/truthframer-platform/receipt/TRUTHFRAMER_PUBLIC_CAPSULE_CONSUMPTION_RECEIPT.json" +}); + +export function stableStringify(value) { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"; + const keys = Object.keys(value).sort(); + return "{" + keys.map((key) => JSON.stringify(key) + ":" + stableStringify(value[key])).join(",") + "}"; +} + +export async function sha256Hex(input) { + const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input; + + if (globalThis.crypto && globalThis.crypto.subtle) { + const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join(""); + } + + const { createHash } = await import("node:crypto"); + return createHash("sha256").update(Buffer.from(bytes)).digest("hex"); +} + +function normalizeKey(key) { + return String(key).replace(/[^a-zA-Z0-9]/g, "").toUpperCase(); +} + +function isSha256(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/i.test(value); +} + +function asString(value) { + if (value === null || value === undefined) return ""; + return String(value); +} + +function asBool(value) { + if (value === true) return true; + if (value === false) return false; + if (typeof value === "string") return value.toLowerCase() === "true"; + return false; +} + +function walk(value, path = [], out = []) { + if (!value || typeof value !== "object") return out; + + if (Array.isArray(value)) { + value.forEach((item, index) => walk(item, path.concat(String(index)), out)); + return out; + } + + for (const [key, child] of Object.entries(value)) { + const itemPath = path.concat(key); + out.push({ + key, + normalizedKey: normalizeKey(key), + normalizedPath: itemPath.map(normalizeKey).join("."), + value: child + }); + walk(child, itemPath, out); + } + + return out; +} + +function firstBoolBySemanticKey(root, names) { + const wanted = new Set(names.map(normalizeKey)); + const hit = walk(root).find((entry) => wanted.has(entry.normalizedKey)); + return hit ? asBool(hit.value) : false; +} + +function firstStringBySemanticKey(root, names) { + const wanted = new Set(names.map(normalizeKey)); + const hit = walk(root).find((entry) => wanted.has(entry.normalizedKey)); + return hit ? asString(hit.value) : ""; +} + +function shaCandidates(root, predicate) { + return walk(root) + .filter((entry) => isSha256(entry.value)) + .filter(predicate) + .map((entry) => ({ + key: entry.key, + path: entry.normalizedPath, + value: String(entry.value).toLowerCase() + })); +} + +function preferredSha(root, predicate) { + const hits = shaCandidates(root, predicate); + if (!hits.length) return ""; + const exact = hits.find((hit) => hit.path.split(".").length <= 3); + return (exact || hits[0]).value; +} + +function capsuleShaFromCapsule(capsuleJson) { + return preferredSha(capsuleJson, (entry) => { + const k = entry.normalizedKey; + const p = entry.normalizedPath; + return ( + k.endsWith("CAPSULESHA256") && + !p.includes("RAWBYTES") && + !p.includes("CANONICALJSON") && + !p.includes("WITHOUTSHAFIELDS") && + !p.includes("RECEIPT") + ); + }); +} + +function capsuleShaFromReceipt(receiptJson) { + return preferredSha(receiptJson, (entry) => { + const k = entry.normalizedKey; + const p = entry.normalizedPath; + return ( + k.endsWith("CAPSULESHA256") && + !p.includes("RAWBYTES") && + !p.includes("CANONICALJSON") && + !p.includes("WITHOUTSHAFIELDS") && + !p.includes("RECEIPT") + ); + }); +} + +function receiptShaFromReceipt(receiptJson) { + return preferredSha(receiptJson, (entry) => { + const k = entry.normalizedKey; + const p = entry.normalizedPath; + return ( + k.endsWith("RECEIPTSHA256") || + k.endsWith("CONSUMPTIONRECEIPTSHA256") || + p.endsWith("RECEIPTSHA256") || + p.endsWith("CONSUMPTIONRECEIPTSHA256") + ); + }); +} + +async function fetchPublicJson(url) { + const response = await fetch(url, { cache: "no-store" }); + const text = await response.text(); + + if (!response.ok) { + throw new Error(`PUBLIC_FETCH_FAILED url=${url} status=${response.status}`); + } + + return { + url, + status: response.status, + text, + json: JSON.parse(text), + raw_bytes_sha256: await sha256Hex(text), + canonical_json_sha256: await sha256Hex(stableStringify(JSON.parse(text))) + }; +} + +export async function verifyTruthframerPublicIndependently(config = {}) { + const capsuleUrl = config.capsuleUrl || DEFAULTS.capsuleUrl; + const receiptUrl = config.receiptUrl || DEFAULTS.receiptUrl; + + const capsule = await fetchPublicJson(capsuleUrl); + const receipt = await fetchPublicJson(receiptUrl); + + const capsuleDeclaredSha = capsuleShaFromCapsule(capsule.json); + const receiptCapsuleSha = capsuleShaFromReceipt(receipt.json); + const receiptDeclaredSha = receiptShaFromReceipt(receipt.json); + const receiptObservedSha = receiptDeclaredSha || receipt.canonical_json_sha256; + + const claims = { + capsule_sha256: capsuleDeclaredSha, + receipt_sha256: receiptObservedSha, + receipt_sha256_basis: receiptDeclaredSha ? "declared_inside_public_receipt_json" : "computed_public_receipt_canonical_json_sha256", + receipt_capsule_sha256: receiptCapsuleSha, + capsule_fetched_from_public_url: firstBoolBySemanticKey(receipt.json, ["CAPSULE_FETCHED_FROM_PUBLIC_URL"]), + capsule_sha256_match: firstBoolBySemanticKey(receipt.json, ["CAPSULE_SHA256_MATCH"]), + capsule_sha256_match_basis: firstStringBySemanticKey(receipt.json, ["CAPSULE_SHA256_MATCH_BASIS"]), + capsule_artifacts_live: firstBoolBySemanticKey(receipt.json, ["CAPSULE_ARTIFACTS_LIVE"]), + release_tag_bound: firstBoolBySemanticKey(receipt.json, ["RELEASE_TAG_BOUND"]), + no_private_source_required: firstBoolBySemanticKey(receipt.json, ["NO_PRIVATE_SOURCE_REQUIRED"]) + }; + + const assertions = { + capsule_public_fetch_ok: capsule.status === 200, + receipt_public_fetch_ok: receipt.status === 200, + capsule_declares_sha256: isSha256(claims.capsule_sha256), + receipt_has_public_sha256_basis: isSha256(claims.receipt_sha256), + receipt_capsule_sha256_matches_capsule: claims.receipt_capsule_sha256 === claims.capsule_sha256, + receipt_claims_capsule_public_fetch: claims.capsule_fetched_from_public_url === true, + receipt_claims_capsule_sha256_match: claims.capsule_sha256_match === true, + receipt_claims_capsule_artifacts_live: claims.capsule_artifacts_live === true, + receipt_claims_release_tag_bound: claims.release_tag_bound === true, + receipt_claims_no_private_source_required: claims.no_private_source_required === true, + receipt_sha_basis_is_public_capsule_internal_digest: + claims.capsule_sha256_match_basis === "expected_sha256_found_inside_public_capsule_json" + }; + + const pass = Object.values(assertions).every(Boolean); + + const result = { + protocol: "TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER", + version: DEFAULTS.version, + status: pass ? "PUBLIC_INDEPENDENT_VERIFICATION_PASS" : "PUBLIC_INDEPENDENT_VERIFICATION_FAIL", + public_inputs: { + public_verification_capsule_url: capsuleUrl, + public_capsule_consumption_receipt_url: receiptUrl + }, + public_object_digests: { + capsule_raw_bytes_sha256: capsule.raw_bytes_sha256, + capsule_canonical_json_sha256: capsule.canonical_json_sha256, + receipt_raw_bytes_sha256: receipt.raw_bytes_sha256, + receipt_canonical_json_sha256: receipt.canonical_json_sha256 + }, + observed_public_claims: claims, + assertions + }; + + result.verifier_result_sha256 = await sha256Hex(stableStringify(result)); + return result; +} + +if (typeof window !== "undefined") { + window.TRUTHFRAMER_PUBLIC_INDEPENDENT_VERIFIER = { + verifyTruthframerPublicIndependently, + stableStringify, + sha256Hex + }; +} diff --git a/verifier/index.html b/verifier/index.html new file mode 100644 index 0000000..419af68 --- /dev/null +++ b/verifier/index.html @@ -0,0 +1,46 @@ + + + + + TRUTHFRAMER Public Independent Verification Console + + + + +
+

TRUTHFRAMER Public Independent Verification Console

+

+ This page verifies the public capsule and public consumption receipt from public URLs only. + No private source, local repository state, maintainer terminal output, or hidden context is required. +

+

VERIFYING...

+

+
+ + +