ECDSAの署名からどうして公開鍵が取得できるの?という聞かれて何でだろうねって話になったので調べてみた。
署名から公開鍵のセットを復元
Rubyではecdsa
のgemを使えば以下のように署名と署名対象のメッセージから公開鍵を取得できる。(署名対象のメッセージはどうせなのでBitcoinで使用されているのと同じトランザクションのsighashを使用。そのためbitcoinrb
のgemも利用している)
実行すると↓のように2つの公開鍵が抽出できる。
0259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520 03a9255e098f4075e9ebfaf7859f459095667879db33e977fc8c91029afa8a2490
↑のコード上の署名はこの内の0259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520
に対応する秘密鍵で行ったもの。
署名とメッセージから公開鍵を復元する際の計算
ecdsa
のgemで署名とメッセージから公開鍵を計算しているのがrecovery_public_key.rb
の↓の部分。
module ECDSA ... def self.recover_public_key(group, digest, signature) return enum_for(:recover_public_key, group, digest, signature) if !block_given? digest = normalize_digest(digest, group.bit_length) each_possible_temporary_public_key(group, digest, signature) do |point| yield calculate_public_key(group, digest, signature, point) end nil end private def self.each_possible_temporary_public_key(group, digest, signature) signature.r.step(group.field.prime - 1, group.order) do |x| group.solve_for_y(x).each do |y| point = group.new_point [x, y] yield point if point.multiply_by_scalar(group.order).infinity? end end end def self.calculate_public_key(group, digest, signature, temporary_public_key) point_field = PrimeField.new(group.order) # public key = (tempPubKey * s - G * e) / r rs = temporary_public_key.multiply_by_scalar(signature.s) ge = group.generator.multiply_by_scalar(digest) r_inv = point_field.inverse(signature.r) rs.add_to_point(ge.negate).multiply_by_scalar(r_inv) end end
ここでは以下の計算をしている。
- 署名データ
(r, s)
のr
を楕円曲線のx座標として、y座標を計算する。
secp256k1の楕円曲線の多項式はy2 = x3 + 7なので、rの値をxに入れればyを計算できる。楕円曲線の平面曲線はx軸に対して対称なのでyの取りうる値はx軸を対称に2つある(↑のrecover_public_keyを呼んで2つの公開鍵が返ってきたのはこのため)。ここで計算した2つの点について2以降の計算をして2つの公開鍵を導出する。 - rと1で計算したyから[r, y]となる楕円曲線上の点を計算する。
- 2で計算した点に
(r, s)
のs
を掛けた点rs
を計算する。 - secp256k1のグループGの生成点にメッセージを掛けた点
ge
を計算する。 - 3で計算した点
rs
に4で計算した点ge
を減算して出来た点にを掛けた点を計算する。
5で計算した点が復元した公開鍵になる。
もう少し簡略化してみよう。署名データを(r, s)
、1でr
を使って計算した2つの点をRとR'とし、メッセージのハッシュ値をe
、secp256k1の楕円曲線の生成点をGとした場合、算出している公開鍵は以下の2つだ。
補足
sec1-v2.pdfの4.1.6章では、Rのx座標としてx = r + jN(j=0~h)となるようでした。
— nayuta-ueno (@nayuta_ueno) 2018年1月22日
secp256k1ではh=1なので、(r, y), (r, -y), (r+N, y), (r+N, -y)の4種類になりそうです。
(すみません、数学的なことはわかってないです。。)
↑の1の計算についてコメントもらったので追記。
1でrをx座標として2つのRとR'を計算すると書いたが、楕円曲線のペーパーには、このx = r + jn
でx座標を求めるようになっている。ここでj
はの集合で
h
は各楕円曲線毎に定義されており、secp256k1の場合h = 1
だ。つまりx座標の候補はr
とr + n
の2つになる(n
はグループの位数)。r + n
がグループの剰余系の素数を超えないようになってるので、実際にその範囲内のr + n
が候補に上がるのは、r
が剰余系の素数 - 1 - n
以下の場合。最初の復元コードの出力結果が2つの公開鍵だったのはr + n
がこの範囲外で、r
のみの計算となったため。
なぜこれが公開鍵になるのか?
という疑問が当然わくが、これはECDSAの署名生成と署名検証時のロジックからそれっぽい理由が分かる↓(数学的にはもっとちゃんとした解説があると思う)
秘密鍵をp
、その公開鍵をQ = pG
とし、メッセージのハッシュ値e
へのECDSAの署名データ(r, s)
は以下のようにして計算される。
- 最初にランダムな値kを選択する。
- kを使って楕円曲線上の点=
kG = (x, y)
を計算し、そのx座標をr
とする。 を計算する。
- 算出した
(r, s)
が署名データとなる。
そしてその署名データが正しいかどうかは以下のようにして判定される。
となる点Rを計算し、Rのx座標がr
と同じであれば署名は正しい。
この検証に使用した式を公開鍵Qを求めるように展開すると
署名が正しい場合Rのx座標はr
と等しいということを思い出そう。公開鍵の復元に使用したR
とR'
のx座標はいずれもr
で、公開鍵による検証の際に使用した楕円曲線上の点と合致する。
このことから(r, s)
およびそのメッセージe
から計算したおよび
が公開鍵と言える。