BIP32 拡張鍵の文字列から、秘密鍵・公開鍵やチェーンコードを抽出する


背景

各ビットコインウォレットサービスより、
拡張公開鍵や拡張秘密鍵を取得することができたりするが、
その鍵を分解して実際の公開鍵や秘密鍵、チェーンコードを取得する方法が不明だったので、
調べてみた。

拡張鍵の中身を分解していく

拡張鍵について

例えばbcwallet等でウォレットを新規作成した場合、
BIP32に基づく拡張秘密鍵/拡張公開鍵が得られる。

拡張公開鍵の例は以下のようになる。

tpubD6NzVbkrYhZ4WsMXiXFSfYkBoJxysBZv1PD79BPS72LTEiCfnf3Qenh5Z173CwfEdw5keu4GAAWum6J4mu1suKL3CWCBCnTB7NFMf3DjNfS

なお、頭文字4文字(プレフィックス)で拡張鍵の種類が分かるようになっている。詳細は以下参照。

プレフィックス 16進数表記 (※1) 鍵の種類
xpub 0x0488B21E 拡張公開鍵(メインネット)
xprv 0x0488ADE4 拡張秘密鍵(メインネット)
tpub 0x043587CF 拡張公開鍵(テストネット)
tprv 0x04358394 拡張秘密鍵(テストネット)

※1 ビットコインのアドレスは、16進数文字列をbase58checkという形式でエンコードしている。
エンコード前の表記を上記に乗せている。例えばxpubをbase58checkでデコードすると
0x0488B21E になるということ。

拡張鍵=> 鍵情報(256bits)+チェーンコード(256bits)

ビットコインの鍵は公開鍵も秘密鍵も通常256bitsである。

先程の例の拡張公開鍵と公開鍵を比較すると、
以下のようになるが、明らかに拡張公開鍵の方が長い。

#公開鍵
217ZvQxj8hRf2VBBEgSAApC3555vj8jmvDMG61aCKepgN

#拡張公開鍵
tpubD6NzVbkrYhZ4WsMXiXFSfYkBoJxysBZv1PD79BPS72LTEiCfnf3Qenh5Z173CwfEdw5keu4GAAWum6J4mu1suKL3CWCBCnTB7NFMf3DjNfS

そこで、著名な本であるMastering Bitcoinを調べてみると、拡張公開鍵には、鍵情報に加えてチェーンコード(256bits)が含まれていることも分かる。

すなわち「拡張鍵 => 鍵情報(256bits) + チェーンコード(256bits)」となる。
ここにプレフィックス(4bytes)の情報を加えれば、拡張鍵は最低68bytesという長さになる。

拡張鍵は68bytesではなく、82bytes、、、

前出の「※1」にも記載したが、
ビットコインのアドレスは、base58checkという方式でエンコードされている。

そのため、先程提示した拡張公開鍵をbase58checkでデコードしてみる。
プレフィックスが32bits,鍵情報が256bits、チェーンコードが256bitなので、
合計68bytesになることを期待したが、82bytesとなった。

0x043587cf00000000000000000035390470be22cc0d18aa6f6d53fd7900ff53446a2cb6075de1c65a5fc8432ed3035f748ce5c61c9991dd5451dbdcb2c664d1cf9a893829a6986c5180ec7fbbcc8f47b766b9

82bytes - 68bytes = 残りの14bytesは何か?

ここまでの情報だと14bytes分謎のbytesが存在する。
構成要素を調べたところ、こちらのサイトから以下の表を得られた。

鍵部分が33bytesになっているが、
おそらくcomporessed鍵となっており、1bytes増えているのだと思う。

それよりも、上記表のバイト数を合計しても
「4+1+4+4+32+33 = 78bytes」にしかならず、4bytes足りない。。。

最後の4bytesはチェックサム

82bytes中78bytesまでの正体はわかったが、残りの4bytesが何かわからない。。。
英語のサイトをあさりはじめ、こちらのサイトに答えが載っていた。

該当の箇所のキャプチャを以下に抜粋する。

箇条書きのすぐ下のパラグラフに、
78bytesに加えて32bitsのチェックサムが追加されているよ、と書いてある。
チェックサムの作り方は、78bytesのデータをSHA256で2回ハッシュしましょうね、ということ。

試しに、前半78bytesをSHA256でダブルハッシュしたところ、
ハッシュ値の上位4bytesと、拡張公開鍵をデコードした文字列の下位4bytesが見事に一致した。

