앵하니의 더 나은 보안

2세대 암호화폐 지갑의 생성 원리 본문

보안 기술/blockchain

2세대 암호화폐 지갑의 생성 원리

앵한 2023. 11. 5. 13:09

개요

2세대 암호화폐, 스마트 컨트랙트를 활용하는 암호화폐의 지갑 생성 과정을 알아보겠다.

 

갑자기 왜 ?

배포되는 컨트랙트도 엄청 많고, 생성되는 지갑들도 말도 안되게 많을텐데 지갑 주소로 사용되는 형식이, 경우의 수가 모두 소진돼서 주소가 겹치면 어떻게 될 지, 그리고 고의적으로 같은 지갑주소를 생성하면 어떻게 될지가 궁금해서 중복되는 지갑주소를 특정해서 만들어보고자 한다.
만들려면 만드는 방법을 먼저 알아야하니, 지갑 만드는법을 알아보고 실제로 python으로 지갑을 한번 생성해보도록 하자

2세대 암호화폐 지갑(주소)의 생성 과정

2세대 암호화폐 지갑의 생성은 개인키 생성 > 공개키 생성 > 지갑주소 생성의 과정으로 이루어진다.

개인키 생성

최초 생성되는 개인키는 예측할 수 없는 값이여야하므로 난수 생성기를 통해 생성된다. 근데 무작위성이 높지 않은 난수 생성기를 사용해 개인키 값을 생성하면, 개인키가 생성될 수 있는 경우의 수를 어느정도 예측할 수 있어, 예측할 수 없도록 특정 무작위성이 높은 난수생성기(os의 난수생성기)를 사용해 난수를 생성한다. 실제로 안드로이드 난수 생성기를 통해 지갑 주소를 만들었던 사례가 있는데, 이 당시 해커가 난수 생성기에서 생성되는 값을 예상해 지갑에 보관된 비트코인이 도난됐었다고 한다.

그래서 무작위성이 높은 난수생성기를 통해 256bit, 그러니까 16진수로 64자리의 개인키가 생성된다.
파이썬에서 os의 난수생성기를 사용해 개인키를 생성해보자

import os

private_key = os.urandom(32)#무작위 32바이트=16진수 64자리 생성
print("생성된 개인키 : ",private_key.hex())

실행 결과

 

공개키 생성

난수 생성기를 통해 얻은 개인키를 ECDSA의 타원곡선함수에 대입해 공개키를 얻는다고 한다.

 > 타원곡선함수에 의해 어떻게 생성되는지 궁금하다면?

더보기

공개키 (K)는 K = k * G 라는 수식으로 만들어집니다. 여기서 k는 개인키를 의미하고 G는 'Generator point)로 타원 곡선 위의 임의의 값을 뜻합니다. 

우선 임의의 점 G 에서 시작합니다. (임의의 점 G는 모든 사용자가 동일합니다). 점 G에서 접선을 긋게 되면 타원 곡선 특성상 점 G와 다른 곡선 상에 접점을 하나 더 만나게 됩니다. 이를 -2G라고 합니다. -2G를 x 축 대칭을 하면 2G가 됩니다. 이 행동을 k(개인키) 만큼 반복하면 kG를 구할 수 있는 것이죠. 정리하면

  1. 임의의 점 G에서 접선을 그어 만나는 다른 접점을 찾는다.
  2. 찾은 접점을 x축 대칭시킨다.
  3. 1),2) 과정을 k번 반복하여 K를 찾는다. 

이렇게 공개키가 생성되게 됩니다 ! 

출처)https://potensj.tistory.com/29

근데 이 값을 얻었다고 끝이 아니라, 타원곡선함수를 통해 얻은 각 256bit의 x, y좌표를 가지고 공개키를 생성하는데 생성하는 방법도 여기서 두가지로 나뉜다.

  1. 비압축형
    단순히 두 좌표를 합치는 방식으로 x좌표와 y좌표를 단순히 줄지어 잇는다.
    그리고 비압축형임을 표시하기 위해 prefix로 최초 1바이트는 0x04로 고정한다.
  2. 압축형
    y좌표 값을 버리고 x좌표 값만 사용한다. y좌표 값은 어차피 타원곡선함수 방정식을 통해 획득할 수 있으므로 생략하고, 압축형임을 표시하기 위해 prefix로 최초 1바이트는 0x02로 고정한다.

그럼 앞에 만든 개인키를 가지고 비압축형 공개키를 한번 만들어보자

from hashlib import sha256
import ecdsa

private_key = bytes.fromhex("f87352c555e0a3fe73cc392d2d8897dc658b30cdf9e640e302f306e1795390de")
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
vk = sk.get_verifying_key()
public_key = vk.to_string()
print("생성된 공개키 : ",public_key.hex())

실행결과

 

