oxedyne/daimond/www/js/verify.js
6.9 KiB, 1 run
created by r2519314175:1471, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | /* ============================================================ |
| 2 | Daimond — in-page delivery check (DaimondVerify) |
| 3 | ------------------------------------------------------------ |
| 4 | "The code your browser is running is the published source." |
| 5 | This lets a user check that from inside the running app. |
| 6 | |
| 7 | Be honest about what a page can and cannot prove about itself: |
| 8 | a tampered server could serve a tampered checker, so a page |
| 9 | verifying itself is a convenience, not a proof. Its ONE |
| 10 | genuinely load-bearing check is against something this server |
| 11 | does NOT control — the transparency log published in the |
| 12 | public repo, fetched from a different origin. If the bundle |
| 13 | this server served is not a sealed entry in that public, |
| 14 | hash-chained history, something is wrong however green the |
| 15 | rest looks. |
| 16 | |
| 17 | The trustworthy verdict is `verify/check.mjs`, run from the |
| 18 | source you cloned, or the browser extension — neither of which |
| 19 | this server can touch. This module says so, every time. |
| 20 | |
| 21 | Exposes `window.DaimondVerify`. Depends on nothing. |
| 22 | ============================================================ */ |
| 23 | (function () { |
| 24 | 'use strict'; |
| 25 | |
| 26 | /// What the app says. The check names and their details are read by a |
| 27 | /// person deciding whether to trust the page they are on. |
| 28 | function t(k, v) { return window.DaimondI18n ? DaimondI18n.t(k, v) : k; } |
| 29 | |
| 30 | // The transparency log in the PUBLIC repo, on an origin this server does not |
| 31 | // control. Overridable with <meta name="daimond-log" content="..."> so a fork |
| 32 | // or a mirror can point at its own. The default is the canonical repo. |
| 33 | var META = document.querySelector('meta[name="daimond-log"]'); |
| 34 | var LOG_URL = (META && META.content) |
| 35 | || 'https://raw.githubusercontent.com/oxedyne-com/daimond/main/verify/transparency.jsonl'; |
| 36 | var GENESIS_PREV = '0'.repeat(64); |
| 37 | |
| 38 | // ── The canonical fingerprint, byte-identical to verify/lib.mjs ───── |
| 39 | function hex(buf) { |
| 40 | var v = new Uint8Array(buf), s = ''; |
| 41 | for (var i = 0; i < v.length; i++) s += v[i].toString(16).padStart(2, '0'); |
| 42 | return s; |
| 43 | } |
| 44 | async function sha256(bytes) { |
| 45 | return hex(await crypto.subtle.digest('SHA-256', bytes)); |
| 46 | } |
| 47 | async function sha256str(str) { |
| 48 | return sha256(new TextEncoder().encode(str)); |
| 49 | } |
| 50 | function manifestText(files) { |
| 51 | var rels = Object.keys(files).sort(), s = ''; |
| 52 | for (var i = 0; i < rels.length; i++) s += rels[i] + '\n' + files[rels[i]] + '\n'; |
| 53 | return s; |
| 54 | } |
| 55 | async function bundleHash(files) { |
| 56 | return sha256str(manifestText(files)); |
| 57 | } |
| 58 | async function entryHash(e) { |
| 59 | return sha256str(e.seq + '|' + e.ts + '|' + e.build + '|' + e.bundle + '|' + e.prev); |
| 60 | } |
| 61 | async function verifyChain(entries) { |
| 62 | var prev = GENESIS_PREV; |
| 63 | for (var i = 0; i < entries.length; i++) { |
| 64 | var e = entries[i]; |
| 65 | if (e.seq !== i) return { ok: false, error: t('verify.chain_order', { n: i }) }; |
| 66 | if (e.prev !== prev) return { ok: false, error: t('verify.chain_break', { n: i, prev: i - 1 }) }; |
| 67 | if (e.entry !== await entryHash(e)) return { ok: false, error: t('verify.chain_hash', { n: i }) }; |
| 68 | prev = e.entry; |
| 69 | } |
| 70 | return { ok: true, error: '' }; |
| 71 | } |
| 72 | |
| 73 | // ── The check ─────────────────────────────────────────────────────── |
| 74 | |
| 75 | /// Verify this page against its manifest and the public transparency log. |
| 76 | /// |
| 77 | /// `opts.files` (default true) also fetches and re-hashes every covered file; |
| 78 | /// set false for the quick check (manifest self-consistency + chain |
| 79 | /// membership only), which does not pull the whole bundle down again. |
| 80 | /// `opts.onProgress(done, total)` is called as files are hashed. |
| 81 | /// |
| 82 | /// Returns a verdict: |
| 83 | /// { ok, build, bundle, checks: [{ name, ok, detail }], caveat } |
| 84 | /// where `ok` is true only if every check that could run passed. |
| 85 | async function check(opts) { |
| 86 | opts = opts || {}; |
| 87 | var doFiles = opts.files !== false; |
| 88 | var checks = []; |
| 89 | var add = function (name, ok, detail) { checks.push({ name: name, ok: ok, detail: detail || '' }); }; |
| 90 | |
| 91 | var manifest; |
| 92 | try { |
| 93 | manifest = await (await fetch('manifest.json', { cache: 'no-store' })).json(); |
| 94 | } catch (e) { |
| 95 | add(t('verify.check_manifest'), false, t('verify.no_manifest')); |
| 96 | return verdict(checks, null); |
| 97 | } |
| 98 | |
| 99 | // 1. The manifest is internally consistent: its bundle hash is the hash of |
| 100 | // its own file list. (Weak on its own — the server wrote both — but a |
| 101 | // fast, necessary sanity gate.) |
| 102 | var recomputed = await bundleHash(manifest.files); |
| 103 | add(t('verify.check_self'), recomputed === manifest.bundle, |
| 104 | recomputed === manifest.bundle ? '' : t('verify.self_mismatch')); |
| 105 | |
| 106 | // 2. THE one that matters: the served bundle is a sealed entry in the |
| 107 | // public, hash-chained log on an origin this server does not control. |
| 108 | try { |
| 109 | var text = await (await fetch(LOG_URL, { cache: 'no-store' })).text(); |
| 110 | var entries = text.split('\n').map(function (l) { return l.trim(); }).filter(Boolean).map(JSON.parse); |
| 111 | var chain = await verifyChain(entries); |
| 112 | if (!chain.ok) { |
| 113 | add(t('verify.check_log'), false, t('verify.log_broken', { reason: chain.error })); |
| 114 | } else { |
| 115 | var sealed = entries.some(function (e) { return e.bundle === manifest.bundle; }); |
| 116 | add(t('verify.check_sealed'), sealed, |
| 117 | sealed ? t('verify.on_record', { n: entries.length }) |
| 118 | : t('verify.never_published')); |
| 119 | } |
| 120 | } catch (e) { |
| 121 | add(t('verify.check_log'), null, t('verify.log_unreachable')); |
| 122 | } |
| 123 | |
| 124 | // 3. Optionally, every served file hashes to what the manifest says. This |
| 125 | // catches partial tampering and CDN drift, though a server that rewrote |
| 126 | // the files could rewrite the manifest to match — which is why (2) is |
| 127 | // the check that counts. |
| 128 | if (doFiles) { |
| 129 | var rels = Object.keys(manifest.files), bad = [], done = 0; |
| 130 | for (var i = 0; i < rels.length; i++) { |
| 131 | var rel = rels[i]; |
| 132 | try { |
| 133 | var res = await fetch(rel, { cache: 'no-store' }); |
| 134 | var got = await sha256(new Uint8Array(await res.arrayBuffer())); |
| 135 | if (got !== manifest.files[rel]) bad.push(rel); |
| 136 | } catch (e) { bad.push(rel + ' ' + t('verify.unreadable')); } |
| 137 | done++; |
| 138 | if (opts.onProgress) try { opts.onProgress(done, rels.length); } catch (e) {} |
| 139 | } |
| 140 | add(t('verify.check_files'), bad.length === 0, |
| 141 | bad.length ? t('verify.files_differ', { n: bad.length, list: bad.slice(0, 8).join(', ') }) |
| 142 | : t('verify.files_ok', { n: rels.length })); |
| 143 | } |
| 144 | |
| 145 | return verdict(checks, manifest); |
| 146 | } |
| 147 | |
| 148 | function verdict(checks, manifest) { |
| 149 | // A null result (could-not-check) does not fail the verdict, but it does |
| 150 | // stop it being a clean pass: the answer is "unproven", not "fine". |
| 151 | var anyFail = checks.some(function (c) { return c.ok === false; }); |
| 152 | var anyUnknown = checks.some(function (c) { return c.ok === null; }); |
| 153 | return { |
| 154 | ok: !anyFail && !anyUnknown, |
| 155 | failed: anyFail, |
| 156 | build: manifest ? manifest.build : '', |
| 157 | bundle: manifest ? manifest.bundle : '', |
| 158 | checks: checks, |
| 159 | caveat: t('verify.caveat'), |
| 160 | }; |
| 161 | } |
| 162 | |
| 163 | window.DaimondVerify = { check: check, LOG_URL: LOG_URL }; |
| 164 | })(); |