技術メモ

役に立てる技術的な何か、時々自分用の覚書。幅広く色々なことに興味があります。

pythonでコードを書きながら楕円曲線暗号を理解する

仮想通貨についての勉強
楕円曲線暗号とは、ビットコインのデジタル署名で使われてる公開鍵暗号方式

この記事にあるコードを写経しながら勉強する。

自分自身まだ深く理解してないところがあるので解説は別の機会に書くことにしてメモ書き程度にとどめておく。

コード

from hashlib import sha256

class ECDSA:
    def __init__(self):
        # parameters are based on SPEC256k1
        self.cp = pow(2,256) - pow(2,32) - pow(2,9) - pow(2,8) - pow(2,7) - pow(2,6) - pow(2,4) - pow(2,0)
        self.cb = 7
        self.ca = 0
        self.base_x = 55066263022277343669578718895168534326250603453777594175500187360389116729240
        self.base_y = 32670510020758816978083085130507043184471273380659243275938904335757337482424
        self.base = (self.base_x, self.base_y)
        self.secret_key = None
        self.public_key = None

    def generate_key(self, string):
        secret_hash = sha256(string.encode('utf-8')).hexdigest()
        secret_key = int(secret_hash, 16)
        pt = self.EC_multi(secret_key)
        public_key = int("04" + "%064x" % pt[0] + "%064x" % pt[1], 16)
        self.secret_key = secret_key
        self.public_key = public_key
        return (public_key, secret_key)

    def EC_add(self, P, Q):
        lam = ((Q[1] - P[1]) * inv_mod(Q[0] - P[0], self.cp)) % self.cp
        x = ((pow(lam, 2)) - P[0] - Q[0]) % self.cp
        y = (lam * (P[0]- x) - P[1]) % self.cp
        return (x,y)

    def EC_double(self, P):
        lam = ((3 * (pow(P[0], 2) + self.ca) * inv_mod(2 * P[1], self.cp))) % self.cp
        x = (pow(lam, 2) - 2*P[0]) % self.cp
        y = (lam * (P[0] - x) - P[1]) % self.cp
        return (x,y)

    def EC_multi(self, scalar):
        if scalar == 0:
            raise ValueError('invalid scalar/ purivate key')
        scalar_bin = str(bin(scalar))[2:]
        point = self.base
        for i in range(1, len(scalar_bin)):
            point = self.EC_double(point)
            if scalar_bin[i] == "1":
                point = self.EC_add(point, self.base)
        return point

def inv_mod(k, mod):
    return pow(k, mod-2, mod)


if __name__=="__main__":
    ecdsa = ECDSA()
    pk, sk = ecdsa.generate_key("Satoshi Nakamoto")
    print(sk)
    print(pk)

(補足)モジュラ逆数

モジュラ逆数とはa^{-1}\equiv x \pmod{m}となるxのこと。
素数 mと互いに素な aに対して

 a^{-1}\equiv a^{m-2} \pmod{m}