그러니까 비압축형 공개키로 표시해보면
0x04ab91c8854ee571a675071005908c4b6ad3631ce089ee6e6ce2d5149a9887cff8d9bf10400ef11e403bb9a576d7b6582884d3b534a6828189366f0ba1fafc4529
가 되겠다.

 

지갑주소 생성

마지막으로, 생성된 공개키를 사용해 드디어 지갑주소가 생성된다. 지갑주소가 생성되는 과정은 간단하다.
공개키를 keccak256이라는 해시 알고리즘에 넣어 나오는 해시 값에서 앞 24자리를 뺀 뒤 40자리의 hex값이 지갑주소가 되시겠다.

from hashlib import sha3_256
#여기서 sha3_256은 keccak256과 동일한것으로 한다.

public_key = bytes.fromhex("ab91c8854ee571a675071005908c4b6ad3631ce089ee6e6ce2d5149a9887cff8d9bf10400ef11e403bb9a576d7b6582884d3b534a6828189366f0ba1fafc4529")
pubkey_keccak256ed = sha3_256(public_key).hexdigest()
wallet_address = "0x"+pubkey_keccak256ed[-40:]
print(wallet_address)

실행결과

그러니까 최초 생성한 난수 값(개인키) f87352c555e0a3fe73cc392d2d8897dc658b30cdf9e640e302f306e1795390de를 통해 생성된 지갑주소는 0x883caac0759b5aeff12846f2aa5b851ea0e83996가 된다.

근데? 이더리움은 지갑주소에 대한 오류검증 관련 checksum이 없어서 오류검증을 위해 이더리움만의 독특한 방식을 사용한다.

생성된 지갑 주소 값을 또 다시 keccak256 해시 알고리즘에 넣어 해시 값을 획득 한 후, 해시 값 각 자릿수가 0x8이상일 경우 그 자릿수에 해당하는 지갑 주소를 대문자로 치환한다.

글로는 이해가 힘드니 직접 해보자

 

우선, 지갑주소의 keccak256 해시 값 획득

keccak256(883caac0759b5aeff12846f2aa5b851ea0e83996)= 9b1eccde5123b4dbdf50de1a54cb7bee651637f7db8298b240191ee8be364529

 

해시 값 중 0x8 이상의 데이터 자릿수 확인(40자리 이상의 수는 불필요하므로 버림)
9b1eccde5123b4dbdf50de1a54cb7bee651637f7…

 

각 자릿수를 원본 지갑 주소에서 확인
883caac0759b5aeff12846f2aa5b851ea0e83996

 

확인 된 자릿수 중 숫자데이터가 아닌 10이상의 hex 데이터는 대문자로 치환
883CAAC0759b5aEFF12846f2aa5B851Ea0e83996

 

그럼 최종적으로 자체 checksum이 적용된 지갑주소는
0x883CAAC0759b5aEFF12846f2aa5B851Ea0e83996가 되겠다.

 

지갑의 소유주 검증 과정

지갑의 주소만 갖고 있다고 다가 아니라 결국 이 지갑 주소의 소유주가 본인이라는 것을 인증해야 비로소 지갑에 있는 토큰들을 사용할 수 있다. 너무나도 당연하게도.

그럼 이 지갑이 본인의 소유라는 것은 어떻게 인증하는 걸까
소유를 증명하는 방식은 네트워크마다 다를수 있지만, 상식적으로 생각해보면 지갑 주소 생성에 사용됐던 개인키 공개키를 사용하면 된다라는 결론이 난다.
공개키를 네트워크에 전달하면 네트워크에서는 공개키를 사용해 지갑 주소를 만들었던 방식과 동일한 방식으로 지갑 주소 검증을 수행한다. 그리고 동시에 사용자가 전달하고자했던 데이터 payload는 개인키로 암호화해 네트워크에 송신, 네트워크에서는 전달받은 암호화데이터를 사용자의 공개키로 복호화해 볼 수 있으면? 검증이 이루어진다고 보면되겠다.

그래서 실제 진단할때도 그냥 burpsuite replace로 지갑 주소만 변경했을때 transaction fail이 떨어진거다.
사용자로부터 전달받은 공개키로 사용자의 지갑주소를 만들수 없으니까

특정 지갑 주소 생성 유도

지갑 주소는 결국 ‘임의로 생성된 개인키’에서 만들어진 ‘공개키의 해시 값의 일부 자리수'인 셈인데, 특정 지갑 주소 생성을 유도한다는 것인즉, (마지막 40자리라해도)해시 값의 충돌을 유도한다는 것이고, 해시 값의 충돌이 일어났다는 것인즉, 해당 해시 알고리즘은 더 이상 안전하지 않다는 것을 의미한다.
sha256 해시 알고리즘은 google같은 굴지의 대기업에서도 스펙 짱짱한 컴퓨터로 충돌을 위한 연산을 돌리고있지만 아직 충돌나지 않은걸로 봐서는 개인수준에서 특정 지갑 주소 생성을 유도하는건 앞으로도 웬만하면 없다고 보면되겠다. 차라리 양자컴퓨터가 대중화돼서 공개키로부터 개인키를 연산해낼 수 있으면 몰라

