Below is a simplified ring-signature simulation. The ring has four members, each with their own key pair. Select which member is the actual signer, type a message, and click Sign. The demo produces a ring signature and lets you Verify it — the verifier confirms the group signed but cannot determine the signer.
<div class="demo-wrap">
<div class="panel members-panel">
<h3 class="panel-title">Ring Members</h3>
<div id="members" class="members"></div>
</div>
<div class="panel sign-panel">
<h3 class="panel-title">Sign a Message</h3>
<div class="field">
<label for="signer-select">Actual signer</label>
<select id="signer-select"></select>
</div>
<div class="field">
<label for="msg-input">Message</label>
<input id="msg-input" type="text" value="The document is authentic." maxlength="80" />
</div>
<div class="btns">
<button id="btn-sign" type="button">Sign</button>
<button id="btn-tamper" type="button" class="ghost" disabled>Tamper & Verify</button>
<button id="btn-verify" type="button" class="ghost" disabled>Verify</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
</div>
<div class="panel sig-panel" id="sig-panel" style="display:none">
<h3 class="panel-title">Ring Signature <span class="badge">— signer hidden</span></h3>
<pre id="sig-display" class="sig-box"></pre>
</div>
</div>
*{box-sizing:border-box;}
body{font-family:system-ui,sans-serif;color:#222;margin:0;font-size:14px;}
h3.panel-title{margin:0 0 .6rem;font-size:.95rem;font-weight:700;color:#1d3557;}
.demo-wrap{display:flex;flex-direction:column;gap:.8rem;}
.panel{background:#f4f7fa;border:1px solid #d0dae5;border-radius:10px;padding:.85rem 1rem;}
.members{display:flex;flex-wrap:wrap;gap:.5rem;}
.member{display:flex;align-items:center;gap:.5rem;background:#fff;border:1px solid #c4d1df;
border-radius:8px;padding:.4rem .7rem;font-size:.85rem;}
.member .avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;
justify-content:center;font-weight:700;font-size:.8rem;color:#fff;}
.member .key{font-family:ui-monospace,monospace;color:#5a7088;font-size:.8rem;}
.field{margin-bottom:.6rem;}
label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.25rem;color:#334;}
select,input[type=text]{width:100%;padding:.38rem .6rem;border:1px solid #c4d1df;
border-radius:6px;font-size:.88rem;outline:none;}
select:focus,input:focus{border-color:#457b9d;}
.btns{display:flex;gap:.45rem;flex-wrap:wrap;margin-top:.5rem;}
button{font:600 13px system-ui,sans-serif;padding:.4rem .85rem;border:1px solid #1d3557;
background:#1d3557;color:#fff;border-radius:7px;cursor:pointer;}
button.ghost{background:#fff;color:#1d3557;}
button:disabled{opacity:.45;cursor:not-allowed;}
.status{margin-top:.55rem;font-weight:600;font-size:.92rem;min-height:1.3em;}
.status.ok{color:#0a7d33;}.status.bad{color:#c92f3c;}.status.info{color:#457b9d;}
.sig-box{background:#1d3557;color:#a8d8ea;border-radius:8px;padding:.75rem .9rem;
font-size:.75rem;white-space:pre-wrap;word-break:break-all;margin:0;max-height:160px;overflow-y:auto;}
.badge{font-size:.75rem;font-weight:400;background:#e63946;color:#fff;
border-radius:4px;padding:.1rem .4rem;margin-left:.3rem;}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified ring-signature simulation using toy modular arithmetic.
// Captures the real ring-signature structure: each member contributes a value;
// the ring "closes"; the verifier checks closure without knowing who opened it.
// Real schemes use 256-bit elliptic curves — here we use a small prime for clarity.
//
// Group: cyclic of order P-1 (prime P), generator G.
// Member i: private key xi, public key yi = G^xi mod P.
// Sign with member s:
// 1. Pick random nonce k for the signer.
// 2. Set c[(s+1)%N] = H(msg, G^k mod P).
// 3. For each non-signer i (going around the ring), pick random r[i] and
// compute c[(i+1)%N] = H(msg, G^r[i] * PUB[i]^c[i] mod P).
// 4. After the ring has been built up to c[s], close it:
// r[s] = (k - xs * c[s]) mod (P-1).
// Verify: recompute each c[i] from c[0] around the full ring; check final c == c[0].
const P = 1000000007; // prime modulus
const G = 2; // generator
function modpow(base, exp, mod) {
let result = 1n;
base = BigInt(Math.round(Math.abs(base))) % BigInt(mod);
exp = BigInt(Math.round(Math.abs(exp)));
const bmod = BigInt(mod);
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % bmod;
exp >>= 1n;
base = base * base % bmod;
}
return Number(result);
}
function hashStr(s) {
let h = 5381;
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 33) + s.charCodeAt(i)) | 0;
return ((h >>> 0) % (P - 2)) + 1;
}
function H(msg, val) { return hashStr(msg + '|' + val); }
const COLORS = ['#e63946','#457b9d','#2a9d8f','#e9c46a'];
const NAMES = ['Alice','Bob','Carol','Dave'];
const N = 4;
const PRIV = [7, 13, 19, 31]; // private keys (tiny for demo)
const PUB = PRIV.map(x => modpow(G, x, P)); // public keys G^x mod P
let currentSig = null;
let currentMsg = '';
let tampered = false;
function renderMembers() {
const el = document.getElementById('members');
el.innerHTML = NAMES.map((name, i) => `
<div class="member">
<div class="avatar" style="background:${COLORS[i]}">${name[0]}</div>
<div>
<div><b>${name}</b></div>
<div class="key">pub: ${String(PUB[i]).slice(0,8)}…</div>
</div>
</div>`).join('');
const sel = document.getElementById('signer-select');
sel.innerHTML = NAMES.map((n,i) => `<option value="${i}">${n}</option>`).join('');
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function sign(signerIdx, msg) {
const xs = PRIV[signerIdx];
const seed = hashStr(msg + '|' + signerIdx);
// Deterministic nonce k (random in a real scheme)
const k = ((seed * 37 + 13) % (P - 2)) + 1;
// Random r[i] for each non-signer member
const r = PRIV.map((_, i) => {
if (i === signerIdx) return 0; // placeholder
return ((seed * (i + 5) * 53 + 7) % (P - 2)) + 1;
});
const c = new Array(N);
// Start the ring one position after the signer
c[(signerIdx + 1) % N] = H(msg, modpow(G, k, P));
// Propagate around the ring (N-1 steps, stopping before signerIdx)
for (let step = 1; step < N; step++) {
const i = (signerIdx + step) % N;
if (i === signerIdx) break;
const next = (i + 1) % N;
const Gri = BigInt(modpow(G, r[i], P));
const Yici = BigInt(modpow(PUB[i], c[i], P));
const term = Number(Gri * Yici % BigInt(P));
c[next] = H(msg, term);
}
// Close the ring: r[s] = (k - xs*c[s]) mod (P-1)
const order = P - 1;
r[signerIdx] = ((k - xs * c[signerIdx] % order) % order + order) % order;
return { c0: c[0], r };
}
function verify(sig, msg) {
const { c0, r } = sig;
let c = c0;
for (let i = 0; i < N; i++) {
const Gri = BigInt(modpow(G, r[i], P));
const Yici = BigInt(modpow(PUB[i], c, P));
const term = Number(Gri * Yici % BigInt(P));
c = H(msg, term);
}
return c === c0; // ring closes iff final c matches starting c0
}
document.getElementById('btn-sign').addEventListener('click', () => {
const sIdx = parseInt(document.getElementById('signer-select').value);
currentMsg = document.getElementById('msg-input').value.trim() || `The document is authentic.`;
tampered = false;
currentSig = sign(sIdx, currentMsg);
const panel = document.getElementById('sig-panel');
panel.style.display = '';
document.getElementById('sig-display').textContent =
fmt(`c₀ = %c0%
r = [%map%]
(%N%-member ring · actual signer hidden among %N% candidates)`, { c0: currentSig.c0, map: currentSig.r.map(v=>String(v).slice(0,8)+'…').join(', '), N: N });
document.getElementById('btn-verify').disabled = false;
document.getElementById('btn-tamper').disabled = false;
setStatus(`Signature produced. Click Verify to check it.`, 'info');
console.log(`Signed by`, NAMES[sIdx], '| c0=', currentSig.c0);
});
document.getElementById('btn-verify').addEventListener('click', () => {
if (!currentSig) return;
const msg = tampered ? currentMsg + 'TAMPERED' : currentMsg;
const ok = verify(currentSig, msg);
if (tampered) {
setStatus(`✗ Tampered message — signature INVALID. Integrity check failed.`, 'bad');
console.log(`Verify (tampered):`, ok, `— expected false`);
} else {
setStatus(ok
? `✓ Valid ring signature — one of the 4 members signed. Identity unknown.`
: `✗ Invalid signature.`, ok ? 'ok' : 'bad');
console.log(`Verify:`, ok);
}
});
document.getElementById('btn-tamper').addEventListener('click', () => {
tampered = true;
setStatus(`Message tampered! Now click Verify to see integrity fail.`, 'bad');
const box = document.getElementById('sig-display');
box.textContent = box.textContent.replace(`actual signer hidden`, `MESSAGE TAMPERED — verify should fail`);
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentSig = null; currentMsg = ''; tampered = false;
document.getElementById('sig-panel').style.display = 'none';
document.getElementById('btn-verify').disabled = true;
document.getElementById('btn-tamper').disabled = true;
document.getElementById('msg-input').value = `The document is authentic.`;
setStatus('');
});
renderMembers();
setStatus(`Select a signer and click Sign.`, 'info');
<div class="demo-wrap">
<div class="panel members-panel">
<h3 class="panel-title">Ring Members</h3>
<div id="members" class="members"></div>
</div>
<div class="panel sign-panel">
<h3 class="panel-title">Sign a Message</h3>
<div class="field">
<label for="signer-select">Actual signer</label>
<select id="signer-select"></select>
</div>
<div class="field">
<label for="msg-input">Message</label>
<input id="msg-input" type="text" value="The document is authentic." maxlength="80" />
</div>
<div class="btns">
<button id="btn-sign" type="button">Sign</button>
<button id="btn-tamper" type="button" class="ghost" disabled>Tamper & Verify</button>
<button id="btn-verify" type="button" class="ghost" disabled>Verify</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
</div>
<div class="panel sig-panel" id="sig-panel" style="display:none">
<h3 class="panel-title">Ring Signature <span class="badge">— signer hidden</span></h3>
<pre id="sig-display" class="sig-box"></pre>
</div>
</div>
*{box-sizing:border-box;}
body{font-family:system-ui,sans-serif;color:#222;margin:0;font-size:14px;}
h3.panel-title{margin:0 0 .6rem;font-size:.95rem;font-weight:700;color:#1d3557;}
.demo-wrap{display:flex;flex-direction:column;gap:.8rem;}
.panel{background:#f4f7fa;border:1px solid #d0dae5;border-radius:10px;padding:.85rem 1rem;}
.members{display:flex;flex-wrap:wrap;gap:.5rem;}
.member{display:flex;align-items:center;gap:.5rem;background:#fff;border:1px solid #c4d1df;
border-radius:8px;padding:.4rem .7rem;font-size:.85rem;}
.member .avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;
justify-content:center;font-weight:700;font-size:.8rem;color:#fff;}
.member .key{font-family:ui-monospace,monospace;color:#5a7088;font-size:.8rem;}
.field{margin-bottom:.6rem;}
label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.25rem;color:#334;}
select,input[type=text]{width:100%;padding:.38rem .6rem;border:1px solid #c4d1df;
border-radius:6px;font-size:.88rem;outline:none;}
select:focus,input:focus{border-color:#457b9d;}
.btns{display:flex;gap:.45rem;flex-wrap:wrap;margin-top:.5rem;}
button{font:600 13px system-ui,sans-serif;padding:.4rem .85rem;border:1px solid #1d3557;
background:#1d3557;color:#fff;border-radius:7px;cursor:pointer;}
button.ghost{background:#fff;color:#1d3557;}
button:disabled{opacity:.45;cursor:not-allowed;}
.status{margin-top:.55rem;font-weight:600;font-size:.92rem;min-height:1.3em;}
.status.ok{color:#0a7d33;}.status.bad{color:#c92f3c;}.status.info{color:#457b9d;}
.sig-box{background:#1d3557;color:#a8d8ea;border-radius:8px;padding:.75rem .9rem;
font-size:.75rem;white-space:pre-wrap;word-break:break-all;margin:0;max-height:160px;overflow-y:auto;}
.badge{font-size:.75rem;font-weight:400;background:#e63946;color:#fff;
border-radius:4px;padding:.1rem .4rem;margin-left:.3rem;}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified ring-signature simulation using toy modular arithmetic.
// Captures the real ring-signature structure: each member contributes a value;
// the ring "closes"; the verifier checks closure without knowing who opened it.
// Real schemes use 256-bit elliptic curves — here we use a small prime for clarity.
//
// Group: cyclic of order P-1 (prime P), generator G.
// Member i: private key xi, public key yi = G^xi mod P.
// Sign with member s:
// 1. Pick random nonce k for the signer.
// 2. Set c[(s+1)%N] = H(msg, G^k mod P).
// 3. For each non-signer i (going around the ring), pick random r[i] and
// compute c[(i+1)%N] = H(msg, G^r[i] * PUB[i]^c[i] mod P).
// 4. After the ring has been built up to c[s], close it:
// r[s] = (k - xs * c[s]) mod (P-1).
// Verify: recompute each c[i] from c[0] around the full ring; check final c == c[0].
const P = 1000000007; // prime modulus
const G = 2; // generator
function modpow(base, exp, mod) {
let result = 1n;
base = BigInt(Math.round(Math.abs(base))) % BigInt(mod);
exp = BigInt(Math.round(Math.abs(exp)));
const bmod = BigInt(mod);
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % bmod;
exp >>= 1n;
base = base * base % bmod;
}
return Number(result);
}
function hashStr(s) {
let h = 5381;
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 33) + s.charCodeAt(i)) | 0;
return ((h >>> 0) % (P - 2)) + 1;
}
function H(msg, val) { return hashStr(msg + '|' + val); }
const COLORS = ['#e63946','#457b9d','#2a9d8f','#e9c46a'];
const NAMES = ['Alice','Bob','Carol','Dave'];
const N = 4;
const PRIV = [7, 13, 19, 31]; // private keys (tiny for demo)
const PUB = PRIV.map(x => modpow(G, x, P)); // public keys G^x mod P
let currentSig = null;
let currentMsg = '';
let tampered = false;
function renderMembers() {
const el = document.getElementById('members');
el.innerHTML = NAMES.map((name, i) => `
<div class="member">
<div class="avatar" style="background:${COLORS[i]}">${name[0]}</div>
<div>
<div><b>${name}</b></div>
<div class="key">pub: ${String(PUB[i]).slice(0,8)}…</div>
</div>
</div>`).join('');
const sel = document.getElementById('signer-select');
sel.innerHTML = NAMES.map((n,i) => `<option value="${i}">${n}</option>`).join('');
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function sign(signerIdx, msg) {
const xs = PRIV[signerIdx];
const seed = hashStr(msg + '|' + signerIdx);
// Deterministic nonce k (random in a real scheme)
const k = ((seed * 37 + 13) % (P - 2)) + 1;
// Random r[i] for each non-signer member
const r = PRIV.map((_, i) => {
if (i === signerIdx) return 0; // placeholder
return ((seed * (i + 5) * 53 + 7) % (P - 2)) + 1;
});
const c = new Array(N);
// Start the ring one position after the signer
c[(signerIdx + 1) % N] = H(msg, modpow(G, k, P));
// Propagate around the ring (N-1 steps, stopping before signerIdx)
for (let step = 1; step < N; step++) {
const i = (signerIdx + step) % N;
if (i === signerIdx) break;
const next = (i + 1) % N;
const Gri = BigInt(modpow(G, r[i], P));
const Yici = BigInt(modpow(PUB[i], c[i], P));
const term = Number(Gri * Yici % BigInt(P));
c[next] = H(msg, term);
}
// Close the ring: r[s] = (k - xs*c[s]) mod (P-1)
const order = P - 1;
r[signerIdx] = ((k - xs * c[signerIdx] % order) % order + order) % order;
return { c0: c[0], r };
}
function verify(sig, msg) {
const { c0, r } = sig;
let c = c0;
for (let i = 0; i < N; i++) {
const Gri = BigInt(modpow(G, r[i], P));
const Yici = BigInt(modpow(PUB[i], c, P));
const term = Number(Gri * Yici % BigInt(P));
c = H(msg, term);
}
return c === c0; // ring closes iff final c matches starting c0
}
document.getElementById('btn-sign').addEventListener('click', () => {
const sIdx = parseInt(document.getElementById('signer-select').value);
currentMsg = document.getElementById('msg-input').value.trim() || `The document is authentic.`;
tampered = false;
currentSig = sign(sIdx, currentMsg);
const panel = document.getElementById('sig-panel');
panel.style.display = '';
document.getElementById('sig-display').textContent =
fmt(`c₀ = %c0%
r = [%map%]
(%N%-member ring · actual signer hidden among %N% candidates)`, { c0: currentSig.c0, map: currentSig.r.map(v=>String(v).slice(0,8)+'…').join(', '), N: N });
document.getElementById('btn-verify').disabled = false;
document.getElementById('btn-tamper').disabled = false;
setStatus(`Signature produced. Click Verify to check it.`, 'info');
console.log(`Signed by`, NAMES[sIdx], '| c0=', currentSig.c0);
});
document.getElementById('btn-verify').addEventListener('click', () => {
if (!currentSig) return;
const msg = tampered ? currentMsg + 'TAMPERED' : currentMsg;
const ok = verify(currentSig, msg);
if (tampered) {
setStatus(`✗ Tampered message — signature INVALID. Integrity check failed.`, 'bad');
console.log(`Verify (tampered):`, ok, `— expected false`);
} else {
setStatus(ok
? `✓ Valid ring signature — one of the 4 members signed. Identity unknown.`
: `✗ Invalid signature.`, ok ? 'ok' : 'bad');
console.log(`Verify:`, ok);
}
});
document.getElementById('btn-tamper').addEventListener('click', () => {
tampered = true;
setStatus(`Message tampered! Now click Verify to see integrity fail.`, 'bad');
const box = document.getElementById('sig-display');
box.textContent = box.textContent.replace(`actual signer hidden`, `MESSAGE TAMPERED — verify should fail`);
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentSig = null; currentMsg = ''; tampered = false;
document.getElementById('sig-panel').style.display = 'none';
document.getElementById('btn-verify').disabled = true;
document.getElementById('btn-tamper').disabled = true;
document.getElementById('msg-input').value = `The document is authentic.`;
setStatus('');
});
renderMembers();
setStatus(`Select a signer and click Sign.`, 'info');