背景
各ビットコインウォレットサービスより、
拡張公開鍵や拡張秘密鍵を取得することができたりするが、
その鍵を分解して実際の公開鍵や秘密鍵、チェーンコードを取得する方法が不明だったので、
調べてみた。
拡張鍵の中身を分解していく
拡張鍵について
例えば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') で呼び出しているのがそれです。
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();
}
}
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";