HD 키 생성
HD Wallet (Hierarchical Deterministic wallet)은 하나의 마스터 키로부터 다수의 키들을 계층적으로 파생하는 월렛을 가리킨다. 일반적으로 HD Wallet은 처음에 Mnemonic Code를 생성하고 이로부터 Binary Seed를 도출한 후, 이 Seed로부터 Master Private Key (BIP32 Root Key)를 생성한다. 이 Master Key로부터 여러 Child Key를 만들 수 있고, 다시 각 Child Key로부터 그 밑의 자식 키들을 만들 수 있다. HD Wallet의 키 구조는 전체적으로 보았을 때 하나의 Root Key로부터 시작되는 계층적인 트리구조를 갖게 된다.
HD Wallet에서 계층적인 키 구조를 만드는 기본적인 표준 방안은 BIP32에 정의되어 있다. 기초적인 표준을 정한 BIP32는 이후 좀 더 발전하여 BIP44에 새로운 표준안이 정의되었다. 현재는 BIP32의 계층 구조는 사용하지 않고, BIP44의 계층 구조를 널리 사용한다. 다음은 HD와 관련된 BIP 정보이다.
- BIP39: Mnemonic Code와 Binary Seed를 생성하는 표준
- BIP32: HD Wallet에서의 키 생성과 관련된 기본적인 표준
- BIP44: HD Wallet에서 Multi-Account 계층 구조를 사용하는 표준. 비트코인 이외에 다른 여러 코인들에 대한 키들을 사용할 수 있다. 비트코인의 경우 Non-Segwit 주소에 사용된다.
- BIP49: Segwit에서 P2WPKH-P2SH 계정(Segwit 3* 주소)을 파생하는데 사용된다.
- BIP84: Segwit에서 P2WPKH 계정(bc1* 주소)을 파생하는데 사용된다.
BIP32
BIP32는 HD에서의 계층적인 키 생성 구조를 정의하고 있는데, 키의 계층 구조는 아래와 같은 문법을 사용한다.
master/account/(external|internal)/index 예) m/0'/0/0
- 위의 예(m/0'/0/0)에서 m은 Master Private Key를 가리키는데, Master Public Key인 경우에는 대문자 M을 사용한다.
- 두번째 0' 은 Account 번호를 가리키는 것으로 0h와 같이도 쓰기도 하는데, 이때의 h는 Hardened Key를 가리킨다. Hardened Key는 부모의 private key를 사용하여 파생되는 Child Key를 가리키고, Non-hardend Key는 부모의 public key를 사용하여 파생된다. Hardened Key의 인덱스는 0x80000000으로부터 시작되며, 0' 혹은 0h은 실제 0x80000000을 가리킨다. 마찬가지로 1' 은 0x80000001 을 의미하게 된다.
- 세번째 요소는 해당 키가 (코인을 Receive하기 위한) External key인지, 아니면 내부적으로 사용되는 Change address의 Key (internal key)인지를 구분하는 것이다. 이 값이 0이면 External을 가리키고, 1이면 Internal을 가리킨다.
- 네번째 요소는 일반 파생키의 인덱스를 가리키는 것으로 0부터 시작해서 0xFFFFFFFF 까지의 값을 가질 수 있다. 이는 한 Account 당 최대 0xFFFFFFFF의 External 키를 가질 수 있음을 의미한다.
BIP32는 Child Key를 생성하는 여러 방법을 정의하고 있다:
(1) 부모 private key에서 자식 private key를 생성하는 방법
(2) 부모 private key에서 자식 public key를 생성하는 방법
(3) 부모 public key에서 자식 public key를 생성하는 방법
당연한 얘기이지만, 부모 public key에서 자식 private key를 생성할 수는 없다. 특히, 부모 public key에서 자식 public key를 생성하는 방법은 하드웨어 월렛의 클라이언트(Desktop 혹은 모바일 등. 예를 들어, Ledger Live)에서 private key를 모른 채, 코인 수신을 위한 public key를 생성할 수 있도록 하는데 유용하게 사용된다.
HD에서 마스터 키 혹은 부모 키는 512비트로 되어 있는데, 이를 반으로 쪼개었을 때 앞부분은 Private (혹은 public) Key가 되고, 뒷부분은 Chaincode가 된다.
부모 private 키에서 자식 private 키를 생성하기 위해서는 기본적으로,
(1) 부모 키의 Chaincode를 HMACSHA512의 키로 사용하여 {key + index} 데이타를 해싱한 후,
(2) 해시결과를 다시 둘로 나누고, 앞부분 256비트를 부모 키와 더하여 Child Key를 만든다.
뒷부분 256비트는 Child Chaincode가 된다.
좀 더 구체적인 부분에서 Hardened 인덱스인 경우 (1)에서 {key + index} 데이타 앞에 0x00을 붙이게 되고,
(2)에서 {부모 키 + Left} 결과를 ECDSA의 n으로 모듈러 연산을 하는 등의 내용이 있다.
아래는 위와 같은 절차를 표현한 예이다.
/// Private parent key -> private child key public HDKey ChildPrivateKeyDerivation(HDKey parent, uint index) { List<byte> data = new List<byte>(); var ecdsa = new ECDSA(); if (IsHardened(index)) { // for hardened, privatekey + index data.Add(0x00); data.AddRange(parent.PrivateKey); data.AddRange(BitConverter.GetBytes(index).Reverse()); } else { // for non-hardened, publickey + index data.AddRange(parent.PublicKey); data.AddRange(BitConverter.GetBytes(index).Reverse()); } using var hmac = new HMACSHA512(parent.Chaincode); byte[] child = hmac.ComputeHash(data.ToArray()); var (L, R) = Split(child); var nParKey = new BigInteger(parent.PrivateKey, true, true); var nLeft = new BigInteger(L, true, true); var childKey = (nLeft + nParKey) % ecdsa.ECParams.n; if (nLeft >= ecdsa.ECParams.n || childKey == 0) { throw new ApplicationException("Invalid key. Try next index"); } return new HDKey(childKey.ToByteArray(true, true), R); }