決定論的署名（Deterministic signature）

# はじめに

署名についてのセキュリティを調べていたところ、決定論的署名を行うことが重要であるものの
サンプル実装などで案外解説されていないので、解説していこうと思います。

# 決定論的署名

ECDSAなどの署名や検証において、必要なのは、秘密鍵、公開鍵、メッセージ、**ナンス**の４つが必要です。
この**ナンス**を適切に設定しないと秘密鍵が漏れてしまいます。
ある秘密鍵において同じ**ナンス**を使用すると署名値から秘密鍵が判明してしまいます。
そこで、**ナンス**を**ランダム**にする必要があります。

しかし、各プログラミング言語で実装されているランダム関数などは大丈夫なのでしょうか？
簡単なものですと、シード（seed）を設定するのに、時間を使ったりします。
なんらかの手段で時間をリセットしたりすると同じ乱数を発生するかもしれません。
実際にPS3ではナンスを同じにしていた為（実装上の不具合）、秘密鍵が漏れてしまいました。

そこで、ECDSAでは、[RFC 6979](https://tools.ietf.org/html/rfc6979)というものが定義されています。
[楕円曲線DSA - Wikipedia](https://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E6%9B%B2%E7%B7%9ADSA#%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3)のセキュリティにも重要性が記載されています。

簡単に言うと、秘密鍵とメッセージから、**ランダム**に見える**ナンス**を生成し署名する仕組みです。

# 悪い例

本当に秘密鍵が漏れてしまうのかを、検証してみたいと思います。
まずは、ECDSAの[仕様](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf)をおさらいします。

署名側と検証側が知っている情報についてです。
大文字は楕円曲線上の点、小文字は数値、ただし$m$はメッセージとします。

署名側が知っている情報

```math
\begin{align*}
x &\text{ : 秘密鍵} \\
P &\text{ : 公開鍵} (P=xG) \\
k &\text{ : ナンス} \\
R &\text{ : ナンスポイント} (R=kG) \\
m &\text{ : メッセージ} \\
H(\cdot)&\text{ : ハッシュ関数} \\
\end{align*}
```

検証側が知っている情報

```math
\begin{align*}
P &\text{ : 署名-公開鍵} \\
m &\text{ : メッセージ} \\
H(\cdot)&\text{ : ハッシュ関数} \\
\end{align*}
```

## ECDSA

ECDSAを見てみます。

### 署名

最終的に、$(r,s)$を検証側に知らせます。

```math
\begin{align*}
&e = \text{H}(m) \\
&R = kG \\
&R : (x_R,y_R) \\
&r = x_R \\
&s = \frac{e+xr}{k} \\
&(r,s)\text{ : 署名値} \\
\end{align*}
```

### 検証

$(r,s)$を受け取ったら以下の手順で検証します。

```math
\begin{align*}
&e = \text{H}(m) \\
&w = \frac{1}{s} \\
&u_1 = ew \\
&u_2 = rw \\
&Q = u_1G + u_2P \\
&Q : (x_Q,y_Q) \\
&v = x_Q \\
&v \overset{?}{=} r \\
\end{align*}
```

## 秘密鍵の算出（ナンスが同じ場合）

ナンスを同じにした場合にどのようにして署名側の秘密鍵を算出するのかを見てみます。
Pythonのecdsaを使ってコードでも検証してみます。

[サンプル](https://colab.research.google.com/gist/tnakagawa/bba0cba50928955fce22cee5d3f045fd/deterministicsignature.ipynb)を作ったので、興味のある方は使ってみてください。

### 署名

署名者は、異なるメッセージ$m_1,m_2$を同じナンス$k$で署名します。

```math
\begin{align*}
&R = kG \\
&R : (x_R,y_R) \\
&r = x_R \\
&e_1 = \text{H}(m_1) \\
&s_1 = \frac{e_1+xr}{k} \\
&(r,s_1)\text{ : 署名値} \\
&e_2 = \text{H}(m_2) \\
&s_2 = \frac{e_2+xr}{k} \\
&(r,s_2)\text{ : 署名値} \\
\end{align*}
```
```py
from hashlib import sha256
from ecdsa import ecdsa
import random

g = ecdsa.generator_secp256k1
n = g.order()

x = random.randint(1, n-1)
pub = ecdsa.Public_key(g, x*g)
pri = ecdsa.Private_key(pub, x)
k = random.randint(1, n-1)
nonce = ecdsa.Public_key(g, k*g)
m1 = 'hello'  # @param {type:"string"}
m2 = 'こんにちは'  # @param {type:"string"}

print('\033[34m'+'秘密鍵(x)：0x{:x}'.format(x)+'\033[0m')
print('公開鍵(P)：(0x{:x},0x{:x})'.format(pub.point.x(), pub.point.y()))
print('\033[35m'+'ナンス(k)：0x{:x}'.format(k)+'\033[0m')
print('ナンスポイント(R)：(0x{:x},0x{:x})'.format(nonce.point.x(), nonce.point.y()))
print('メッセージ１：{}'.format(m1))
print('メッセージ２：{}'.format(m2))

e1 = int.from_bytes(sha256(m1.encode('utf-8')).digest(), 'big')
sig1 = pri.sign(e1, k)

e2 = int.from_bytes(sha256(m2.encode('utf-8')).digest(), 'big')
sig2 = pri.sign(e2, k)

r1 = sig1.r
r2 = sig2.r
s1 = sig1.s
s2 = sig2.s
print('署名値１(r,s1)=(0x{:x},0x{:x})'.format(r1, s1))
print('署名値２(r,s2)=(0x{:x},0x{:x})'.format(r2, s2))
```

### 検証

検証側は、$(r,s_1),(r,s_2)$を受け取って以下の手順で署名者の秘密鍵$x$と求めます。
最初にナンス$k$を求め、署名値から秘密鍵$x$を算出します。

```math
\begin{align*}
&e_1 = \text{H}(m_1) \\
&e_2 = \text{H}(m_2) \\
&k = \frac{e_1-e_2}{s_1-s_2} \\
&x = \frac{s_1 \cdot k - e_1}{r} \\
\end{align*}
```

```py
e1 = int.from_bytes(sha256(m1.encode('utf-8')).digest(), 'big')
e2 = int.from_bytes(sha256(m2.encode('utf-8')).digest(), 'big')
print('署名値1の検証結果：{}'.format(pub.verifies(e1, sig1)))
print('署名値2の検証結果：{}'.format(pub.verifies(e2, sig2)))

if (r1 == r2):
    print('ナンスポイントが一致したので、ナンス(k)と秘密鍵(x)を算出します。')
    k = (((e1 - e2) % n) * pow(((s1 - s2) % n), n-2, n)) % n
    print('\033[35m'+'ナンス(k)：0x{:x}'.format(k)+'\033[0m')
    r = r1
    x = (((((s1 * k) % n) - e1) % n)*pow(r, n-2, n)) % n
    print('\033[34m'+'秘密鍵(x)：0x{:x}'.format(x)+'\033[0m')
else:
    print('ナンスポイントが一致しません')
```

# RFC6979

もちろん、PythonのコードにもRFC6979は実装されていますので、サンプルコードの乱数を使わないでこちらを利用した方が望ましいです。

```py
from ecdsa import rfc6979

msg = sha256(m1.encode('utf-8')).digest()
rfc6979_k = rfc6979.generate_k(n, pri.secret_multiplier, sha256, msg)
print('メッセージ「{}」のナンス(k)：0x{:x}'.format(m1, rfc6979_k))
```

# さいごに

ナンスを使う署名全般に言えますが、ナンスは大事です。
しかし、ナンスを単なる乱数として考えない場合はナンスの生成方法を考える必要があります。

[暗号論的擬似乱数生成器](https://ja.wikipedia.org/wiki/%E6%9A%97%E5%8F%B7%E8%AB%96%E7%9A%84%E6%93%AC%E4%BC%BC%E4%B9%B1%E6%95%B0%E7%94%9F%E6%88%90%E5%99%A8)とか調べないといけないなぁ・・・どんどん、脱線していく・・・