결론

특정 지갑 주소 생성한다는건 결국 두가지 방법이 있겠다.
하나는 특정 지갑 주소가 생성될때까지, 그러니까 해시값 충돌이 발생할 때까지 개인키를 계속 생성해보는것이고 다른 하나는 외부에 노출되는 공개키/지갑 주소를 이용해 개인키를 알아내는 건데, 전자는 해시를 모조리 깨버리는 압도적인 연산속도를 자랑하는 슈퍼컴퓨터가 나와야 확률이 높아지고, 후자는 양자컴퓨터가 나와야 가능하겠다.

번외

지갑을 찾는 Mnemonic?

개인키 만들기가 그렇게 힘든거면 지갑을 잃어버렸을 때 복구를 위해 사용하는 Mnemonic 코드는 어떻게 지갑을 복구할 수 있는걸까?

그 답은 바로 최초 개인키 생성에 사용됐던 난수생성기의 시드 값에 있다.

Mnemonic 코드는 개인키를 만들 때 난수생성기에서 사용됐던 시드 값을 생성하기 위한 단어들인거다.
그렇다면 Mnemonic 코드는 언제 어떻게 생성돼서 어떻게 시드 값을 만드는 걸까?

Mnemonic 코드 생성

개인키를 만들기위해 난수생성기를 사용할 때, 시드가 생성되고, 해당 시드를 통해 Mnemonic 코드가 생성된다고 생각할 수 있지만 그 반대다. Mnemonic 코드가 먼저 생성되고, 생성된 Menemonic 코드를 사용해 시드가 생성된다. 그리고 그 생성된 시드 값을 통해 난수생성기에서 개인키가 생성된다.

  1. 암호학적으로 랜덤한 128~256 bits의 시퀀스 S를 만든다.
  2. S의 SHA-256 해시값 중에서 앞(왼쪽)에서 S의 길이 / 32비트만큼을 체크섬으로 만든다.
  3. 2번에서 만든 체크섬을 S의 끝에 추가한다.
  4. 3번에서 만든 시퀀스와 체크섬의 연결을 11 bits 단위로 자른다.
  5. 각각의 11비트를 2048(2^11)개의 미리 정의된 단어로 치환한다.
  6. 단어 시퀀스로부터 순서를 유지하면서 니모닉 코드를 생성한다.
    이렇게 니모닉코드가 생성되고 이 이후 니모닉코드를 통해 마스터 시드를 생성한다.

Mnemonic 코드로 시드 생성

만들어진 Mnemonic 코드로 시드를 생성하는데 이때 키 스트레칭을 수행하게 된다.
키 스트레칭 함수는 해시를 여러 번 반복해 무작위 대입 공격에 대비하는 것을 의미하는데, 니모닉과 salt를 파라미터로 사용한다. salt는 무차별대입공격을 가능하게 하는 조회 테이블(Lookup Table) 생성을 어렵게한다.

  1. PBKDF2 키 스트레칭 함수에 앞에서 만든 12개의 니모닉코드를 사용한다.
  2. 그리고 salt 값을 사용한다. salt는 상수 문자열 'Mnemonic'에 사용자가 지정한 암호문을 붙인 것이다.
  3. PBKDF2는 니모닉과 솔트를 HMAC-SHA512로 2048번의 해싱해서 512 bits 값을 만들어 내는데, 이 값이 마스터시드이다.

그리고 이 마스터시드를 난수생성기에 넣으면, 난수생성기에서 해당 시드값에 따른 개인키가 나오게 된다.

 


참고

https://borntodevelop.tistory.com/entry/%EC%9D%B4%EB%8D%94%EB%A6%AC%EC%9B%80-%EC%A7%80%EA%B0%91-%EC%A3%BC%EC%86%8CEOA-%EC%83%9D%EC%84%B1-%EC%9B%90%EB%A6%AC-Ethereum-EOA-ethersJS-code-example-by-JS

https://boxfoxs.tistory.com/400

https://brunch.co.kr/@doyoulovez/7

https://potensj.tistory.com/29

https://www.boannews.com/media/view.asp?idx=109580&kind=5

http://wiki.hash.kr/index.php/%EB%8B%88%EB%AA%A8%EB%8B%89#.EC.8B.9C.EB.93.9C_.EC.83.9D.EC.84.B1

https://velog.io/@ham3798/%EB%8B%88%EB%AA%A8%EB%8B%89mnemonic-%EC%BD%94%EB%93%9C

https://jepark-diary.tistory.com/114

Comments