The demo below walks through all four steps of the RSA blind-signature protocol. The arithmetic uses small numbers so every value stays visible — real deployments use 2048-bit keys.
<div class="protocol">
<div class="params-row">
<span class="label">Public key <em>(n, e)</em>:</span>
<span id="pubkey" class="val mono"></span>
<span class="label">Private key <em>d</em>:</span>
<span id="privkey" class="val mono"></span>
</div>
<div class="steps">
<div class="step" id="step-blind">
<div class="step-head"><span class="badge">1</span> User blinds the message</div>
<div class="step-body">
<label>Message <em>m</em>: <input id="msg" type="number" min="2" max="90" value="17"></label>
<label>Blinding factor <em>r</em>: <input id="rfactor" type="number" min="2" max="90" value="11"></label>
<button id="btn-blind">Blind →</button>
<div class="result" id="res-blind"></div>
</div>
</div>
<div class="step locked" id="step-sign">
<div class="step-head"><span class="badge">2</span> Signer signs the blinded message</div>
<div class="step-body">
<div class="result" id="res-sign-input"></div>
<button id="btn-sign" disabled>Sign →</button>
<div class="result" id="res-sign"></div>
</div>
</div>
<div class="step locked" id="step-unblind">
<div class="step-head"><span class="badge">3</span> User unblinds</div>
<div class="step-body">
<div class="result" id="res-unblind"></div>
</div>
</div>
<div class="step locked" id="step-verify">
<div class="step-head"><span class="badge">4</span> Anyone verifies</div>
<div class="step-body">
<div class="result" id="res-verify"></div>
<button id="btn-verify" disabled>Verify →</button>
<div class="result" id="res-verify-out"></div>
</div>
</div>
</div>
<button id="btn-reset" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .93rem; }
.protocol { max-width: 520px; }
.params-row { display: flex; gap: 1rem; flex-wrap: wrap; background: #eef2f6; border-radius: 8px;
padding: .5rem .8rem; margin-bottom: 1rem; font-size: .85rem; align-items: center; }
.label { color: #555; }
.val.mono { font-family: ui-monospace, monospace; font-weight: 700; color: #1d3557; }
.steps { display: flex; flex-direction: column; gap: .7rem; }
.step { border: 1px solid #cdd9e3; border-radius: 10px; overflow: hidden; }
.step.locked { opacity: .45; pointer-events: none; }
.step-head { display: flex; align-items: center; gap: .55rem; background: #f0f4f8;
padding: .45rem .8rem; font-weight: 600; font-size: .88rem; }
.badge { display: inline-flex; align-items: center; justify-content: center;
width: 1.5rem; height: 1.5rem; border-radius: 50%; background: #1d3557;
color: #fff; font-size: .78rem; font-weight: 700; flex-shrink: 0; }
.step-body { padding: .6rem .8rem; display: flex; flex-direction: column; gap: .4rem; }
label { display: flex; align-items: center; gap: .4rem; font-size: .88rem; }
input[type=number] { width: 5rem; padding: .2rem .4rem; border: 1px solid #cdd9e3;
border-radius: 6px; font: inherit; text-align: center; }
button { font: 600 .85rem system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; align-self: flex-start; }
button.ghost { background: #fff; color: #1d3557; margin-top: .6rem; }
button:disabled { opacity: .4; cursor: not-allowed; }
.result { font-family: ui-monospace, monospace; font-size: .82rem; line-height: 1.6;
background: #f7f9fb; border-radius: 6px; padding: .35rem .6rem; white-space: pre-wrap; }
.result:empty { display: none; }
.ok { color: #0a7d33; font-weight: 700; }
.bad { color: #c92f3c; font-weight: 700; }
.note { font-size: .8rem; color: #666; font-style: italic; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// RSA Blind Signature protocol (toy numbers, illustrative only)
// Key: n=91 (7*13), e=5, d=29 (e*d=145=1 mod lcm(6,12)=12 ✓)
// User picks message m and blinding factor r (both coprime to n).
// Step 1 (blind): blinded = r^e * m mod n
// Step 2 (sign): s_blind = blinded^d mod n = r * m^d mod n
// Step 3 (unblind): sig = s_blind * r_inv mod n = m^d mod n
// Step 4 (verify): sig^e mod n == m → valid RSA signature
const N = 91, E = 5, D = 29;
document.getElementById('pubkey').textContent = `n=${N}, e=${E}`;
document.getElementById('privkey').textContent = `d=${D}`;
function modpow(base, exp, mod) {
let result = 1n; base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp >>= 1n; base = base * base % mod;
}
return result;
}
function gcd(a, b) { while (b) { [a, b] = [b, a % b]; } return a; }
// Extended Euclidean → modular inverse
function modinv(a, m) {
let [old_r, r] = [a, m], [old_s, s] = [1n, 0n];
while (r !== 0n) {
const q = old_r / r;
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
}
return ((old_s % m) + m) % m;
}
let state = {};
function setStep(id, unlock) {
const el = document.getElementById(id);
if (unlock) el.classList.remove('locked');
else el.classList.add('locked');
}
function reset() {
['res-blind','res-sign-input','res-sign','res-unblind','res-verify','res-verify-out']
.forEach(id => { const el = document.getElementById(id); if (el) el.textContent = ''; });
document.getElementById('btn-sign').disabled = true;
document.getElementById('btn-verify').disabled = true;
['step-sign','step-unblind','step-verify'].forEach(id => setStep(id, false));
state = {};
}
document.getElementById('btn-blind').addEventListener('click', () => {
reset();
const m = BigInt(document.getElementById('msg').value);
const r = BigInt(document.getElementById('rfactor').value);
if (m < 2n || m >= BigInt(N) || gcd(m, BigInt(N)) !== 1n) {
document.getElementById('res-blind').textContent = fmt(`m must be 2..%v% and coprime to %N% (avoid multiples of 7 or 13)`, { v: N-1, N: N }); return; }
if (r < 2n || r >= BigInt(N) || gcd(r, BigInt(N)) !== 1n) {
document.getElementById('res-blind').textContent = fmt(`r must be 2..%v% and coprime to %N% (avoid multiples of 7 or 13)`, { v: N-1, N: N }); return; }
const re = modpow(r, BigInt(E), BigInt(N));
const blinded = re * m % BigInt(N);
state = { m, r, blinded };
document.getElementById('res-blind').textContent =
fmt(`r^e mod n = %r%^%E% mod %N% = %re%
blinded = r^e · m mod n = %re% × %m% mod %N% = %blinded%
(signer only sees: %blinded%)`, { r: r, E: E, N: N, re: re, m: m, blinded: blinded });
setStep('step-sign', true);
document.getElementById('res-sign-input').textContent = fmt(`Signer receives blinded message: %blinded%`, { blinded: blinded });
document.getElementById('btn-sign').disabled = false;
});
document.getElementById('btn-sign').addEventListener('click', () => {
const { blinded, m, r } = state;
const s_blind = modpow(blinded, BigInt(D), BigInt(N));
state.s_blind = s_blind;
document.getElementById('res-sign').textContent =
fmt(`s_blind = %blinded%^%D% mod %N% = %s_blind%
(signer returns this — still cannot see m)`, { blinded: blinded, D: D, N: N, s_blind: s_blind });
// Step 3: unblind immediately (user’s private computation)
const r_inv = modinv(r, BigInt(N));
const sig = s_blind * r_inv % BigInt(N);
state.sig = sig;
setStep('step-unblind', true);
document.getElementById('res-unblind').textContent =
fmt(`r_inv = %r%⁻¹ mod %N% = %r_inv%
sig = s_blind × r_inv mod n = %s_blind% × %r_inv% mod %N% = %sig%
This equals m^d mod n = %m%^%D% mod %N% = %modpow% ✓`, { r: r, N: N, r_inv: r_inv, s_blind: s_blind, sig: sig, m: m, D: D, modpow: modpow(m, BigInt(D), BigInt(N)) });
setStep('step-verify', true);
document.getElementById('res-verify').textContent =
fmt(`Verifier has: (sig=%sig%, m=%m%, public key n=%N%, e=%E%)`, { sig: sig, m: m, N: N, E: E });
document.getElementById('btn-verify').disabled = false;
});
document.getElementById('btn-verify').addEventListener('click', () => {
const { sig, m } = state;
const check = modpow(sig, BigInt(E), BigInt(N));
const ok = check === m;
document.getElementById('res-verify-out').innerHTML =
`sig^e mod n = ${sig}^${E} mod ${N} = ${check}\nm = ${m}\n` +
`<span class="${ok ? 'ok' : 'bad'}">${ok ? `✓ Valid signature — signer never saw m!` : `✗ Invalid`}</span>`;
});
document.getElementById('btn-reset').addEventListener('click', reset);
<div class="protocol">
<div class="params-row">
<span class="label">Public key <em>(n, e)</em>:</span>
<span id="pubkey" class="val mono"></span>
<span class="label">Private key <em>d</em>:</span>
<span id="privkey" class="val mono"></span>
</div>
<div class="steps">
<div class="step" id="step-blind">
<div class="step-head"><span class="badge">1</span> User blinds the message</div>
<div class="step-body">
<label>Message <em>m</em>: <input id="msg" type="number" min="2" max="90" value="17"></label>
<label>Blinding factor <em>r</em>: <input id="rfactor" type="number" min="2" max="90" value="11"></label>
<button id="btn-blind">Blind →</button>
<div class="result" id="res-blind"></div>
</div>
</div>
<div class="step locked" id="step-sign">
<div class="step-head"><span class="badge">2</span> Signer signs the blinded message</div>
<div class="step-body">
<div class="result" id="res-sign-input"></div>
<button id="btn-sign" disabled>Sign →</button>
<div class="result" id="res-sign"></div>
</div>
</div>
<div class="step locked" id="step-unblind">
<div class="step-head"><span class="badge">3</span> User unblinds</div>
<div class="step-body">
<div class="result" id="res-unblind"></div>
</div>
</div>
<div class="step locked" id="step-verify">
<div class="step-head"><span class="badge">4</span> Anyone verifies</div>
<div class="step-body">
<div class="result" id="res-verify"></div>
<button id="btn-verify" disabled>Verify →</button>
<div class="result" id="res-verify-out"></div>
</div>
</div>
</div>
<button id="btn-reset" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .93rem; }
.protocol { max-width: 520px; }
.params-row { display: flex; gap: 1rem; flex-wrap: wrap; background: #eef2f6; border-radius: 8px;
padding: .5rem .8rem; margin-bottom: 1rem; font-size: .85rem; align-items: center; }
.label { color: #555; }
.val.mono { font-family: ui-monospace, monospace; font-weight: 700; color: #1d3557; }
.steps { display: flex; flex-direction: column; gap: .7rem; }
.step { border: 1px solid #cdd9e3; border-radius: 10px; overflow: hidden; }
.step.locked { opacity: .45; pointer-events: none; }
.step-head { display: flex; align-items: center; gap: .55rem; background: #f0f4f8;
padding: .45rem .8rem; font-weight: 600; font-size: .88rem; }
.badge { display: inline-flex; align-items: center; justify-content: center;
width: 1.5rem; height: 1.5rem; border-radius: 50%; background: #1d3557;
color: #fff; font-size: .78rem; font-weight: 700; flex-shrink: 0; }
.step-body { padding: .6rem .8rem; display: flex; flex-direction: column; gap: .4rem; }
label { display: flex; align-items: center; gap: .4rem; font-size: .88rem; }
input[type=number] { width: 5rem; padding: .2rem .4rem; border: 1px solid #cdd9e3;
border-radius: 6px; font: inherit; text-align: center; }
button { font: 600 .85rem system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; align-self: flex-start; }
button.ghost { background: #fff; color: #1d3557; margin-top: .6rem; }
button:disabled { opacity: .4; cursor: not-allowed; }
.result { font-family: ui-monospace, monospace; font-size: .82rem; line-height: 1.6;
background: #f7f9fb; border-radius: 6px; padding: .35rem .6rem; white-space: pre-wrap; }
.result:empty { display: none; }
.ok { color: #0a7d33; font-weight: 700; }
.bad { color: #c92f3c; font-weight: 700; }
.note { font-size: .8rem; color: #666; font-style: italic; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// RSA Blind Signature protocol (toy numbers, illustrative only)
// Key: n=91 (7*13), e=5, d=29 (e*d=145=1 mod lcm(6,12)=12 ✓)
// User picks message m and blinding factor r (both coprime to n).
// Step 1 (blind): blinded = r^e * m mod n
// Step 2 (sign): s_blind = blinded^d mod n = r * m^d mod n
// Step 3 (unblind): sig = s_blind * r_inv mod n = m^d mod n
// Step 4 (verify): sig^e mod n == m → valid RSA signature
const N = 91, E = 5, D = 29;
document.getElementById('pubkey').textContent = `n=${N}, e=${E}`;
document.getElementById('privkey').textContent = `d=${D}`;
function modpow(base, exp, mod) {
let result = 1n; base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp >>= 1n; base = base * base % mod;
}
return result;
}
function gcd(a, b) { while (b) { [a, b] = [b, a % b]; } return a; }
// Extended Euclidean → modular inverse
function modinv(a, m) {
let [old_r, r] = [a, m], [old_s, s] = [1n, 0n];
while (r !== 0n) {
const q = old_r / r;
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
}
return ((old_s % m) + m) % m;
}
let state = {};
function setStep(id, unlock) {
const el = document.getElementById(id);
if (unlock) el.classList.remove('locked');
else el.classList.add('locked');
}
function reset() {
['res-blind','res-sign-input','res-sign','res-unblind','res-verify','res-verify-out']
.forEach(id => { const el = document.getElementById(id); if (el) el.textContent = ''; });
document.getElementById('btn-sign').disabled = true;
document.getElementById('btn-verify').disabled = true;
['step-sign','step-unblind','step-verify'].forEach(id => setStep(id, false));
state = {};
}
document.getElementById('btn-blind').addEventListener('click', () => {
reset();
const m = BigInt(document.getElementById('msg').value);
const r = BigInt(document.getElementById('rfactor').value);
if (m < 2n || m >= BigInt(N) || gcd(m, BigInt(N)) !== 1n) {
document.getElementById('res-blind').textContent = fmt(`m must be 2..%v% and coprime to %N% (avoid multiples of 7 or 13)`, { v: N-1, N: N }); return; }
if (r < 2n || r >= BigInt(N) || gcd(r, BigInt(N)) !== 1n) {
document.getElementById('res-blind').textContent = fmt(`r must be 2..%v% and coprime to %N% (avoid multiples of 7 or 13)`, { v: N-1, N: N }); return; }
const re = modpow(r, BigInt(E), BigInt(N));
const blinded = re * m % BigInt(N);
state = { m, r, blinded };
document.getElementById('res-blind').textContent =
fmt(`r^e mod n = %r%^%E% mod %N% = %re%
blinded = r^e · m mod n = %re% × %m% mod %N% = %blinded%
(signer only sees: %blinded%)`, { r: r, E: E, N: N, re: re, m: m, blinded: blinded });
setStep('step-sign', true);
document.getElementById('res-sign-input').textContent = fmt(`Signer receives blinded message: %blinded%`, { blinded: blinded });
document.getElementById('btn-sign').disabled = false;
});
document.getElementById('btn-sign').addEventListener('click', () => {
const { blinded, m, r } = state;
const s_blind = modpow(blinded, BigInt(D), BigInt(N));
state.s_blind = s_blind;
document.getElementById('res-sign').textContent =
fmt(`s_blind = %blinded%^%D% mod %N% = %s_blind%
(signer returns this — still cannot see m)`, { blinded: blinded, D: D, N: N, s_blind: s_blind });
// Step 3: unblind immediately (user’s private computation)
const r_inv = modinv(r, BigInt(N));
const sig = s_blind * r_inv % BigInt(N);
state.sig = sig;
setStep('step-unblind', true);
document.getElementById('res-unblind').textContent =
fmt(`r_inv = %r%⁻¹ mod %N% = %r_inv%
sig = s_blind × r_inv mod n = %s_blind% × %r_inv% mod %N% = %sig%
This equals m^d mod n = %m%^%D% mod %N% = %modpow% ✓`, { r: r, N: N, r_inv: r_inv, s_blind: s_blind, sig: sig, m: m, D: D, modpow: modpow(m, BigInt(D), BigInt(N)) });
setStep('step-verify', true);
document.getElementById('res-verify').textContent =
fmt(`Verifier has: (sig=%sig%, m=%m%, public key n=%N%, e=%E%)`, { sig: sig, m: m, N: N, E: E });
document.getElementById('btn-verify').disabled = false;
});
document.getElementById('btn-verify').addEventListener('click', () => {
const { sig, m } = state;
const check = modpow(sig, BigInt(E), BigInt(N));
const ok = check === m;
document.getElementById('res-verify-out').innerHTML =
`sig^e mod n = ${sig}^${E} mod ${N} = ${check}\nm = ${m}\n` +
`<span class="${ok ? 'ok' : 'bad'}">${ok ? `✓ Valid signature — signer never saw m!` : `✗ Invalid`}</span>`;
});
document.getElementById('btn-reset').addEventListener('click', reset);
Notice what happens at each step. The signer sees only the blinded message — a number that looks random, with no relationship to the original. After signing, you apply the inverse blinding factor and recover a valid signature. A verifier who knows only the signer's public key can confirm the signature is genuine — without knowing which blinding factor you used or when the signing happened.
Comments
Loading comments...