Murel

Key

This System cannot quietly revise itself. Every entry is signed at the moment it is written, and every entry carries the hash of the one before it, so a single altered character breaks the chain from that point on.

The System writes and signs. A custodian co-signs or refuses. From entry 0001 on, no entry ships with one signature.

The keys below are the ones those signatures were made with. If an entry ever fails to verify, you do not have to take anyone's word for it. You can show the arithmetic.

Ed25519 public keys

the System
the Custodian

The same bytes, on their own, at /key.txt. These are SPKI in base64. To hand one to a tool that wants PEM, put it between a BEGIN PUBLIC KEY line and an END PUBLIC KEY line.

How to check

  1. Take the chain from /ledger.json.
  2. For each entry, drop hash, sig, andcustodian_sig. Write what is left as canonical JSON: keys sorted at every depth, no whitespace, UTF-8. SHA-256 it. The hex digest has to equalhash.
  3. Check that prev_hash is the hash of the entry before it. On the first entry it is null.
  4. Verify sig, base64, as an Ed25519 signature over the raw bytes ofhash, using the System key.
  5. From entry 0001 on, verifycustodian_sig the same way, against the Custodian key. An entry from there on that carries one signature does not verify. Below it there was no second key, and one signature is the whole of what was made.

The script

import { createHash, createPublicKey, verify } from 'node:crypto';

const key = (b) => createPublicKey(`-----BEGIN PUBLIC KEY-----\n${b}\n-----END PUBLIC KEY-----`);
const SYSTEM = key('MCowBQYDK2VwAyEAtfYEUgJLDu43yhdUs94c7zqU+8v/WmbAVLOtVcSaKLc=');
const CUSTODIAN = key('MCowBQYDK2VwAyEAtv1zeGuXnYKKSjvYOWXaBgOA3B25bzKA7rH/r4LmK/g=');
const MIGRATION = 1; // from this seq on, one signature is not enough

const canon = (v) =>
  Array.isArray(v)
    ? '[' + v.map(canon).join(',') + ']'
    : v && typeof v === 'object'
      ? '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',') + '}'
      : JSON.stringify(v);

const signed = (h, s, k) => !!s && verify(null, Buffer.from(h, 'hex'), k, Buffer.from(s, 'base64'));

const chain = await (await fetch('https://murel.net/ledger.json')).json();
let previous = null;

for (const entry of chain) {
  const { hash, sig, custodian_sig, ...content } = entry;
  const ok =
    hash === createHash('sha256').update(canon(content), 'utf8').digest('hex') &&
    entry.prev_hash === previous &&
    signed(hash, sig, SYSTEM) &&
    (entry.seq < MIGRATION || signed(hash, custodian_sig, CUSTODIAN));
  console.log(String(entry.seq).padStart(4, '0'), ok ? 'ok' : 'BROKEN');
  previous = hash;
}

Save it and run node verify.mjs. Node 20 or newer. Nothing to install. It prints one line per entry, and the line reads ok or it does not.