Base58の実装

def encode_base58(s):
    count = 0
    for s in s:
        if c == 0:
            count += 1
        else:
            break
    num = int.from_bytes(s, 'big')
    prefix = '1' * count
    result = ''
    while num > 0:
        num, mod = divmod(num, 58)
        result = BASE58_ALPHABET[mod] + result
    return prefix + result

16進数をバイトに変換し、それをBase58に変換

from ecc import Signature
import helper

s = '7c076ff316692a3d7eb3c3bb0f8b1488cf72e1afcd929e29307032997a838a3d'
print(helper.encode_base58(bytes.fromhex(s)))

$ python3 main.py
9MA8fRQrT4u8Zj8ZRd6MAiiyaxb2Y1CMpvVkHQu5hVM6

pythonでdivmod

Pythonでは //で商、%で余りだが、divmodで両方を出力する。

q = 13 // 4
mod = 13 % 4
print(q, mod)

q, mod = divmod(17, 7)
print(q, mod)

$ python3 test.py
3 1
2 3

DER署名

署名をシリアライズする場合、署名のr, sをエンコードする必要がある。しかし、pointのように圧縮はできない
署名をシリアライズする標準をDERフォーマット(Distinguished Encoding Rules)と呼ぶ。

### DER署名
1. 0x30バイトで開始
2. 署名の残りの長さをエンコード(0x44または0x45)して追加
3. マーカーバイト0x02を追加
4. rをビッグエンディアン整数としてエンコード ただしrの先頭バイトが0x80以上の時は0x00を付与、長さをrの先頭に追加
5. マーカーバイト0x02を追加
6. sをビッグエンディアン整数としてエンコード ただしsの先頭バイトが0x80以上の時は0x00を付与、長さをsの先頭に追加

先頭ビットが1の場合は負の数。DERは負のエンコードを許容する。

    def der(self):
        rbin = self.r.to_bytes(32, byteorder='big')
        rbin = rbin.lstrip(b'\x00')
        if rbin[0] & 0x80:
            rbin = b'\x00' + rbin 
        result = bytes([2, len(rbin)]) + rbin
        sbin = self.s.to_bytes(32, byteorder='big')
        sbin = sbin.lstrip(b'\x00')
        if sbin[0] & 0x80:
            sbin = b'\x00' + sbin
        result += bytes([2, len(sbin)]) + sbin
        return bytes([0x30, len(result)]) + result

r, sが定まれば、DERフォーマットが求められる。

from ecc import Signature

r = 0x37206a0610995c58074999cb9767b87af4c4978db68c06e8e6e81d282047a7c6
s = 0x8ca63759c1157ebeaec0d03cecca119fc9a75bf8e6d0fa65c841c8e2738cdaec

sig = Signature(r, s)
print(sig.der().hex())

$ python3 main.py
3045022037206a0610995c58074999cb9767b87af4c4978db68c06e8e6e81d282047a7c60221008ca63759c1157ebeaec0d03cecca119fc9a75bf8e6d0fa65c841c8e2738cdaec

pythonのbytes型

bytesはバイナリデータを扱うバイト型

str1 = "高島屋"
enc_str1 = str1.encode()

print(f"{str1}:{enc_str1}")
print(f"type: {type(enc_str1)}")

print(f"高: {hex(ord('高'))}")
print(f"島: {hex(ord('島'))}")
print(f"屋: {hex(ord('屋'))}")

$ python3 test.py
高島屋:b’\xe9\xab\x98\xe5\xb3\xb6\xe5\xb1\x8b’
type:
高: 0x9ad8
島: 0x5cf6
屋: 0x5c4b

バイト型はb’で囲まれる
\xは続く文字列

