From ae3e4316fb447be3c1d67555a677d84d87d6499e Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 28 May 2026 17:50:12 -0400 Subject: [PATCH] web: stamp each zebra-report page with build date + md5/sha256 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an integrity footer to chat.html, zebra-audio.html, and how-it-works.html showing the build date (2026-05-28) and the page's own MD5 + SHA-256. A file can't hold its own hash, so web/stamp.js (make stamp) computes the hashes with the two hash fields zeroed, then writes the real values back — self-consistent and idempotent. To verify a served page: blank the two fields and re-hash; confirmed it reproduces the stamped value with plain sha256sum. --- Makefile | 6 +++++- web/chat.html | 6 ++++++ web/how-it-works.html | 6 ++++++ web/stamp.js | 35 +++++++++++++++++++++++++++++++++++ web/zebra-audio.html | 6 ++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 web/stamp.js diff --git a/Makefile b/Makefile index 0a84183..70ff986 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,11 @@ LDFLAGS = $(shell pkg-config --libs libpulse) -lrt -lpthread PULSE = src/pulse.c -.PHONY: all clean serve blog test test-all test-web zebrad +.PHONY: all clean serve blog test test-all test-web stamp zebrad + +# stamp each web page with today's date + its own md5/sha256 (run before deploy) +stamp: + @node web/stamp.js web/chat.html web/zebra-audio.html web/how-it-works.html all: tx rx chat bt carrier zebrad diff --git a/web/chat.html b/web/chat.html index 8cc7d5e..840352a 100644 --- a/web/chat.html +++ b/web/chat.html @@ -1590,5 +1590,11 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry })(); + diff --git a/web/how-it-works.html b/web/how-it-works.html index 3be4828..6c4de39 100644 --- a/web/how-it-works.html +++ b/web/how-it-works.html @@ -225,5 +225,11 @@ unturf

+ diff --git a/web/stamp.js b/web/stamp.js new file mode 100644 index 0000000..e031aa1 --- /dev/null +++ b/web/stamp.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/* Stamp each given HTML page with today's date + its own MD5 and SHA-256. + * + * A file can't contain its own hash directly (writing the hash changes the + * hash). So the hashes are computed over the page with the two hash fields + * ZEROED, then the real values are written back (same length, so the file the + * verifier hashes-after-zeroing is exactly what we hashed). Self-consistent and + * idempotent. + * + * Verify a served page: sed the md5 field to 32 zeros and the sha256 field to + * 64 zeros, then `md5sum` / `sha256sum` — must match the printed values. + * + * node web/stamp.js web/chat.html web/zebra-audio.html web/how-it-works.html + */ +const { readFileSync, writeFileSync } = require('fs'); +const { createHash } = require('crypto'); + +const date = process.env.STAMP_DATE || new Date().toISOString().slice(0, 10); +const Z32 = '0'.repeat(32), Z64 = '0'.repeat(64); +let bad = 0; + +for (const f of process.argv.slice(2)) { + let s = readFileSync(f, 'utf8'); + if (!/class="stamp-md5"/.test(s)) { console.error(`! ${f}: no integrity footer, skipped`); bad++; continue; } + s = s.replace(/()[^<]*(<\/span>)/, `$1${date}$2`); + s = s.replace(/()[0-9a-fA-F]*(<\/span>)/, `$1${Z32}$2`) + .replace(/()[0-9a-fA-F]*(<\/span>)/, `$1${Z64}$2`); + const md5 = createHash('md5').update(s).digest('hex'); + const sha = createHash('sha256').update(s).digest('hex'); + s = s.replace(`${Z32}`, `${md5}`) + .replace(`${Z64}`, `${sha}`); + writeFileSync(f, s); + console.log(`${f}\n date ${date}\n md5 ${md5}\n sha256 ${sha}`); +} +process.exit(bad ? 1 : 0); diff --git a/web/zebra-audio.html b/web/zebra-audio.html index d1c0a33..d529513 100644 --- a/web/zebra-audio.html +++ b/web/zebra-audio.html @@ -395,5 +395,11 @@ $('music-mode').addEventListener('change', async (e)=>{ logLine('', 'ready — type a rendezvous code and call. mic stays muted to the room until connected.'); })(); +