楕円曲線暗号のPythonによる実装その1(有限体とECDH鍵共有)
お断り
この記事は『Software Design2022年3月号』の「第4章:電子署名のプロセスを体験 Pythonによる楕円曲線暗号の実装」の入稿記事を技術評論社のご好意で公開したものです。
元はLaTeXだったのをマークダウンに修正し、二つに分けています。
記事中のサンプルコードはサポートページからダウンロードできます。
はじめに
この章では楕円曲線を用いた鍵共有や署名をPythonで実装します。実装するために必要な数学は随時解説します。
動作確認はPython 3.8.10で行いました。
コードは動作原理を理解するためのものであり、細かいエラー処理などはしていません。
プロダクト製品などで利用できるものではないことをご了承ください。
用語のおさらい
楕円曲線暗号の位置づけ
まず最初に用語の確認をします。
「暗号」は複数の意味で使われます。
一つは「データを秘匿化するために、他人に読めない形にする暗号化(Encryption)」です。
もう一つは、暗号化だけでなく、鍵共有や署名などの真正性や認証などを含む暗号技術全般を指す暗号(Cryptography)です。
同様に公開鍵暗号も複数の意味で使われ、「公開鍵を使って暗号化し、秘密鍵で復号する」公開鍵暗号PKE(Public Key Encryption)や
「公開情報と秘密情報を組み合わせた暗号技術全般」を指す公開鍵暗号PKC(Public Key Cryptography)などがあります。
両者を区別したいときはPKEを公開鍵暗号方式、PKCを公開鍵暗号技術などということがありますが、確定しているとはいえません。
楕円曲線暗号は英語ではECC(Elliptic Curve Cryptography)といい、楕円曲線EC(Elliptic Curve)を使った暗号技術全般を指します。
TLSやSSH、FIDO2、ブロックチェーンなどでは楕円曲線を使った鍵共有や署名が使われます。
暗号技術の分類(『暗号と認証のしくみと理論がこれ1冊でしっかりわかる教科書』p.168より引用)
意外かもしれませんが身近なところで楕円曲線が「暗号化」を意味する公開鍵暗号PKEで使われることはほとんどありません。
CRYPTREC(CRYPTography Research and Evaluation Committees)が提示している電子政府推奨暗号リストを見ても、公開鍵暗号の守秘(暗号化)で推奨されているのはRSA暗号ベースのRSA-OAEPだけで楕円曲線ベースのものはありません。
楕円曲線を使った署名の説明で「暗号化」の言葉が出てきたらそれは通常間違っているのでご注意ください。
この章で実装するのは鍵共有と署名です。
DH鍵共有
前章で説明されたDH鍵共有をPythonを使いながら復習します。
アリスとボブで鍵共有をする場合、あらかじめ利用する素数
ここでは
そしてアリスとボブがそれぞれ秘密の値
Pythonでは
次にアリスとボブはそれぞれ
掛け算やベキ乗の
Pythonでは
アリスが
import secrets
p = 65537
g = 3
a = secrets.randbelow(p)
b = secrets.randbelow(p)
print("a =", a)
print("b =", b)
A = pow(g, a, p)
B = pow(g, b, p)
print("A =", A)
print("B =", B)
s1 = pow(B, a, p)
s2 = pow(A, b, p)
print("s1 =", s1)
print("s2 =", s2)
print("s1 == s2?", s1 == s2)
DH鍵共有
a = 37562
b = 4823
A = 64643
B = 38750
s1 = 59475
s2 = 59475
s1 == s2? True
実行例(実行する度変わります)
DH鍵共有の安全性
DH鍵共有の通信を盗聴している攻撃者は素数
したがってまず
この性質を離散対数問題DLP(Discrete Logarithm Problem)の困難性といいます。
次にDLPが解けなくても
この性質をDH問題DHP(DH Problem)の困難性といいます。
安全にDH鍵共有をするにはDHPの困難性が求められます。
ベキ乗(
DLPとDHP
楕円曲線暗号
楕円曲線
前節で紹介したようにDH鍵共有はDLPやDHPなどの困難性を利用した方式でした。
似た性質を持つ困難性があれば、それを利用して暗号を作れる可能性があります。
その一つが近年普及している楕円曲線です。
楕円曲線のイメージは浮輪の表面(これをトーラスと言います)です。
そのトーラスには糸が巻きついていて、
そしてこれらの点に対して
楕円曲線とその上の点
ECDLPとECDHP
楕円曲線上でDLPやDHPに相当する問題を考えます。
DLPやDHPは掛け算やベキ乗算で表現されていましたが、楕円曲線上では点の足し算や整数倍で表現します。
楕円曲線の点の演算
点
同様に
ECDLPとECDHPの困難性
楕円曲線のパラメータを適切に取るとECDLPやECDHPが困難であることが知られています。
楕円曲線を使うと今までのDH鍵共有に比べてずっと少ないビット長でより高い安全性を達成できます。
具体的には2048ビットの素数のDH鍵共有よりも256ビットの楕円曲線を使ったDH鍵共有の方が安全です。
ECDH鍵共有
ECDHPが困難という仮定の元で、楕円曲線版のDH鍵共有ができます。これをECDH鍵共有といいます。
- アリスが乱数
を選びa をボブに送る。aP - ボブが乱数
を選びb をアリスに送る。bP - アリスは
を計算する。a(bP)=abP - ボブは
を計算する。b(aP)=abP
ECDH鍵共有
楕円曲線の実装
ECDH鍵共有の例
まず全体像を把握するために、楕円曲線クラス
詳細は後述しますが、楕円曲線の点は2個の整数の組
サンプルの中で
関数の戻り値は楕円曲線の点です。
そして、
内部ではECDH鍵共有の節で紹介した
そのあと、アリスが
最後にそれぞれの値を表示して
from ec import Ec, Fr
from ec import initSecp256k1
P = initSecp256k1()
a = Fr()
b = Fr()
a.setRand()
b.setRand()
print(f"a={a}")
print(f"b={b}")
aP = P * a
bP = P * b
baP = aP * b
abP = bP * a
print(f"baP={baP}")
print(f"abP={abP}")
print(f"baP == abP? {baP == abP}")
ECDH鍵共有の例
a=0x374a3960204d5170e615181dec9f8b40810a88f81af9f66e8bc096260beb1444
b=0x1ba9a286dcfbb544fd2ba4d163a1a6dc8bdb3eecbe08779082aa8052194cc129
baP=(0x6c86ae1d8a0b3e0311c02064668854687d4fc00e6c642716669cf6ce7ac97f76, 0xed9e0d8d69dce6be8702769547ba9edc693bb18e026d09f811a5f96598a51aa9)
abP=(0x6c86ae1d8a0b3e0311c02064668854687d4fc00e6c642716669cf6ce7ac97f76, 0xed9e0d8d69dce6be8702769547ba9edc693bb18e026d09f811a5f96598a51aa9)
baP == abP? True
ECDH鍵共有の結果
楕円曲線クラスの使い方が伝わったでしょうか。
有限体
さて、これからいよいよ楕円曲線の実装に入ります。
そのためには有限体(ゆうげんたい)という概念が必要です。
四則演算ができる集合を体(たい)といいます。
分数(有理数)の集合や実数の集合は四則演算の結果も分数や実数です。そのため有理数体や実数体といいます。
今回扱うのは有限個の集合からなる体なので有限体といいます。
従来のDH鍵共有では掛け算やベキ乗算のあとに
ここでは素数
有限体の加減乗算
有限体クラスは素数
class Fp:
@classmethod
def init(cls, p):
cls.p = p
有限体クラスの初期化
四則演算のうち、足し算、引き算、掛け算は普通に演算をした後
メンバ変数はvとしましょう。
class Fp:
...
# 値を設定する
def __init__(self, v=0):
self.v = v % Fp.p
def __add__(self, rhs):
return Fp(self.v + rhs.v)
def __sub__(self, rhs):
return Fp(self.v - rhs.v)
def __mul__(self, rhs):
return Fp(self.v * rhs.v)
有限体クラスのadd, sub, mul
有限体の除算
有限体の割り算はちょっと頭をひねります。
整数
そのためこの方法では体になりません。
そこで割り算の定義を少し変えます。
たとえば
このような
フェルマーの小定理とは素数
です。RSA暗号で登場するのでご存じの方もいらっしゃるかもしれません。ここではこの定理を認めて先に進めます。
と変形します。
つまり
Pythonでは
0の逆数は普通の数と同じく存在しないのでエラーにします。
def inv(self):
v = self.v
if v == 0:
raise Exception("zero inv")
p = Fp.p
return Fp(pow(v, p-2, p))
有限体クラスのinv
逆数ができれば割り算
def __truediv__(self, rhs):
return self * rhs.inv()
有限体クラスの除算
実際に
from fp import Fp
p = 7
Fp.init(p)
a = Fp(5)
for i in range(1, p):
x = Fp(i)
r = a / x
print(f"{x}*{r}={x*r}")
除算の例
0x1*0x5=0x5
0x2*0x6=0x5
0x3*0x4=0x5
0x4*0x3=0x5
0x5*0x1=0x5
0x6*0x2=0x5
除算の例の結果
期待する結果になっています(楕円曲線暗号のPythonによる実装その2(楕円曲線とECDSA)に続く)。
Discussion