직렬화는 객체를 바이트 스트림으로 바꾸는 것을 말한다.
즉, 객체에 저장된 데이터를 스트림에 작성하기 위하여 연속적인 데이터 값으로 변환하는 것을 의미한다.
이 과정의 목적은 객체의 상태를 그대로 저장하고 필요할 때 재생성하여 사용함에 있다.
앞서 유한체부터 시작해 타원곡선 암호까지 작성한 다양한 클래스가 있을 것 이다.
이 해당 클래스의 객체들을 네트워크를 통하여 다른쪽으로 전송하거나 저장장치에 저장하기 위함에 있다.
바로 이전에 작성한 공개키 클래스인 S256Point 클래스부터 직렬화해보자.
우선 ECC 방식에서 공개키는 좌표 형식임을 기억하자.
이미 ECDSA 공개키를 직렬화 하는 표준안이 존재한다.
이를 SEC 형식 이라고 한다.
이 방식은 아주 작은 오버헤드만 요구하며, 공개키에 관련해서는 두가지 SEC 형식이 존재한다.
빅 엔디안 방식은 SPARC, ARM, Motorola 계열의 CPU가 사용하는 아키텍쳐 방식에서 사용한다.
리틀 엔디안 방식은 x86의 인텔 계열의 CPU 에서 사용한다.
예시를 들어 설명하면 아래와 같다.
Big Endian
{ 0000 0000, 0000 0000, 0010 0000, 0000 1000 } == { 0x00, 0x00, 0x20, 0x10 }
== { 0, 0, 32 16 }
Little Endian
{ 0000 1000, 0010 0000, 0000 0000, 0000 0000 } == { 0x10, 0x20, 0x00, 0x00 }
== { 16, 32, 0 0 }
우선 주어진 점 에 대한 비압축 SEC 형식 표현 방법이다.
아래 그림을 통해 비압축 SEC 형식을 볼 수 있다.
class S256Point(Point):
...
def sec(self):
'''returns the binary version of the SEC format'''
return b'\x04' + self.x.num/to_bytes(32, 'big') \
+ self.y.num.to_bytes(32, 'big')
값이 짝수면 x , 홀수이면 x 인 바이트 접두부로 시작한다.
그 다음 좌표를 바이트 빅엔디안 정수로 표현한다.
class S256Point(Point):
...
def sec(self, compressded=True):
'''returns the binary version of the SEC format'''
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')
이는 이 로 나누어 떨어진다는 의미이므로 는 정수가 된다.
정리하면 아래와 같다.
위 공식을 다음과 같이 S256Field 클래스에 일반 메소드로 추가 가능하다.
class S256Field(FieldElement):
...
def sqrt(self):
return self**((P + 1)//4
class S256Point:
...
@classmethod
def parse(self, sec_bin)
'''returns a Point object from a SEC binary (not hex)'''
if sec_bin[0] == 4: #1
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 #2
x = S256Field(int.from_bytes(sec_bin[1:], 'big'))
#right side of the equation y^2 = x^3 + 7
alpha = x**3 + S256Field(B)
#solve for left side
beta = alpha.sqrt() #3
if beta.num % 2 == 0: #4
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)
우선 DER 서명 형식은 아래와 같이 정의된다.
번과 번 규칙에서 첫 번째 바이트 x 인 경우 을 넣는 이유는 DER 형식이 음수값도 수용 가능한 일반 형식이기 때문이다.
또한 부호 있는 이진수에서 첫 번째 비트가 1인 것은 음수를 의미한다.
ECDSA 서명에서 모든 숫자는 양수이며, x 을 앞에 넣어야 첫 번째 비트가 이 되어 양수로 인식된다.
아래의 그림과 같이 DER 형식의 예를 볼 수 있다.
class Signature:
...
def der(self):
rbin = self.r.to_bytes(32, byteorder='big')
rbin = rbin.lstrip(b'\x00')
if rbin[0] & 0x00:
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] & 0x00:
sbin = b'\x00' + sbin
result += bytes([2, len(sbin)]) + sbin
return bytes([0x30, len(result)]) + result
BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def encode_base58(s):
count = 0
for c in s: #1
if c == 0:
count += 1
else:
break
num = int.from_bytes(s, 'big')
prefix = '1' * count
result = ''
while num > 0: #2
num, mod = divmod(num, 58)
result = BASE58_ALPHABET[mod] + result
return prefix + result #3
def encode_base58_checksum(b):
return encode_base58(b + hash256(b)[:4])
주석을 통해 살펴보면 아래와 같다.
해당 파이썬 함수는 임의 길이의 bytes 형 값을 받아 Base 로 부호화된 str 형 값을 반환한다.
압축 SEC 형식의 비트도 여전히 비트 수가 많으며, 보안에 취약한 면이 있다.
주소의 길이도 줄이고 보안성도 높이기 위하여 ripemd 해시를 사용 가능하다.
그럴 경우 바이트의 SEC 형식을 바이트로 줄일 수 있다.
비트코인 주소를 생성하는 방법은 아래와 같다.
위의 절차를 아래와 같이 한줄 코드로 표현 가능하다.
def encode_base58_checksum(b):
return encode_base58(b + hash(b)[:4])
def hash160(s):
'''sha256 followed by ripemd160'''
return hashlib.new('ripemd160', hashlib.sha256(s).digest()).digest()
sha 해시는 hashlib.sha256(s).digest로 얻은 뒤 ripemd160 해시함수의 입력으로 넘겨준다.
이후 다음과 같이 SPoint 클래스에 hash, address 메소드를 추가할 수 있다.
class S256Point:
...
def hash160(self, compressed=True)
return hash160(self.esec(compressed))
def address(self, compressed=True, testnet=False):
'''Returns the address string'''
h160 = self.hash160(compressed)
if testnet:
prefix = b'\x6f'
else:
prefix = b'\x00'
return encode_base58_checksum(prefix + h160)
일반적으로 비밀키를 직렬화 할 경우는 별로 없는데, 비밀키는 네트워크로 전파하지 않기 때문이다.
비밀키를 전파시키는 것은 매우 위험한 행동이지만 가끔씩 다른 지갑으로 비밀키를 옮기고 싶은 경우가 있다.
이러한 경우에 WIF 형식을 사용할 수 있다.
비밀키를 읽기 쉽도록 직렬화 하는 방법인데, WIF는 주소에서 사용했던 Base 부호화를 사용한다.
아래는 비밀키를 WIF 형식으로 만드는 방법이다.
이를 통해 PrivateKey 클래스에 WIF 메소드를 아래와 같이 수정 가능하다.
class PrivateKey
...
def wif(self, compressed=True, testnet=False):
secret_bytes = self.secret.to_bytes(32, 'big')
if testnet:
prefix = b'\xef'
else:
prefix = b'\x80'
if compressed:
suffix = b'\x01'
else:
suffix = b''
return encode_base58_checksum(prefix + secret_bytes + suffix)