<p class="hint">Tiny ECDSA over <b>y² ≡ x³ + 2x + 2 (mod 17)</b>, generator G=(5,1), order n=19.
Two messages signed with the <em>same</em> nonce k — watch one formula recover the private key.</p>
<div class="scenario" id="scenario">
<div class="params" id="params"></div>
</div>
<div class="btns">
<button id="btn-reveal" type="button">Reveal k and recover private key d</button>
<button id="btn-new" type="button" class="ghost">New random scenario</button>
</div>
<div class="result" id="result"></div>
<div class="steps" id="steps"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.params { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px; padding: .7rem 1rem; font-size: .88rem; line-height: 1.8; }
.params b { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .8rem 0; }
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; }
.result { font-weight: 700; font-size: 1rem; margin-bottom: .5rem; min-height: 1.4em; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.steps { font-size: .83rem; color: #333; line-height: 2; font-family: ui-monospace, monospace; background: #f8f9fa; border-radius: 8px; padding: .6rem .9rem; display: none; }
.steps.show { display: block; }
.label { color: #888; margin-right: .3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ── Tiny elliptic curve: y² ≡ x³ + 2x + 2 (mod 17) ──────────────────────
// Points (precomputed multiples of G=(5,1), order n=19):
// This is a standard toy curve used in textbooks (order-19 subgroup).
const p = 17, a = 2, n = 19;
const G = { x: 5, y: 1 };
function mod(x, m) { return ((x % m) + m) % m; }
function modInv(a, m) {
// Extended Euclidean algorithm
let [old_r, r] = [a, m], [old_s, s] = [1, 0];
while (r !== 0) { const q = Math.floor(old_r / r); [old_r, r] = [r, old_r - q * r]; [old_s, s] = [s, old_s - q * s]; }
return mod(old_s, m);
}
function pointAdd(P, Q) {
if (!P) return Q; if (!Q) return P;
if (P.x === Q.x && P.y === Q.y) {
// Point doubling
const lam = mod(mod(3 * P.x * P.x + a, p) * modInv(mod(2 * P.y, p), p), p);
const x3 = mod(lam * lam - 2 * P.x, p);
return { x: x3, y: mod(lam * (P.x - x3) - P.y, p) };
}
if (P.x === Q.x) return null; // point at infinity
const lam = mod(mod(Q.y - P.y, p) * modInv(mod(Q.x - P.x, p), p), p);
const x3 = mod(lam * lam - P.x - Q.x, p);
return { x: x3, y: mod(lam * (P.x - x3) - P.y, p) };
}
function scalarMul(k, P) {
let R = null, Q = P, exp = ((k % n) + n) % n;
while (exp > 0) { if (exp & 1) R = pointAdd(R, Q); Q = pointAdd(Q, Q); exp >>= 1; }
return R;
}
// ── Precompute all valid scalar multiples of G (1..n-1) ──────────────────
const POINTS = [];
for (let i = 1; i < n; i++) POINTS.push({ k: i, pt: scalarMul(i, G) });
// ── ECDSA sign (returns {r,s} or null if r==0) ────────────────────────────
function sign(h, d, k) {
const R = scalarMul(k, G);
const r = mod(R.x, n); if (r === 0) return null;
const s = mod(modInv(k, n) * mod(h + d * r, n), n); if (s === 0) return null;
return { r, s };
}
// ── Generate a random scenario ────────────────────────────────────────────
function randInt(lo, hi) { return lo + Math.floor(Math.random() * (hi - lo + 1)); }
let scenario = null;
function newScenario() {
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
document.getElementById('steps').className = 'steps';
document.getElementById('steps').innerHTML = '';
document.getElementById('btn-reveal').disabled = false;
let d, k, h1, h2, sig1, sig2;
do {
d = randInt(2, n - 2); // private key
k = randInt(2, n - 2); // REUSED nonce
h1 = randInt(1, n - 1); // hash of message 1
h2 = randInt(1, n - 1); // hash of message 2 (different)
if (h1 === h2) continue;
sig1 = sign(h1, d, k);
sig2 = sign(h2, d, k);
} while (!sig1 || !sig2 || sig1.r !== sig2.r);
const Q = scalarMul(d, G);
scenario = { d, k, h1, h2, sig1, sig2, Q };
const params = document.getElementById('params');
params.innerHTML =
fmt('<b>Curve:</b> y² ≡ x³ + 2x + 2 (mod 17), G=(5,1), order n=19<br><b>Public key Q:</b> (%x%, %y%)<br><b>Signature 1</b> on h₁=%h1%: r=%r%, s₁=%s%<br><b>Signature 2</b> on h₂=%h2%: r=%r2%, s₂=%s2%<br><span style="color:#c92f3c;font-size:.82rem">⚠ Both signatures share the same r — same nonce k was used!</span>', { x: Q.x, y: Q.y, h1: h1, r: sig1.r, s: sig1.s, h2: h2, r2: sig2.r, s2: sig2.s });
}
// ── Key recovery from nonce reuse ─────────────────────────────────────────────
function recover() {
if (!scenario) return;
const { d, k: trueK, h1, h2, sig1, sig2, Q } = scenario;
// k = (h1 - h2) / (s1 - s2) mod n
const num_k = mod(h1 - h2, n);
const den_k = mod(sig1.s - sig2.s, n);
const kRec = mod(num_k * modInv(den_k, n), n);
// d = (s1*k - h1) / r mod n
const num_d = mod(sig1.s * kRec - h1, n);
const dRec = mod(num_d * modInv(sig1.r, n), n);
// Verify
const Qcheck = scalarMul(dRec, G);
const correct = dRec === d && Qcheck && Qcheck.x === Q.x && Qcheck.y === Q.y;
const res = document.getElementById('result');
res.className = 'result ' + (correct ? 'ok' : 'bad');
res.textContent = correct
? fmt('✓ Private key recovered: d = %dRec% (secret key was %trueK% — exposed!)', { dRec: dRec, trueK: trueK })
: '✗ Recovery failed (unexpected — check the math).';
const steps = document.getElementById('steps');
steps.className = 'steps show';
steps.innerHTML =
fmt('<span class="label">Step 1 – recover k:</span>k = (h₁ − h₂) · (s₁ − s₂)⁻¹ mod n<br> = (%h1% − %h2%) · (%s% − %s2%)⁻¹ mod %n%<br> = %num_k% · %modInv% mod %n%<br> = <b>%kRec%</b><br><br><span class="label">Step 2 – recover d:</span>d = (s₁·k − h₁) · r⁻¹ mod n<br> = (%s%·%kRec% − %h1%) · %modInv2% mod %n%<br> = %num_d% · %modInv2% mod %n%<br> = <b>%dRec%</b><br><br><span class="label">Verify:</span> d·G = (%v%, %v2%) = Q = (%x%, %y%) ✓', { h1: h1, h2: h2, s: sig1.s, s2: sig2.s, n: n, num_k: num_k, modInv: modInv(den_k, n), kRec: kRec, modInv2: modInv(sig1.r, n), num_d: num_d, dRec: dRec, v: Qcheck ? Qcheck.x : '?', v2: Qcheck ? Qcheck.y : '?', x: Q.x, y: Q.y });
document.getElementById('btn-reveal').disabled = true;
console.log('True d:', d, '| Recovered d:', dRec, '| True k:', trueK, '| Recovered k:', kRec);
}
document.getElementById('btn-reveal').addEventListener('click', recover);
document.getElementById('btn-new').addEventListener('click', newScenario);
newScenario();
<p class="hint">Tiny ECDSA over <b>y² ≡ x³ + 2x + 2 (mod 17)</b>, generator G=(5,1), order n=19.
Two messages signed with the <em>same</em> nonce k — watch one formula recover the private key.</p>
<div class="scenario" id="scenario">
<div class="params" id="params"></div>
</div>
<div class="btns">
<button id="btn-reveal" type="button">Reveal k and recover private key d</button>
<button id="btn-new" type="button" class="ghost">New random scenario</button>
</div>
<div class="result" id="result"></div>
<div class="steps" id="steps"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.params { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px; padding: .7rem 1rem; font-size: .88rem; line-height: 1.8; }
.params b { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .8rem 0; }
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; }
.result { font-weight: 700; font-size: 1rem; margin-bottom: .5rem; min-height: 1.4em; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.steps { font-size: .83rem; color: #333; line-height: 2; font-family: ui-monospace, monospace; background: #f8f9fa; border-radius: 8px; padding: .6rem .9rem; display: none; }
.steps.show { display: block; }
.label { color: #888; margin-right: .3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ── Tiny elliptic curve: y² ≡ x³ + 2x + 2 (mod 17) ──────────────────────
// Points (precomputed multiples of G=(5,1), order n=19):
// This is a standard toy curve used in textbooks (order-19 subgroup).
const p = 17, a = 2, n = 19;
const G = { x: 5, y: 1 };
function mod(x, m) { return ((x % m) + m) % m; }
function modInv(a, m) {
// Extended Euclidean algorithm
let [old_r, r] = [a, m], [old_s, s] = [1, 0];
while (r !== 0) { const q = Math.floor(old_r / r); [old_r, r] = [r, old_r - q * r]; [old_s, s] = [s, old_s - q * s]; }
return mod(old_s, m);
}
function pointAdd(P, Q) {
if (!P) return Q; if (!Q) return P;
if (P.x === Q.x && P.y === Q.y) {
// Point doubling
const lam = mod(mod(3 * P.x * P.x + a, p) * modInv(mod(2 * P.y, p), p), p);
const x3 = mod(lam * lam - 2 * P.x, p);
return { x: x3, y: mod(lam * (P.x - x3) - P.y, p) };
}
if (P.x === Q.x) return null; // point at infinity
const lam = mod(mod(Q.y - P.y, p) * modInv(mod(Q.x - P.x, p), p), p);
const x3 = mod(lam * lam - P.x - Q.x, p);
return { x: x3, y: mod(lam * (P.x - x3) - P.y, p) };
}
function scalarMul(k, P) {
let R = null, Q = P, exp = ((k % n) + n) % n;
while (exp > 0) { if (exp & 1) R = pointAdd(R, Q); Q = pointAdd(Q, Q); exp >>= 1; }
return R;
}
// ── Precompute all valid scalar multiples of G (1..n-1) ──────────────────
const POINTS = [];
for (let i = 1; i < n; i++) POINTS.push({ k: i, pt: scalarMul(i, G) });
// ── ECDSA sign (returns {r,s} or null if r==0) ────────────────────────────
function sign(h, d, k) {
const R = scalarMul(k, G);
const r = mod(R.x, n); if (r === 0) return null;
const s = mod(modInv(k, n) * mod(h + d * r, n), n); if (s === 0) return null;
return { r, s };
}
// ── Generate a random scenario ────────────────────────────────────────────
function randInt(lo, hi) { return lo + Math.floor(Math.random() * (hi - lo + 1)); }
let scenario = null;
function newScenario() {
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
document.getElementById('steps').className = 'steps';
document.getElementById('steps').innerHTML = '';
document.getElementById('btn-reveal').disabled = false;
let d, k, h1, h2, sig1, sig2;
do {
d = randInt(2, n - 2); // private key
k = randInt(2, n - 2); // REUSED nonce
h1 = randInt(1, n - 1); // hash of message 1
h2 = randInt(1, n - 1); // hash of message 2 (different)
if (h1 === h2) continue;
sig1 = sign(h1, d, k);
sig2 = sign(h2, d, k);
} while (!sig1 || !sig2 || sig1.r !== sig2.r);
const Q = scalarMul(d, G);
scenario = { d, k, h1, h2, sig1, sig2, Q };
const params = document.getElementById('params');
params.innerHTML =
fmt('<b>Curve:</b> y² ≡ x³ + 2x + 2 (mod 17), G=(5,1), order n=19<br><b>Public key Q:</b> (%x%, %y%)<br><b>Signature 1</b> on h₁=%h1%: r=%r%, s₁=%s%<br><b>Signature 2</b> on h₂=%h2%: r=%r2%, s₂=%s2%<br><span style="color:#c92f3c;font-size:.82rem">⚠ Both signatures share the same r — same nonce k was used!</span>', { x: Q.x, y: Q.y, h1: h1, r: sig1.r, s: sig1.s, h2: h2, r2: sig2.r, s2: sig2.s });
}
// ── Key recovery from nonce reuse ─────────────────────────────────────────────
function recover() {
if (!scenario) return;
const { d, k: trueK, h1, h2, sig1, sig2, Q } = scenario;
// k = (h1 - h2) / (s1 - s2) mod n
const num_k = mod(h1 - h2, n);
const den_k = mod(sig1.s - sig2.s, n);
const kRec = mod(num_k * modInv(den_k, n), n);
// d = (s1*k - h1) / r mod n
const num_d = mod(sig1.s * kRec - h1, n);
const dRec = mod(num_d * modInv(sig1.r, n), n);
// Verify
const Qcheck = scalarMul(dRec, G);
const correct = dRec === d && Qcheck && Qcheck.x === Q.x && Qcheck.y === Q.y;
const res = document.getElementById('result');
res.className = 'result ' + (correct ? 'ok' : 'bad');
res.textContent = correct
? fmt('✓ Private key recovered: d = %dRec% (secret key was %trueK% — exposed!)', { dRec: dRec, trueK: trueK })
: '✗ Recovery failed (unexpected — check the math).';
const steps = document.getElementById('steps');
steps.className = 'steps show';
steps.innerHTML =
fmt('<span class="label">Step 1 – recover k:</span>k = (h₁ − h₂) · (s₁ − s₂)⁻¹ mod n<br> = (%h1% − %h2%) · (%s% − %s2%)⁻¹ mod %n%<br> = %num_k% · %modInv% mod %n%<br> = <b>%kRec%</b><br><br><span class="label">Step 2 – recover d:</span>d = (s₁·k − h₁) · r⁻¹ mod n<br> = (%s%·%kRec% − %h1%) · %modInv2% mod %n%<br> = %num_d% · %modInv2% mod %n%<br> = <b>%dRec%</b><br><br><span class="label">Verify:</span> d·G = (%v%, %v2%) = Q = (%x%, %y%) ✓', { h1: h1, h2: h2, s: sig1.s, s2: sig2.s, n: n, num_k: num_k, modInv: modInv(den_k, n), kRec: kRec, modInv2: modInv(sig1.r, n), num_d: num_d, dRec: dRec, v: Qcheck ? Qcheck.x : '?', v2: Qcheck ? Qcheck.y : '?', x: Q.x, y: Q.y });
document.getElementById('btn-reveal').disabled = true;
console.log('True d:', d, '| Recovered d:', dRec, '| True k:', trueK, '| Recovered k:', kRec);
}
document.getElementById('btn-reveal').addEventListener('click', recover);
document.getElementById('btn-new').addEventListener('click', newScenario);
newScenario();
Comments
Loading comments...