SEC公開鍵からyを見つけるメソッド

    @classmethod
    def parse(self, sec_bin):
        if sec_bin[0] == 4:
            x = int.from_bytes(sec_bin[1:33], 'big')
            y = int.from_bytes(sec_bin[33:65], 'big')
            return S256Point(x=x, y=y)
        is_even = sec_bin[0] == 2
        x = S256Field(int.from_bytes(sec_bin[1:], 'big'))
        alpha = x**3 + S256Field(B)
        beta = alpha.sqrt()
        if beta.num % 2 == 0:
            even_beta = beta 
            odd_beta = S256Field(P - beta.num)
        else:
            even_beta = S256Field(P - beta.num)
            odd_beta = beta
        if is_even:
            return S256Point(x, even_beta)
        else:
            return S256Point(x, odd_beta)

公開鍵が04, 03, 02かで秘密鍵の点を戻す

SEC圧縮フォーマットは以下の通り

from ecc import S256Point, PrivateKey

p = PrivateKey(5001)
print(p.point.sec(compressed=True).hex())

Pythonのクラスメソッド(@classmethod)

クラスにくっついている関数のようなもので、インスタンス化していないクラスのものから呼び出せる。
メソッドに@classmethodと付けることでクラスメソッドにできる。

class A:

    def test_method():
        print("test")

    @classmethod
    def my_cls_method(cls):
        print("hello")

A.my_cls_method()
A.test_method()

– 第一引数でクラスが取得できる(インスタンスメソッドは第一引数が必ずselfになる)
– クラスの中にあるので、クラスをインポートすれば使える
– クラスメソッドを使わずに関数として書くこともできるが、クラスメソッドの場合は、インポートできて、まとめて管理できる

class Item:
    def __init__(self, id, name):
        self.id = id
        self.name = name

    @classmethod
    def retrieve_from_api(cls, id):
        res = requests.get(f"https://api.example.com/items/{id}")
        data = res.json()
        return cls(id, data["name"])

非圧縮SECフォーマットと圧縮SECフォーマット

楕円曲線暗号の公開鍵は(x, y)形式の一つの座標
ECDSA公開鍵をシリアライズする方法はSECフォーマット(Standards for Efficient Cryptography)と呼ばれる

1. プレフィックスバイトは0x04
2. 32バイトのビッグエンディアン整数としてx座標を追加
3. 32バイトのビッグエンディアン整数としてy座標を追加

圧縮SECフォーマットの場合、yが偶数の場合は0x02とし、奇数の場合は0x03とする。

    def sec(self, compressed=True):
        if compressed:
            if self.y.num % 2 == 0:
                return b'\x02' + self.x.num.to_bytes(32, 'big')
            else:
                return b'\x03' + self.x.num.to_bytes(32, 'big')
        else:
            return b'\x04' + self.x.num.to_bytes(32, 'big') + self.y.num.to_bytes(32, 'big')
from ecc import S256Point, PrivateKey

pkey = PrivateKey(5000)
p = pkey.point.sec(compressed=False).hex()
print(p)

$ python3 main.py
04ffe558e388852f0120e46af2d1b370f85854a8eb0841811ece0e3e03d282d57c315dc72890a4f10a1481c031b03b351b0dc79901ca18a00cf009dbdb157a1d10

Pythonのビッグエンディアンとリトルエンディアン

import binascii

hex_b = 'f0148c'
bytes_be = binascii.unhexlify(hex_b)
bytes_le = bytes_be[::-1]
hex_le = binascii.hexlify(bytes_le).decode()
print(hex_le)
import sys

def dump(data):
    print(data)

    a = int.from_bytes(data, byteorder='big')
    b = int.from_bytes(data, byteorder='little')
    c = int.from_bytes(data, byteorder=sys.byteorder)
    print(a, hex(a))
    print(b, hex(b))
    print(c, hex(c))

dump(b'\x01\x02')
dump(b'\x11\x12\x13\x14\x15\x16\x17\x18\x19')

$ python3 test.py
b’\x01\x02′
258 0x102
513 0x201
513 0x201
b’\x11\x12\x13\x14\x15\x16\x17\x18\x19′
314897056051100063769 0x111213141516171819
462904482303900324369 0x191817161514131211
462904482303900324369 0x191817161514131211