Below is a tiny RSA-style keypair. Type a message, sign it with the private key, and a signature appears. Then verify it with the public key — only the public key is needed to check.
<p class="hint">A digital signature proves a message came from you <b>and</b> was not
altered — without ever revealing your private key. Sign the message with the
<b>private key</b>, then verify with the matching <b>public key</b>. Change a single
character and watch verification fail.</p>
<label class="lbl">Message</label>
<textarea id="msg" rows="2">Transfer $100 to Ana</textarea>
<div class="btns">
<button id="sign" type="button">Sign with private key</button>
<button id="verify" type="button">Verify with public key</button>
<button id="tamper" type="button" class="ghost">Tamper (flip 1 char)</button>
</div>
<div class="row"><span class="tag">Signature</span><code id="sig">—</code></div>
<div class="status" id="status">Sign the message, then verify.</div>
<div class="keys">
<span>public key (e,n) = (<b id="pe">17</b>, <b id="pn">3233</b>)</span>
<span class="secret">private key d = ••• (never shared)</span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.lbl { font: 600 .8rem system-ui; color: #1d3557; display: block; margin-bottom: .25rem; }
textarea { width: 100%; font: 500 14px ui-monospace, monospace; padding: .5rem;
border: 1px solid #cdd9e3; border-radius: 8px; resize: vertical; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .7rem 0; }
.row { display: flex; align-items: center; gap: .5rem; margin: .4rem 0; }
.tag { font: 600 .72rem system-ui; background: #e8eef3; color: #1d3557;
padding: .15rem .5rem; border-radius: 6px; }
code { font: 600 13px ui-monospace, monospace; color: #1d3557; word-break: break-all; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.keys { font: 500 .8rem ui-monospace, monospace; color: #555; display: flex;
flex-direction: column; gap: .2rem; margin-top: .6rem; }
.keys .secret { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// A textbook RSA keypair (tiny, for illustration only — real keys are 2048+ bits).
// n = p*q = 61*53 = 3233, phi = 60*52 = 3120, public e = 17, private d = 2753.
// Signing: s = (hash(message))^d mod n — needs the PRIVATE key d.
// Verifying: recompute hash, check (s)^e mod n == hash — needs only the PUBLIC key e.
// Anyone can verify; only the holder of d can produce a signature that verifies.
const n = 3233, e = 17, d = 2753;
function modpow(base, exp, mod){ // (base^exp) mod mod, no big-int overflow
let r = 1; base %= mod;
while(exp > 0){ if(exp & 1) r = (r * base) % mod; exp >>= 1; base = (base * base) % mod; }
return r;
}
// Simple deterministic hash of the message into the range [1, n-1].
function hash(str){
let h = 7;
for(let i = 0; i < str.length; i++){ h = (h * 31 + str.charCodeAt(i)) % n; }
return h === 0 ? 1 : h; // avoid the fixed point 0
}
let signature = null; // the signature is bound to the message that was signed
const $ = id => document.getElementById(id);
function setStatus(t, cls){ const s = $('status'); s.textContent = t; s.className = 'status ' + cls; }
function sign(){
const m = $('msg').value;
signature = modpow(hash(m), d, n); // uses the PRIVATE key
$('sig').textContent = signature;
setStatus('Signed. The signature is bound to this exact message.', '');
}
function verify(){
if(signature === null){ setStatus('Nothing signed yet — sign first.', 'bad'); return; }
const m = $('msg').value;
const recovered = modpow(signature, e, n); // uses only the PUBLIC key
if(recovered === hash(m)) setStatus('✓ Valid: this message really was signed with the matching private key.', 'ok');
else setStatus('✗ Invalid: the message was altered or the signature does not match.', 'bad');
}
function tamper(){
const t = $('msg'); const s = t.value;
if(!s.length) return;
const i = Math.floor(s.length / 2);
const c = s[i] === '0' ? '9' : (s[i] === '9' ? '0' : (s.charCodeAt(i) === 65 ? 66 : 65));
const ch = typeof c === 'number' ? String.fromCharCode(c) : c;
t.value = s.slice(0, i) + ch + s.slice(i + 1);
setStatus('Message tampered. Verify again — the old signature should fail.', 'bad');
}
$('sign').addEventListener('click', sign);
$('verify').addEventListener('click', verify);
$('tamper').addEventListener('click', tamper);
<p class="hint">A digital signature proves a message came from you <b>and</b> was not
altered — without ever revealing your private key. Sign the message with the
<b>private key</b>, then verify with the matching <b>public key</b>. Change a single
character and watch verification fail.</p>
<label class="lbl">Message</label>
<textarea id="msg" rows="2">Transfer $100 to Ana</textarea>
<div class="btns">
<button id="sign" type="button">Sign with private key</button>
<button id="verify" type="button">Verify with public key</button>
<button id="tamper" type="button" class="ghost">Tamper (flip 1 char)</button>
</div>
<div class="row"><span class="tag">Signature</span><code id="sig">—</code></div>
<div class="status" id="status">Sign the message, then verify.</div>
<div class="keys">
<span>public key (e,n) = (<b id="pe">17</b>, <b id="pn">3233</b>)</span>
<span class="secret">private key d = ••• (never shared)</span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.lbl { font: 600 .8rem system-ui; color: #1d3557; display: block; margin-bottom: .25rem; }
textarea { width: 100%; font: 500 14px ui-monospace, monospace; padding: .5rem;
border: 1px solid #cdd9e3; border-radius: 8px; resize: vertical; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .7rem 0; }
.row { display: flex; align-items: center; gap: .5rem; margin: .4rem 0; }
.tag { font: 600 .72rem system-ui; background: #e8eef3; color: #1d3557;
padding: .15rem .5rem; border-radius: 6px; }
code { font: 600 13px ui-monospace, monospace; color: #1d3557; word-break: break-all; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.keys { font: 500 .8rem ui-monospace, monospace; color: #555; display: flex;
flex-direction: column; gap: .2rem; margin-top: .6rem; }
.keys .secret { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// A textbook RSA keypair (tiny, for illustration only — real keys are 2048+ bits).
// n = p*q = 61*53 = 3233, phi = 60*52 = 3120, public e = 17, private d = 2753.
// Signing: s = (hash(message))^d mod n — needs the PRIVATE key d.
// Verifying: recompute hash, check (s)^e mod n == hash — needs only the PUBLIC key e.
// Anyone can verify; only the holder of d can produce a signature that verifies.
const n = 3233, e = 17, d = 2753;
function modpow(base, exp, mod){ // (base^exp) mod mod, no big-int overflow
let r = 1; base %= mod;
while(exp > 0){ if(exp & 1) r = (r * base) % mod; exp >>= 1; base = (base * base) % mod; }
return r;
}
// Simple deterministic hash of the message into the range [1, n-1].
function hash(str){
let h = 7;
for(let i = 0; i < str.length; i++){ h = (h * 31 + str.charCodeAt(i)) % n; }
return h === 0 ? 1 : h; // avoid the fixed point 0
}
let signature = null; // the signature is bound to the message that was signed
const $ = id => document.getElementById(id);
function setStatus(t, cls){ const s = $('status'); s.textContent = t; s.className = 'status ' + cls; }
function sign(){
const m = $('msg').value;
signature = modpow(hash(m), d, n); // uses the PRIVATE key
$('sig').textContent = signature;
setStatus('Signed. The signature is bound to this exact message.', '');
}
function verify(){
if(signature === null){ setStatus('Nothing signed yet — sign first.', 'bad'); return; }
const m = $('msg').value;
const recovered = modpow(signature, e, n); // uses only the PUBLIC key
if(recovered === hash(m)) setStatus('✓ Valid: this message really was signed with the matching private key.', 'ok');
else setStatus('✗ Invalid: the message was altered or the signature does not match.', 'bad');
}
function tamper(){
const t = $('msg'); const s = t.value;
if(!s.length) return;
const i = Math.floor(s.length / 2);
const c = s[i] === '0' ? '9' : (s[i] === '9' ? '0' : (s.charCodeAt(i) === 65 ? 66 : 65));
const ch = typeof c === 'number' ? String.fromCharCode(c) : c;
t.value = s.slice(0, i) + ch + s.slice(i + 1);
setStatus('Message tampered. Verify again — the old signature should fail.', 'bad');
}
$('sign').addEventListener('click', sign);
$('verify').addEventListener('click', verify);
$('tamper').addEventListener('click', tamper);
Now press Tamper to flip a single character, and verify again. The signature was bound to the exact original message, so the check fails immediately. Notice the asymmetry once more: signing used the secret key, but verifying — and detecting the tampering — needed only the public one. Real keys are 2048 bits or larger; the principle is identical. The hardness underneath is the same one explored in factoring.
Comments
Loading comments...