Posted at

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

More than 1 year has passed since last update.


背景

各ビットコインウォレットサービスより、

拡張公開鍵や拡張秘密鍵を取得することができたりするが、

その鍵を分解して実際の公開鍵や秘密鍵、チェーンコードを取得する方法が不明だったので、

調べてみた。


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


拡張鍵について

例えば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が存在する。

構成要素を調べたところ、こちらのサイトから以下の表を得られた。

Screenshot from 2018-05-04 12-43-31.png

鍵部分が33bytesになっているが、

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

それよりも、上記表のバイト数を合計しても

「4+1+4+4+32+33 = 78bytes」にしかならず、4bytes足りない。。。


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

82bytes中78bytesまでの正体はわかったが、残りの4bytesが何かわからない。。。

英語のサイトをあさりはじめ、こちらのサイトに答えが載っていた。

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

Screenshot from 2018-05-04 12-49-09.png

箇条書きのすぐ下のパラグラフに、

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";