Below is a toy key-transparency log with eight entries (users). The log is organized as a binary Merkle tree: each leaf holds SHA-256(user || publicKey), and each internal node holds the hash of its two children. The root hash is the single fingerprint that commits the entire log.
<p class="hint">A key-transparency log stores user→key bindings as leaves of a Merkle tree. Pick a user to look up their key. The server returns the key plus a short <b>inclusion proof</b> (sibling hashes). You recompute the root — if it matches the trusted root, the key is genuine.</p>
<div class="lookup-row">
<label for="user-select">Look up user:</label>
<select id="user-select"></select>
<button id="btn-lookup" type="button">Look up</button>
</div>
<div id="result-box" class="result-box hidden">
<div class="result-line"><span class="label">User</span><span id="r-user" class="val"></span></div>
<div class="result-line"><span class="label">Public key</span><span id="r-key" class="val mono"></span></div>
<div class="result-line"><span class="label">Leaf hash</span><span id="r-leaf" class="val mono hash"></span></div>
<div class="proof-section">
<div class="proof-title">Inclusion proof (sibling hashes, bottom to top):</div>
<ol id="proof-list" class="proof-list"></ol>
</div>
<div class="result-line recomputed"><span class="label">Recomputed root</span><span id="r-comp" class="val mono hash"></span></div>
<div class="result-line trusted"><span class="label">Trusted root</span><span id="r-root" class="val mono hash"></span></div>
<div id="verdict" class="verdict"></div>
</div>
<div id="tree-vis" class="tree-vis"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.lookup-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-weight: 600; }
select { padding: .35rem .5rem; border: 1px solid #aaa; border-radius: 6px; font-size: .9rem; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
.result-box { border: 1px solid #cdd9e3; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .8rem;
background: #f5f8fb; }
.result-box.hidden { display: none; }
.result-line { display: flex; gap: .6rem; align-items: flex-start; margin-bottom: .35rem; font-size: .82rem; }
.label { font-weight: 700; min-width: 110px; color: #1d3557; }
.val { word-break: break-all; }
.mono { font-family: ui-monospace, monospace; font-size: .78rem; }
.hash { color: #5a2d82; }
.proof-section { margin: .5rem 0; }
.proof-title { font-weight: 700; font-size: .82rem; color: #1d3557; margin-bottom: .3rem; }
.proof-list { margin: 0 0 .3rem 1.4rem; padding: 0; }
.proof-list li { font-family: ui-monospace, monospace; font-size: .78rem; color: #5a2d82;
margin-bottom: .15rem; }
.proof-list li span.side { color: #888; font-family: system-ui; font-size: .75rem; margin-right: .3rem; }
.recomputed .val { color: #0a7d33; }
.trusted .val { color: #1d3557; }
.verdict { font-weight: 700; font-size: .95rem; margin-top: .4rem; padding: .35rem .6rem;
border-radius: 6px; }
.verdict.ok { background: #d4edda; color: #0a7d33; }
.verdict.fail { background: #fce8e8; color: #c92f3c; }
.tree-vis { font-family: ui-monospace, monospace; font-size: .72rem; overflow-x: auto; }
.tree-vis table { border-collapse: collapse; }
.tree-vis td { padding: 2px 4px; text-align: center; white-space: nowrap; }
.node { display: inline-block; padding: 2px 5px; border-radius: 4px; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; }
.node.leaf { background: #f0e8fb; border-color: #b89fcc; color: #5a2d82; }
.node.highlight { background: #fff3cd; border-color: #e6a817; color: #7a4800; font-weight: 700; }
.node.proof-node { background: #d4edda; border-color: #5aaa6f; color: #0a5c22; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Key-transparency Merkle demo.
// We use a deterministic "toy SHA-256" (djb2-based 32-bit hash displayed as hex)
// so it runs instantly in the sandbox without crypto APIs.
// The proof logic and tree structure are identical to a real SHA-256 Merkle tree.
function toyHash(str) {
// djb2 extended to 64-bit using two independent 32-bit hashes for display length
let h1 = 5381, h2 = 52711;
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
h1 = ((h1 << 5) + h1) ^ c;
h2 = ((h2 << 5) + h2) ^ (c * 1000003);
}
const toHex = n => (n >>> 0).toString(16).padStart(8, '0');
return toHex(h1) + toHex(h2 ^ (h1 * 2654435761));
}
// 8 fixed users and their toy public keys
const USERS = [
{ name: 'alice', key: 'pk_a1b2c3d4e5f6' },
{ name: 'bob', key: 'pk_b2c3d4e5f6a1' },
{ name: 'carol', key: 'pk_c3d4e5f6a1b2' },
{ name: 'dave', key: 'pk_d4e5f6a1b2c3' },
{ name: 'eve', key: 'pk_e5f6a1b2c3d4' },
{ name: 'frank', key: 'pk_f6a1b2c3d4e5' },
{ name: 'grace', key: 'pk_g7a8b9c0d1e2' },
{ name: 'hank', key: 'pk_h8b9c0d1e2a3' },
];
// Build leaf hashes
const leaves = USERS.map(u => toyHash(u.name + '|' + u.key));
// Build the full Merkle tree bottom-up.
// tree[0] = leaves (level 0), tree[1] = level 1, ..., tree[3] = root
function buildTree(lvs) {
const levels = [lvs.slice()];
let cur = lvs.slice();
while (cur.length > 1) {
const next = [];
for (let i = 0; i < cur.length; i += 2) next.push(toyHash(cur[i] + cur[i + 1]));
levels.push(next);
cur = next;
}
return levels;
}
const tree = buildTree(leaves);
const ROOT = tree[tree.length - 1][0];
// Generate inclusion proof for leaf at index idx
function inclusionProof(idx) {
const proof = [];
let i = idx;
for (let lvl = 0; lvl < tree.length - 1; lvl++) {
const sibling = (i % 2 === 0) ? i + 1 : i - 1;
const side = (i % 2 === 0) ? 'right' : 'left';
proof.push({ hash: tree[lvl][sibling], side });
i = Math.floor(i / 2);
}
return proof;
}
// Recompute root from leaf and proof
function verifyProof(leafHash, proof) {
let cur = leafHash;
for (const { hash, side } of proof) {
cur = (side === 'right') ? toyHash(cur + hash) : toyHash(hash + cur);
}
return cur;
}
// Populate select
const sel = document.getElementById('user-select');
USERS.forEach((u, i) => {
const opt = document.createElement('option');
opt.value = i; opt.textContent = u.name;
sel.appendChild(opt);
});
let highlightIdx = -1;
document.getElementById('btn-lookup').addEventListener('click', () => {
const idx = parseInt(sel.value, 10);
const user = USERS[idx];
const leafHash = leaves[idx];
const proof = inclusionProof(idx);
const computed = verifyProof(leafHash, proof);
const ok = computed === ROOT;
document.getElementById('r-user').textContent = user.name;
document.getElementById('r-key').textContent = user.key;
document.getElementById('r-leaf').textContent = leafHash;
const list = document.getElementById('proof-list');
list.innerHTML = '';
proof.forEach(({ hash, side }, i) => {
const li = document.createElement('li');
li.innerHTML = '<span class="side">[' + side + ']</span>' + hash;
list.appendChild(li);
});
document.getElementById('r-comp').textContent = computed;
document.getElementById('r-root').textContent = ROOT;
const verdict = document.getElementById('verdict');
if (ok) {
verdict.textContent = '✓ Root matches — key is authentic and in the log.';
verdict.className = 'verdict ok';
} else {
verdict.textContent = '✗ Root mismatch — key may have been tampered with.';
verdict.className = 'verdict fail';
}
document.getElementById('result-box').classList.remove('hidden');
highlightIdx = idx;
renderTree(idx, proof);
// Log for node verification
console.log(fmt('VERIFY idx=%idx% user=%name% leaf=%leafHash% computed=%computed% root=%ROOT% match=%ok%', { idx: idx, name: user.name, leafHash: leafHash, computed: computed, ROOT: ROOT, ok: ok }));
});
function renderTree(leafIdx, proof) {
// Build a set of proof-node indices per level for coloring
const proofNodes = new Map(); // level -> Set of indices
let i = leafIdx;
for (let lvl = 0; lvl < tree.length - 1; lvl++) {
const sib = (i % 2 === 0) ? i + 1 : i - 1;
if (!proofNodes.has(lvl)) proofNodes.set(lvl, new Set());
proofNodes.get(lvl).add(sib);
i = Math.floor(i / 2);
}
const vis = document.getElementById('tree-vis');
vis.innerHTML = '';
const tbl = document.createElement('table');
// Draw from root (top) to leaves (bottom)
for (let lvl = tree.length - 1; lvl >= 0; lvl--) {
const tr = document.createElement('tr');
const nodes = tree[lvl];
// Pad to keep visual alignment
const cols = tree[0].length; // = 8 leaves
const step = cols / nodes.length;
for (let n = 0; n < nodes.length; n++) {
const td = document.createElement('td');
td.colSpan = step;
const div = document.createElement('div');
// Determine class
let cls = 'node';
const isLeaf = (lvl === 0);
if (isLeaf) cls += ' leaf';
// Highlight the looked-up leaf path
let pathIdx = leafIdx;
let onPath = false;
for (let l = 0; l <= lvl; l++) {
if (l === lvl && Math.floor(leafIdx / Math.pow(2, l)) === n) onPath = true;
}
// simpler: recompute which index is on path at this level
const pathAtLevel = Math.floor(leafIdx / Math.pow(2, lvl === 0 ? 0 : lvl));
if (pathAtLevel === n) cls += ' highlight';
else if (proofNodes.has(lvl) && proofNodes.get(lvl).has(n)) cls += ' proof-node';
div.className = cls;
div.title = nodes[n];
div.textContent = nodes[n].slice(0, 6) + '…';
td.appendChild(div);
tr.appendChild(td);
}
tbl.appendChild(tr);
}
vis.appendChild(tbl);
}
// Initial render (no highlight)
renderTree(-1, []);
<p class="hint">A key-transparency log stores user→key bindings as leaves of a Merkle tree. Pick a user to look up their key. The server returns the key plus a short <b>inclusion proof</b> (sibling hashes). You recompute the root — if it matches the trusted root, the key is genuine.</p>
<div class="lookup-row">
<label for="user-select">Look up user:</label>
<select id="user-select"></select>
<button id="btn-lookup" type="button">Look up</button>
</div>
<div id="result-box" class="result-box hidden">
<div class="result-line"><span class="label">User</span><span id="r-user" class="val"></span></div>
<div class="result-line"><span class="label">Public key</span><span id="r-key" class="val mono"></span></div>
<div class="result-line"><span class="label">Leaf hash</span><span id="r-leaf" class="val mono hash"></span></div>
<div class="proof-section">
<div class="proof-title">Inclusion proof (sibling hashes, bottom to top):</div>
<ol id="proof-list" class="proof-list"></ol>
</div>
<div class="result-line recomputed"><span class="label">Recomputed root</span><span id="r-comp" class="val mono hash"></span></div>
<div class="result-line trusted"><span class="label">Trusted root</span><span id="r-root" class="val mono hash"></span></div>
<div id="verdict" class="verdict"></div>
</div>
<div id="tree-vis" class="tree-vis"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.lookup-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-weight: 600; }
select { padding: .35rem .5rem; border: 1px solid #aaa; border-radius: 6px; font-size: .9rem; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
.result-box { border: 1px solid #cdd9e3; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .8rem;
background: #f5f8fb; }
.result-box.hidden { display: none; }
.result-line { display: flex; gap: .6rem; align-items: flex-start; margin-bottom: .35rem; font-size: .82rem; }
.label { font-weight: 700; min-width: 110px; color: #1d3557; }
.val { word-break: break-all; }
.mono { font-family: ui-monospace, monospace; font-size: .78rem; }
.hash { color: #5a2d82; }
.proof-section { margin: .5rem 0; }
.proof-title { font-weight: 700; font-size: .82rem; color: #1d3557; margin-bottom: .3rem; }
.proof-list { margin: 0 0 .3rem 1.4rem; padding: 0; }
.proof-list li { font-family: ui-monospace, monospace; font-size: .78rem; color: #5a2d82;
margin-bottom: .15rem; }
.proof-list li span.side { color: #888; font-family: system-ui; font-size: .75rem; margin-right: .3rem; }
.recomputed .val { color: #0a7d33; }
.trusted .val { color: #1d3557; }
.verdict { font-weight: 700; font-size: .95rem; margin-top: .4rem; padding: .35rem .6rem;
border-radius: 6px; }
.verdict.ok { background: #d4edda; color: #0a7d33; }
.verdict.fail { background: #fce8e8; color: #c92f3c; }
.tree-vis { font-family: ui-monospace, monospace; font-size: .72rem; overflow-x: auto; }
.tree-vis table { border-collapse: collapse; }
.tree-vis td { padding: 2px 4px; text-align: center; white-space: nowrap; }
.node { display: inline-block; padding: 2px 5px; border-radius: 4px; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; }
.node.leaf { background: #f0e8fb; border-color: #b89fcc; color: #5a2d82; }
.node.highlight { background: #fff3cd; border-color: #e6a817; color: #7a4800; font-weight: 700; }
.node.proof-node { background: #d4edda; border-color: #5aaa6f; color: #0a5c22; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Key-transparency Merkle demo.
// We use a deterministic "toy SHA-256" (djb2-based 32-bit hash displayed as hex)
// so it runs instantly in the sandbox without crypto APIs.
// The proof logic and tree structure are identical to a real SHA-256 Merkle tree.
function toyHash(str) {
// djb2 extended to 64-bit using two independent 32-bit hashes for display length
let h1 = 5381, h2 = 52711;
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
h1 = ((h1 << 5) + h1) ^ c;
h2 = ((h2 << 5) + h2) ^ (c * 1000003);
}
const toHex = n => (n >>> 0).toString(16).padStart(8, '0');
return toHex(h1) + toHex(h2 ^ (h1 * 2654435761));
}
// 8 fixed users and their toy public keys
const USERS = [
{ name: 'alice', key: 'pk_a1b2c3d4e5f6' },
{ name: 'bob', key: 'pk_b2c3d4e5f6a1' },
{ name: 'carol', key: 'pk_c3d4e5f6a1b2' },
{ name: 'dave', key: 'pk_d4e5f6a1b2c3' },
{ name: 'eve', key: 'pk_e5f6a1b2c3d4' },
{ name: 'frank', key: 'pk_f6a1b2c3d4e5' },
{ name: 'grace', key: 'pk_g7a8b9c0d1e2' },
{ name: 'hank', key: 'pk_h8b9c0d1e2a3' },
];
// Build leaf hashes
const leaves = USERS.map(u => toyHash(u.name + '|' + u.key));
// Build the full Merkle tree bottom-up.
// tree[0] = leaves (level 0), tree[1] = level 1, ..., tree[3] = root
function buildTree(lvs) {
const levels = [lvs.slice()];
let cur = lvs.slice();
while (cur.length > 1) {
const next = [];
for (let i = 0; i < cur.length; i += 2) next.push(toyHash(cur[i] + cur[i + 1]));
levels.push(next);
cur = next;
}
return levels;
}
const tree = buildTree(leaves);
const ROOT = tree[tree.length - 1][0];
// Generate inclusion proof for leaf at index idx
function inclusionProof(idx) {
const proof = [];
let i = idx;
for (let lvl = 0; lvl < tree.length - 1; lvl++) {
const sibling = (i % 2 === 0) ? i + 1 : i - 1;
const side = (i % 2 === 0) ? 'right' : 'left';
proof.push({ hash: tree[lvl][sibling], side });
i = Math.floor(i / 2);
}
return proof;
}
// Recompute root from leaf and proof
function verifyProof(leafHash, proof) {
let cur = leafHash;
for (const { hash, side } of proof) {
cur = (side === 'right') ? toyHash(cur + hash) : toyHash(hash + cur);
}
return cur;
}
// Populate select
const sel = document.getElementById('user-select');
USERS.forEach((u, i) => {
const opt = document.createElement('option');
opt.value = i; opt.textContent = u.name;
sel.appendChild(opt);
});
let highlightIdx = -1;
document.getElementById('btn-lookup').addEventListener('click', () => {
const idx = parseInt(sel.value, 10);
const user = USERS[idx];
const leafHash = leaves[idx];
const proof = inclusionProof(idx);
const computed = verifyProof(leafHash, proof);
const ok = computed === ROOT;
document.getElementById('r-user').textContent = user.name;
document.getElementById('r-key').textContent = user.key;
document.getElementById('r-leaf').textContent = leafHash;
const list = document.getElementById('proof-list');
list.innerHTML = '';
proof.forEach(({ hash, side }, i) => {
const li = document.createElement('li');
li.innerHTML = '<span class="side">[' + side + ']</span>' + hash;
list.appendChild(li);
});
document.getElementById('r-comp').textContent = computed;
document.getElementById('r-root').textContent = ROOT;
const verdict = document.getElementById('verdict');
if (ok) {
verdict.textContent = '✓ Root matches — key is authentic and in the log.';
verdict.className = 'verdict ok';
} else {
verdict.textContent = '✗ Root mismatch — key may have been tampered with.';
verdict.className = 'verdict fail';
}
document.getElementById('result-box').classList.remove('hidden');
highlightIdx = idx;
renderTree(idx, proof);
// Log for node verification
console.log(fmt('VERIFY idx=%idx% user=%name% leaf=%leafHash% computed=%computed% root=%ROOT% match=%ok%', { idx: idx, name: user.name, leafHash: leafHash, computed: computed, ROOT: ROOT, ok: ok }));
});
function renderTree(leafIdx, proof) {
// Build a set of proof-node indices per level for coloring
const proofNodes = new Map(); // level -> Set of indices
let i = leafIdx;
for (let lvl = 0; lvl < tree.length - 1; lvl++) {
const sib = (i % 2 === 0) ? i + 1 : i - 1;
if (!proofNodes.has(lvl)) proofNodes.set(lvl, new Set());
proofNodes.get(lvl).add(sib);
i = Math.floor(i / 2);
}
const vis = document.getElementById('tree-vis');
vis.innerHTML = '';
const tbl = document.createElement('table');
// Draw from root (top) to leaves (bottom)
for (let lvl = tree.length - 1; lvl >= 0; lvl--) {
const tr = document.createElement('tr');
const nodes = tree[lvl];
// Pad to keep visual alignment
const cols = tree[0].length; // = 8 leaves
const step = cols / nodes.length;
for (let n = 0; n < nodes.length; n++) {
const td = document.createElement('td');
td.colSpan = step;
const div = document.createElement('div');
// Determine class
let cls = 'node';
const isLeaf = (lvl === 0);
if (isLeaf) cls += ' leaf';
// Highlight the looked-up leaf path
let pathIdx = leafIdx;
let onPath = false;
for (let l = 0; l <= lvl; l++) {
if (l === lvl && Math.floor(leafIdx / Math.pow(2, l)) === n) onPath = true;
}
// simpler: recompute which index is on path at this level
const pathAtLevel = Math.floor(leafIdx / Math.pow(2, lvl === 0 ? 0 : lvl));
if (pathAtLevel === n) cls += ' highlight';
else if (proofNodes.has(lvl) && proofNodes.get(lvl).has(n)) cls += ' proof-node';
div.className = cls;
div.title = nodes[n];
div.textContent = nodes[n].slice(0, 6) + '…';
td.appendChild(div);
tr.appendChild(td);
}
tbl.appendChild(tr);
}
vis.appendChild(tbl);
}
// Initial render (no highlight)
renderTree(-1, []);
When a user looks up a contact's key, the server returns the leaf value plus a short proof — just the sibling hashes along the path from the leaf to the root (log2 N hashes for N entries). You recompute the root from those siblings and compare it to the trusted root you already have. If they match, the key was really in the log. If the server swapped the key or rewrote the log, the root will not match.
Comments
Loading comments...