となることを使っている。(フェルマーの小定理

(証明)
オイラー関数 \phi(m)を用いて、mが素数の時
\displaystyle
\left\{
\begin{array}{1}
 a^{\phi(m)}\equiv 1 \pmod{m}\\
 \phi(m) = m-1 
\end{array}
\right .
なので

a^{\phi(m) - 1} \equiv a^{-1} \pmod{m} \\
a^{m-2} \equiv a^{-1} \pmod{m}
(Q.E.D)

pythonでは a^{m-2}\pmod{m}

pow(a, m-2, m)

の1行で書ける

pythonでコードを書きながらデジタル署名を理解する

仮想通貨の仕組みについての勉強の続き。
仮想通貨にはなくてはならないデジタル署名を実装して勉強した。
デジタル署名自体は目新しいものではなく昔からある技術で、今でもネット決済など広く使われている。

デジタル署名とは

ザックリとした説明

デジタル署名とは、その名の通り文書に「署名」すること。(ここでいう文書とは文字列や数字列といったデータのこと)
では署名に必要な要件とは何か。

  • 第一に、署名をすることができるのは署名者(アリスとする)だけだが、署名を見た人は誰でもそれが有効であることが確認できなければいけない。
  • 第二に、署名と文書は密接に結びついており、別の文書に対するアリスの保証を示すためには使えない。つまり色々な署名からアリスの署名の仕方を推測すること(本当の署名でいうところの筆跡など真似に相当するような行為)、はできないということ。

という2つの要件が必要になってくる。

踏み込んだ説明

暗号理論を使えば上の条件が実現できる。
公開鍵暗号を用いる方法だ。(公開鍵暗号については RSA暗号を実装してみる(知識編) - 技術メモ
具体的にはデジタル署名には3つのスキームで成り立つ。

  • 公開鍵と秘密鍵のペアを生成する (generateKey)
  • 秘密鍵を使って文書に対する署名を生成する (sign)
  • 公開鍵・文書をつかって署名が正しいものかどうかを検証する (verify)

署名者(アリスとする)はgenerateKeyを使って秘密鍵skと公開鍵pkを生成し、公開鍵を公開する。
アリスは署名したい文書Dataをハッシュ関数にかけたうえで秘密鍵skを使って署名signatureを生成する。
承認者は公開されている公開鍵pk・文書Data・アリスから受け取った署名signatureを使って、pk,Data,signatureの組が正当であればTrueを返し違っていればFalseを返す。署名が間違っているか文書が書き換えられていれば承認がうまくいかないことになる。
f:id:swdrsker:20180124031957p:plain

普通の公開鍵暗号方式と違うところは、文書自体はそのまま(暗号化せずに)通信するというところだ。
だから、文書は誰でも見れるものでも良いという事。
文書を見れる人なら誰でも署名と公開鍵さえ渡されればそれがアリスによる署名だとわかる。

デジタル署名に対する攻撃

N通りの{文書、署名}および公開鍵の組み合わせから秘密鍵を見破ることができればデジタル署名は破られてしまう。(chosen message attack)
だが、現在一般に使われている署名方式ではこれを行うのはほぼ不可能に近い。

コード

python (python3.5)で書いた。RSAハッシュ関数(SHA256)など暗号技術諸々はpycryptoを使った。
ちなみに実際のビットコインでは公開鍵暗号方式RSAではなく楕円曲線暗号が使われている。

digitalsignature.pyという名前で保存

from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from base64 import b64decode, b64encode
import sys

def generate_key(keysize=2048, passphrase = None):
    new_key = RSA.generate(keysize)
    public_key = new_key.publickey().exportKey()
    secret_key = new_key.exportKey(passphrase = passphrase)
    return secret_key, public_key

def sign(secret_key, data, passphrase = None):
    try:
        rsakey = RSA.importKey(secret_key, passphrase = passphrase)
    except ValueError as e:
        print(e)
        sys.exit(1)
    signer = PKCS1_v1_5.new(rsakey)
    digest = SHA256.new()
    digest.update(b64decode(data))
    sign = signer.sign(digest)
    return b64encode(sign)

def verify(pub_key, signature, data):
    rsakey = RSA.importKey(pub_key)
    signer = PKCS1_v1_5.new(rsakey)
    digest = SHA256.new()
    digest.update(b64decode(data))
    if signer.verify(digest, b64decode(signature)):
        return True
    else:
        return False
テスト

digitalsignature.pyと同じフォルダ内で実行

from digitalsignature import *

# 秘密鍵と公開鍵を作る。(パスワードはなくても良い)
password = "password"
sk, pk = generate_key(passphrase = password)

# メッセージに署名する(署名者が行う)
message = "hoge"
sig = sign(sk, message, passphrase = password)

# 承認テスト(承認者が行う)
if verify(pk, sig, message):
    print("承認 OK")
else:
    print("承認 NG")

# メッセージの書き換えに対するテスト
changed_message = "hogehoge"
if verify(pk, sig, changed_message):
    print("書き換えテスト NG")
else:
    print("書き換えテスト OK") # 承認されなければOK

# 間違った秘密鍵の署名に対するテスト
sk2, pk2 = generate_key(passphrase = password)
sig2 = sign(sk2, message, passphrase = password)
if verify(pk, sig2, message):
    print("不正署名テスト NG")
else:
    print("不正署名テスト OK") # 承認されなければOK
  • 実行結果
承認 OK
書き換えテスト OK
不正署名テスト OK

参考にした本

仮想通貨の教科書

仮想通貨の教科書

楕円曲線暗号について勉強した

pythonでコードを書きながらブロックチェーンを理解する

いまや知らない人はいない仮想通貨、別名暗号通貨(cryptocurrency)。
ドル円とは比べ物にならないほどの殺人的なボラティリティを見せているけれど、しばらくすれば落ち着いてくるんだろうか。
はっきりいって今の相場と税率でレバレッジ15倍とか正気の沙汰とは思えない。樹海の中に宝探しに行くようなものだ。

今回の記事の本題はその仮想通貨の仕組みについて。
投資にも興味はあるが、仮想通貨の仕組み自体にも興味がある。
仮想通貨の仕組みを知れば投資先の選択にも参考になるんじゃないかなと思うし、なにより以前から技術的に面白そうだと思っていた。
その仮想通貨の基盤となる技術がブロックチェーンというもので、これを理解しないことには始まらない。ということでファーストステップとしてブロックチェーンを理解するためにコードを写経してみた。

写経するのはid:mizchi氏の記事

コード自体はjavascriptで書かれているものを、アレンジを加えつつpython(python3.5)に書き直してみる。
そっくりなタイトルを見てわかる通りまあただの写経。)
大元のソースはこれnaivechain/main.js at master · lhartikk/naivechain · GitHub

ザックリとした説明

ブロックチェーンとは

  • ブロックチェーンとはその名の通りブロックを鎖状に繋げたもの
  • ブロックとは ①自身のID番号(ハッシュ値) ②前のブロックのID番号 ③その他(取引履歴など)の情報 を持つもの*1

ブロックチェーンは書き換えができない

ID番号は計算によって出てくるものなんだけど、計算するにあたって前のブロックのID番号や自身のブロックの情報を使うことになる。
なので途中のブロックの情報を書き換えるとそれ以降に続いているブロックのID番号が計算と合わなくなってしまう。これがブロックの書き換えができないと言われている大きな理由。

f:id:swdrsker:20180123121137p:plain

コード

blockchain.pyというファイル名で保存

import hashlib
import time
from datetime import datetime

class Block:
    def __init__(self, index, previous_hash, data, timestamp, this_hash):
        self.index = index
        self.previous_hash = previous_hash
        self.data = data
        self.timestamp = timestamp
        self.this_hash = this_hash

    def equal(self, block):
        if not self.index == block.index:
            return False
        elif not self.previous_hash == block.previous_hash:
            return False
        elif not self.data == block.data:
            return False
        elif not self.timestamp == block.timestamp:
            return False
        elif not self.this_hash == block.this_hash:
            return False
        return True


class Blockchain:
    def __init__(self):
        self.blockchain = [self.get_initial_block()]

    def get_initial_block(self):
        return Block(0,
                    "0",
                    "my genesis block!!",
                    1465154705,
                    "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7")

    def get_length(self):
        return len(self.blockchain)

    def get_latest_block(self):
        return self.blockchain[-1]

    def generate_next_block(self, block_data):
        previous_block = self.get_latest_block()
        next_index = previous_block.index + 1
        next_timestamp = int(time.mktime(datetime.now().timetuple()))
        next_hash = calculate_hash(next_index, previous_block.this_hash, block_data, next_timestamp)
        return Block(next_index, previous_block.this_hash, block_data, next_timestamp, next_hash)

    def is_valid_new_block(self, new_block, previous_block):
        if previous_block.index + 1 != new_block.index:
            return False
        elif previous_block.this_hash != new_block.previous_hash:
            return False
        elif calculate_hash_for_block(new_block) != new_block.this_hash:
            return False
        return True

    def is_valid_chain(self):
        if not self.blockchain[0].equal(self.get_initial_block()):
            return False
        else:
            tmp_block = self.blockchain[0]
            for i in range(1,self.get_length()):
                if not self.is_valid_new_block(self.blockchain[i], tmp_block):
                    return False
                else:
                    tmp_block = self.blockchain[i]
        return True

    def add_block(self, new_block):
        if self.is_valid_new_block(new_block, self.get_latest_block()):
            self.blockchain.append(new_block)
        else:
            raise ValueError("add block error!")


def calculate_hash(index, previous_hash, data, timestamp):
    string = str(index) + previous_hash + data + str(timestamp)
    return hashlib.sha256(string.encode('utf-8')).hexdigest()

def calculate_hash_for_block(block):
    return calculate_hash(block.index, block.previous_hash, block.data, block.timestamp)
簡単に動かしてみる

同じフォルダ内で呼び出して使う

from blockchain import *

# ブロックチェーンのインスタンス
blockchain = Blockchain()

# 新しいブロック
new_block = blockchain.generate_next_block("Hello World!")

# ブロックを加える
try:
    blockchain.add_block(new_block)
except ValueError as e:
    print(e)

# もう一つ加える
new_block = blockchain.generate_next_block("hoge")
try:
    blockchain.add_block(new_block)
except ValueError as e:
    print(e)
競合ブロックを作って追加するテスト

先ほどの続きに書く
もし何かの手違いで新しいブロックを生成するタイミングが被ってしまうとどうなるか、その時は同じ「前のブロックチェーンのID番号」を持ったブロックができてしまう。そのようなブロックを追加する場合はエラーが出て弾かれるようにしたい。*2
競合ブロックを作ってみて、2つ目のブロックが追加できないことを確認する

block1 = blockchain.generate_next_block("original block")
block2 = blockchain.generate_next_block("second block")
try:
    blockchain.add_block(block1)
except ValueError as e:
    print(e)
try:
    blockchain.add_block(block2)
    print("競合テストNG")
except ValueError as e:
    print("競合テストOK!") # ここでエラーになれば成功
  • 結果
競合テストOK!
途中のブロックを書き換えるテスト

途中のブロックのデータを書き換えて、ブロックチェーンが不適切だと判定されることを確認する

# 各々のブロックのデータを見てみる
print("-----Block data------")
for i in range(blockchain.get_length()):
    print(blockchain.blockchain[i].data)

# 途中のブロックのデータを書き換える
blockchain.blockchain[2].data = "invalid message!!!!"
print("-----Block data (changed)------")
for i in range(blockchain.get_length()):
    print(blockchain.blockchain[i].data)

# 今のブロックチェーンが妥当かどうか検証
if not blockchain.is_valid_chain():
    print("書き換えテストOK!")
else:
    print("書き換えテストNG")
  • 実行結果
-----Block data------
my genesis block!!
Hello World!
hoge
original block
-----Block data (changed)------
my genesis block!!
Hello World!
invalid message!!!!
original block
書き換えテストOK!

おわり

ブロックチェーンについてはなんとなくわかった(気がする)。でもまだ、マイニングとは具体的になにをしてるんだ、どういう仕組みで仮想通貨の安全性が保たれるんだ、ビットコインと色々なアルトコインは技術的にどこが違うのか、みたいなところまでは全然わかってないのでそれはこれからの課題。

*1:わかりやすいかと思いあえて「ID番号」とするけれど、ハッシュ値のこと。ハッシュ関数ビットコインではSHA256が使われている、コード上でもSHA256にしている。

*2:仮想通貨のマイニングではこの性質がきいてくる