Every audit export from /hub/audit-log ships as a gzipped JSONL file with a manifest on the first line, one JSON row per line after that, and a footer on the last line carrying an Ed25519 signature over a SHA-256 hash of the file body (manifest + all rows, footer excluded).
The signing keypair is owned by the hub β public half below, private half sealed in the hubβs secrets vault. If a downloaded archive verifies against this public key, it came from this hub and was not tampered with after download.
Current signing key
HUB-ANCIENT-HOLDINGSed25519-sha256:fe5131a79593a78e2026-04-24T09:08:28.788ZH.1.19Public key (base64, 32 bytes)
PEpla1foDn0/8ca155eAqdMhW0Di5FUyiZBH+aRPpc8=
Verifying an export
Save the snippet below to verify-audit.mjs and run it with Node 18+ against a downloaded audit-*.jsonl.gz:
// verify-audit.mjs β Node 18+. Usage: node verify-audit.mjs audit-*.jsonl.gz
import fs from 'node:fs';
import zlib from 'node:zlib';
import crypto from 'node:crypto';
const filename = process.argv[2];
if (!filename) { console.error('usage: node verify-audit.mjs <file.jsonl.gz>'); process.exit(2); }
// Paste the current hub public key here (from the box above).
const PUBLIC_KEY_B64 = 'PASTE_PUBLIC_KEY_B64_FROM_DOCS_PAGE';
const gz = fs.readFileSync(filename);
const plain = zlib.gunzipSync(gz).toString('utf8');
const lines = plain.split('\n').filter(Boolean);
if (lines.length < 3) { console.error('archive too short'); process.exit(1); }
const manifest = JSON.parse(lines[0]);
const footer = JSON.parse(lines[lines.length - 1]);
const body = lines.slice(0, -1).join('\n') + '\n';
const hash = crypto.createHash('sha256').update(body).digest();
if (hash.toString('hex') !== footer.bodySha256) {
console.error('bodySha256 mismatch: expected ' + footer.bodySha256 + ', got ' + hash.toString('hex'));
process.exit(1);
}
// Ed25519 verify. Node 18+ supports `verify` with type 'ed25519'.
const pubKey = crypto.createPublicKey({
key: Buffer.concat([
Buffer.from('302a300506032b6570032100', 'hex'), // Ed25519 SPKI prefix
Buffer.from(PUBLIC_KEY_B64, 'base64'),
]),
format: 'der',
type: 'spki',
});
const sigBytes = Buffer.from(footer.signature, 'base64');
const ok = crypto.verify(null, hash, pubKey, sigBytes);
if (!ok) { console.error('signature invalid'); process.exit(1); }
console.log('verified β ' + manifest.exportedBy + ' on ' + manifest.exportedAt);
console.log(' rows: ' + footer.rowCount);
console.log(' fingerprint: ' + footer.hubSigningKeyFingerprint);
The script reads the gzip, pulls the first line as the manifest and the last line as the footer, recomputes the body hash over everything in between (inclusive of the manifest), compares it against footer.bodySha256, and then verifies the detached Ed25519 signature against the public key above.
What breaks verification
- Any byte mutation in the manifest or any row line β hash mismatch.
- Removing, duplicating, or reordering a row β hash mismatch.
- Replacing the signature with a forgery β signature verification fails.
- Substituting a different public key β signature verification fails (the one printed above is the only signer this hub trusts).
A single-byte flip experiment: change any character inside a row line in the archive, re-gzip, re-run the verifier β itβll report bodySha256 mismatch: expected β¦, got β¦ and exit non-zero.
Key rotation
The signing key can be rotated (Phase E settings page lands that UI; until then, clearing the audit_signing_* entries in system_state + next export regenerates). When a new key comes online this page lists the new fingerprint and issued-at; past archives continue to verify against the key that was current when they were exported β every archive embeds its signerβs fingerprint in the manifest + footer.
There is no third-party CA in this chain β trust roots here are simply the owner of ancientholdings.eu publishing this page. If you want external attestation, pin the fingerprint via archive.org shortly after a rotation and save the archive link.