//前半78bytesをSHA256でダブルハッシュした16進数値
0x47b766b959dfa60a0e2927b46052837d8cb3a6f988223845f18ced4c770db41c
→ (上位4bytes) 47b766b9

//拡張公開鍵をデコードした文字列
0x043587cf00000000000000000035390470be22cc0d18aa6f6d53fd7900ff53446a2cb6075de1c65a5fc8432ed3035f748ce5c61c9991dd5451dbdcb2c664d1cf9a893829a6986c5180ec7fbbcc8f47b766b9
→(下位4bytes) 47b766b9

日本語でまとまった情報がなく、回り道をしてしまったが、
最終的にこのように各要素を分解できた。

拡張鍵をインプットすると、各要素に分解してくれるコード書いた

最後に拡張鍵を各要素に分解するコードを書いたので、
以下に示す。言語はPHP。

補足

bitwasp/bitcoin-phpというパッケージを利用している。require('./vendor/autoload.php') で呼び出しているのがそれです。

extendedKey.php

use BitWasp\Bitcoin\Base58;
use BitWasp\Bitcoin\Crypto\Hash;

require('./vendor/autoload.php');

class ExtendedKey {

  private $keyString;
  private $version;
  private $depth;
  private $fingerprint;
  private $childNumber;
  private $chainCode;
  private $key;
  private $checksum;

  public function __construct($keyString){

    $this->keyString = $keyString;

    try {

      if (strlen($keyString) > 112) {
        throw new \Exception('[ERROR]the key string exceeds maximum size'."\n");
      }

      if (!in_array(substr($keyString,0, 4), ['xpub', 'xprv', 'tpub', 'tprv'])) {
        throw new \Exception('[ERROR]the key string is invalid'."\n");
      }

      $decoded = (new Base58())->decode($keyString);
      $keyMain = $decoded->slice(0,78);
      $this->checksum = $decoded->slice(78,4);

      //$keyStringをbase58デコードした文字列において、
      //下位4バイトが上位78バイトのチェックサムになっているので比較。
      $hash        = (Hash::sha256(Hash::sha256($keyMain)));
      $checkBytes  = $hash->slice(0,4);

      print_r($hash);

      if(!$checkBytes->equals($this->checksum)){
        throw new \Exception('[ERROR]the key string is invalid'."\n");
      }

      $this->version     = $keyMain->slice(0,4);
      $this->depth       = $keyMain->slice(4,1);
      $this->fingerprint = $keyMain->slice(5,4);
      $this->childNumber = $keyMain->slice(9,4);
      $this->chainCode   = $keyMain->slice(13,32);
      $this->key         = $keyMain->slice(45,33);

    }catch(Exception $e){
      echo $e->getMessage();
      exit();
    }
  }

  //以下はただのゲッター
  public function getVersion(){
    return $this->version->getHex();
  }

  public function getDepth(){
    return $this->depth->getHex();
  }

  public function getFingerprint(){
    return $this->fingerprint->getHex();
  }

  public function getChildNumber(){
    return $this->childNumber->getHex();
  }

  public function getChainCode(){
    return $this->chainCode->getHex();
  }

  public function getKey(){
    return $this->key->getHex();
  }

  public function getChecksum(){
    return $this->checksum->getHex();
  }

}
利用例.php
require('./extendedKey.php');

$key = 'tpubD6NzVbkrYhZ4WsMXiXFSfYkBoJxysBZv1PD79BPS72LTEiCfnf3Qenh5Z173CwfEdw5keu4GAAWum6J4mu1suKL3CWCBCnTB7NFMf3DjNfS';

$exKey = new ExtendedKey($key);

//043587cf
echo $exKey->getVersion()         . "\n";

//00
echo $exKey->getDepth()           . "\n";

//00000000
echo $exKey->getFingerprint()     . "\n";

//00000000
echo $exKey->getChildNumber()     . "\n";

//35390470be22cc0d18aa6f6d53fd7900ff53446a2cb6075de1c65a5fc8432ed3
echo $exKey->getChainCode()       . "\n";

//035f748ce5c61c9991dd5451dbdcb2c664d1cf9a893829a6986c5180ec7fbbcc8f
echo $exKey->getKey()             . "\n";

//47b766b9
echo $exKey->getChecksum()        . "\n";