Back to blog

受理された署名は、何も証明していない――Schnorr と nonce 再利用

zk-tokyo Advanced Cryptography Program 2026 の Week 3 で楕円曲線と Schnorr プロトコルを扱いました。秘密鍵を一度も使わずに作った会話が検証に通ること、nonce を 1 回使い回すだけで secp256k1 の秘密鍵が復元されることを、実際に走らせた結果つきでまとめます。

zk-tokyo のAdvanced Cryptography Program 2026を受講しています。Week 3 のテーマは楕円曲線と Schnorr プロトコルで、課題schnorr-from-scratchでは有限体の逆元から始めて楕円曲線の群演算を組み、その上にシグマプロトコルと Fiat-Shamir 変換を積んで、最後は Bitcoin と同じ曲線 secp256k1 上で署名を作りました。

今回の収穫は、検証に通ったという事実そのものには、何の証明力もないと体で理解できたことです。

3 手のやりとり

公開鍵P = xGについて「秘密鍵 x を知っている」ことを示すプロトコルは、たった 3 手です。証明者がランダムなrを選んでR = rGを送り、検証者がランダムなチャレンジeを返し、証明者がs = r + e·xを返す。検証者はsG = R + ePを確かめます。

正直に計算したsなら検証式は必ず成り立ちます。展開すればsG = (r + e·x)G = rG + e·(xG) = R + ePで、たった 1 行です。

秘密鍵を使わずに、同じ会話を作る

ところが、この会話は秘密鍵なしでも作れます。順番を逆にするだけです。先にesをランダムに決めておき、検証式から逆算してR := sG - ePと置く。すると検証式は定義上成り立ちます。

自習ノートに載せたラボで実際に走らせると、こうなります。

x を使わずに作った会話:
  (R, e, s) = ((146, 214), 190, 284)
検証者はこれを受理するか: True

つまり「受理された記録が 1 本ある」ことは、秘密を知っている証拠になりません。本物との違いは中身ではなく順番だけです。本物はRを先に出し、そのあとでチャレンジを受け取る。偽物はチャレンジを知ってからRを作る。

証明者が先にコミットし、チャレンジを後から受け取るという順番が、健全性の全部を担いでいます。ここが今週いちばん腑に落ちた部分でした。

抽出器と攻撃は、同じ 1 本の式

では「知っている」ことは何で保証されるのか。使う道具は抽出器と呼ばれるもので、証明者を巻き戻して同じ R のまま別のチャレンジを投げます。2 つの応答が得られたなら、そこから秘密鍵を計算して取り出せる。取り出せた以上、秘密鍵は最初から証明者の中にあった、という論法です。

s1 = r + e1·x
s2 = r + e2·x
s1 - s2 = (e1 - e2)·x        ← r が消える
x = (s1 - s2)·(e1 - e2)^-1

この式は、そのまま nonce 再利用攻撃の中身でもあります。安全性の根拠と攻撃手順が同じ 1 本の式である、というのがきれいでした。

本物の曲線でも、署名 2 本で鍵が出る

トイの曲線だからではありません。課題の実装で、secp256k1 上の 256 ビットの秘密鍵に対して、同じ nonce で 2 回署名した結果です。

署名1: R.x = 0xb44cf0c87f8f960c...  s1 = 0x28807ada8ee210bd...
署名2: R.x = 0xb44cf0c87f8f960c...  s2 = 0x24da65e7523016c0...
R が一致している: True

復元した x = 0x1b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
本物の  x = 0x1b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
一致: True

攻撃者が使ったのは公開値だけです。しかもRが一致しているかどうかは誰でも見えるので、再利用が起きたこと自体が公開情報から分かります。乱数を毎回変えるのは運用上の注意ではなく、安全性の前提そのものでした。

壊して分かった、実装の細部

課題は Part 1 から Part 3 まで一本につながっていて、下が間違っていると上が動きません。わざと壊して採点器に通すと、失敗の形がそれぞれ違うのが面白いところでした。

壊し方失敗の形
逆元を 0..p-1 に正規化しない値は正しいのに「正規化して返してください」で落ちる
y 座標の符号を逆にする点が曲線から外れる。is_on_curveが即座に捕まえる
スカラー倍を素朴に k 回足す落ちるのではなく終わらない。約 3.0×10^63 年かかる
スカラーの mod を n でなく p にする値が法より小さいケースだけ通り、それ以外で失敗する
ハッシュの引数順を入れ替える自分の署名は自分で検証できてしまう

最後の 2 つが特に厄介です。どちらも自己完結したテストなら通ってしまう種類の間違いで、外部の期待値と突き合わせて初めて表面化します。

持ち帰った問い

Week 1 の問いは「この信号を縛っている式はどれか」、Week 2 は「この値を隠している乱数は何か」でした。Week 3 ではこうなります。この値を隠しているのは何で、その条件は何本あるか

noncerがランダムであること、毎回違うこと、ハッシュに正しい文脈が入っていること。1 本でも欠ければ、静かに壊れます。Beaver triple を使い回した瞬間、OT の乱数範囲から 0 を除いた瞬間、そして nonce を再利用した瞬間。3 週続けて同じ教訓を別の形で見ました。

自習ノートを公開しました

知っていることだけを、渡す

単一の HTML ファイルで、外部への通信はありません。正直な証明者と simulator を切り替えて 3 手を回せるラボと、nonce 再利用から秘密鍵を復元する攻撃ラボが付いています。検問クイズは有限体・楕円曲線・レスポンス計算・受理判定・鍵復元・原理の 6 分野から毎回その場で出題され、判定はラボと同じ計算器が行います。