Pebble Coding

ソフトウェアエンジニアによるIT技術、数学の備忘録

ECDSA(secp256k1)での署名と検証をswiftで行う(secp256k1swiftライブラリ使用)

前回、pythonで行なったECDSA(secp256k1)での署名と検証をswiftで行なってみます。

拙作 secp256k1swift

GitHub - pebble8888/secp256k1swift: secp256k1 by pure swift

を用いてみます。
ちなみに、このライブラリはbitcoinのC言語で書かれたライブラリ

GitHub - bitcoin-core/secp256k1: Optimized C library for EC operations on curve secp256k1

をそのままswiftに書き直しただけ(と言いつつめちゃ大変でしたが)なので、使い方の参考になると思います。

ではソースです。

guard var ctx: secp256k1_context = secp256k1_context_create([.SECP256K1_CONTEXT_SIGN, .SECP256K1_CONTEXT_VERIFY]) else { fatalError() }
let seckey: [UInt8] = [0,0,0,0,0,0,0,0,
                       0,0,0,0,0,0,0,0,
                       0,0,0,0,0,0,0,0,
                       0,0,0,0,0,0,0,3]
var pubkey = secp256k1_pubkey()
let result = secp256k1_ec_pubkey_create(ctx, &pubkey, seckey)
XCTAssert(result)
print("\(pubkey)")

let msg32:[UInt8] = [0x9a, 0xf1, 0x5b, 0x33, 0x6e, 0x6a, 0x96, 0x19,
                     0x92, 0x85, 0x37, 0xdf, 0x30, 0xb2, 0xe6, 0xa2,
                     0x37, 0x65, 0x69, 0xfc, 0xf9, 0xd7, 0xe7, 0x73,
                     0xec, 0xce, 0xde, 0x65, 0x60, 0x65, 0x29, 0xa0]
var sig = secp256k1_ecdsa_signature()

let nonce_func: secp256k1_nonce_function = { (
    _ nonce32: inout [UInt8],
    _ msg32: [UInt8],
    _ key32:[UInt8],
    _ algo16:[UInt8]?,
    _ data: [UInt8]?,
    _ counter: UInt
    ) -> Bool in
    guard let data = data else { return false }
    nonce32 = data
    return true
}
// noncedata: BigEndian
let noncedata: [UInt8] = [0,0,0,0,0,0,0,0,
                          0,0,0,0,0,0,0,0,
                          0,0,0,0,0,0,0,0,
                          0,0,0,0,0,0,0,2]
let ret2 = secp256k1_ecdsa_sign(ctx, &sig, msg32, seckey, nonce_func, noncedata)
XCTAssert(ret2)
print("\(sig)")

let ret3 = secp256k1_ecdsa_verify(ctx, sig, msg32, pubkey)
XCTAssert(ret3)

secp256k1_context_destroy(&ctx)
f9 36 e0 bc 13 f1 01 86 b0 99 6f 83 45 c8 31 b5 29 52 9d f8 85 4f 34 49 10 c3 58 92 01 8a 30 f9 72 e6 b8 84 75 fd b9 6c 1b 23 c2 34 99 a9 00 65 56 f3 37 2a e6 37 e3 0f 14 e8 2d 63 0f 7b 8f 38
c6 04 7f 94 41 ed 7d 6d 30 45 40 6e 95 c0 7c d8 5c 77 8e 4b 8c ef 3c a7 ab ac 09 b9 5c 70 9e e5 09 80 93 07 e5 e6 78 cf 6e 55 83 6a 87 05 d1 68 71 a0 40 ea 36 9a 21 a4 27 d2 10 0a 7d 75 de ba

出力を32バイトで区切ったものがこちらです。

f9 36 e0 bc 13 f1 01 86 b0 99 6f 83 45 c8 31 b5 29 52 9d f8 85 4f 34 49 10 c3 58 92 01 8a 30 f9
72 e6 b8 84 75 fd b9 6c 1b 23 c2 34 99 a9 00 65 56 f3 37 2a e6 37 e3 0f 14 e8 2d 63 0f 7b 8f 38
c6 04 7f 94 41 ed 7d 6d 30 45 40 6e 95 c0 7c d8 5c 77 8e 4b 8c ef 3c a7 ab ac 09 b9 5c 70 9e e5
09 80 93 07 e5 e6 78 cf 6e 55 83 6a 87 05 d1 68 71 a0 40 ea 36 9a 21 a4 27 d2 10 0a 7d 75 de ba

seckey=3はbigendianの32バイトで設定しています。
nonce=2もbigendianで設定していますが、
このecdsaライブラリではnonceを生成するクロージャと元となるデータを指定するようになっています。
明示的にnonce=2を設定するには、noncedata引数にnonce=2を与え、
クロージャではデータをそのままセットするようにします。
最初に表示されているのが、公開鍵ですが、このecdsaライブラリでは内部的にbigendianで持っているので、
python版での
pk = (f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9, 388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672)
とは表示が逆になっていますが、一致しています。

次に表示されているのが、署名のrとsです。
bigendianでrが先頭32バイト、次の32バイトがsです。
rはpython版のr = 0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
と一致していますが、
sはpython版のs = 0xf67f6cf81a19873091aa7c9578fa2e96490e9bfc78ae7e9798004e8252c06287
と一致していないように見えます。
これはこのecdsaライブラリでは、rは必ず正の値を返すが、sは大きさが0に近い方の値を返すという実装になっているためです。
0に近いというのは群の位数lのモジュラで考えてということです。
pythonを使って確認してみます。

>>> py_s = 0xf67f6cf81a19873091aa7c9578fa2e96490e9bfc78ae7e9798004e8252c06287
>>> l = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
>>> print("%x" % (py_s-l))
-9809307e5e678cf6e55836a8705d16871a040ea369a21a427d2100a7d75deba

符号を除き一致していることが分かります。
このライブラリではrは正の値を返しますが、sでは値が群の位数の半分より大きい場合、sの代わりに正の値 s' = l-s を返します。
符号が一致していないけど、こんなことしても大丈夫なのか?と気になりますが、結論からいうと署名の検証はパスしますので大丈夫 です。
s'を使って検証してみます。
 u_1' = m {s'} ^ {-1} (\mod n)
 = m \frac {1} {n-s} (\mod n)
 = m {s} ^ {-1} \frac {s} {n-s} (\mod n)
 = m {s} ^ {-1} \frac {s -n} {n-s} (\mod n)
 = -m {s} ^ {-1} (\mod n)となり、値に負号がついただけです。
 u_2'も同じように計算すると結局、
 u_1' G + u_2' A = -1 \cdot nonce G となりますが、楕円曲線の点のマイナスはy座標点の正負を反転させるだけなので、xの値は変わりません。
そのため、検証はパスするというわけです。
ただし、このように使うライブラリ間で、選択する値にいくつか自由度があると、複数ライブラリを使った
際に違いが生じるので気をつける必要があります。
例えば、bitcoinのsecp256k1実装ではrの値が、群の位数の半分より大きな値だと検証を失敗させる実装になっているので、
これと同じ実装になっていないecdsaライブラリを使うと、まともに動かないものになります。

ECDSA(secp256k1)での署名と検証をpythonで行う(外部ライブラリ使用なし)

ECDSA(secp256k1)での署名と検証をpythonで行ってみます。

まずコードです。
ちなみにpython3です。

#!/usr/bin/env python
#
# secp256k1
# http://www.secg.org/SEC2-Ver-1.0.pdf
#  
import sys
import hashlib

sys.setrecursionlimit(1500)

b = 256
# q is prime
q = 2**256 - 2**32 - 977
# l (order of group) is prime
l = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

# no depend q,l
def expmod(v,e,m):
    if e == 0: return 1
    t = expmod(v,e//2,m)**2 % m
    if e & 1: t = (t*v) % m
    return t

# no depend q,l
def inv(x, m):
    return expmod(x,m-2,m)

def double_pt(P):
    x = P[0]
    y = P[1]
    if y == 0: return [0, 0]
    nu = 3*expmod(x,2,q)*inv(2*y,q)
    x3 = expmod(nu,2,q)-2*x
    y3 = nu*(x-x3)-y
    return [x3 % q, y3 % q]

def add_pt(P, Q):
    x1 = P[0]
    y1 = P[1]
    x2 = Q[0]
    y2 = Q[1]
    if x1 == -1 and y1 == -1: return Q
    if x2 == -1 and y2 == -1: return P
    if x1 == x2:
        if (y1 + y2) % q == 0:
            return [-1, -1]
        else:
            return double_pt(P)

    lm = (y1-y2)*inv(x1-x2, q)
    x3 = expmod(lm,2,q)-(x1+x2)
    y3 = lm*(x1-x3)-y1
    return [x3 % q, y3 % q]

def scalarmult(P, e):
    if e == 0: return [-1, -1]
    Q = scalarmult(P, e//2)
    Q = add_pt(Q, Q)
    if e & 1: Q = add_pt(Q, P)
    return Q

def isoncurve(P):
    x = P[0]
    y = P[1]
    return (y**2 - x**3 - 7) % q == 0

Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
G = [Gx, Gy]

# big endian
def bit(h, i):
    return (h[i//8] >> (7-(i%8))) & 1

# 32 byte array to big endian integer
def decodeint(s):
    assert(len(s) == 32)
    return sum(2**(b-1-i) * bit(s,i) for i in range(0,b))

print("q  = %x" % q)
print("Gx = %x" % Gx)
print("Gy = %x" % Gy)
print("l  = %x,%d" % (l, l))
if isoncurve(G):
    print("G is on curve")
else:
    assert False, "G is not on curve!"

T = scalarmult(G, l)
print("T  = (%x, %x)" % (T[0], T[1]) )

sk = 3
PK = scalarmult(G, sk)
print("sk = %x" % sk)
print("pk = (%x, %x)" % (PK[0], PK[1]))

# string to byte array
msg = str("0000").encode('utf-8')

# -- sign

# STEP1
nonce = 2
print("nonce = %d" % nonce)
R = scalarmult(G, nonce) 
r = R[0] % l
# STEP2  check r is not 0
#   not implement
# STEP3

msg_digest = hashlib.sha256(msg).digest()
print("msg_digest:" + msg_digest.hex())
msg_i = decodeint(msg_digest)
print("msg_i:" + str(msg_i))
s = (inv(nonce,l) * (msg_i + r * sk)) % l
# STEP4  check s is not 0
#   not implement

print("r = %d" % r)
print("r = 0x%x" % r)
print("s = %d" % s)
print("s = 0x%x" % s)

# -- verify
si = inv(s,l)
u1 = (msg_i * si) % l
u2 = (r * si) % l
V = add_pt(scalarmult(G, u1), scalarmult(PK, u2)) 
print("V = (%d, %d)" % (V[0], V[1]))
result = (((V[0] - r) % l) == 0)

print("result %d" % result)

rubyの場合でやった

ECDSA(secp256k1)での署名と検証をrubyで行う(ecdsaライブラリ使用) - Pebble Coding

のと同じですが、ここではsecp256k1計算を関数として実装してしています。

q  = fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
Gx = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Gy = 483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
l  = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,115792089237316195423570985008687907852837564279074904382605163141518161494337
G is on curve
T  = (-1, -1)
sk = 3
pk = (f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9, 388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672)
nonce = 2
msg_digest:9af15b336e6a9619928537df30b2e6a2376569fcf9d7e773eccede65606529a0
msg_i:70082618269937651554445935544647643695199256361993126870837541449102805772704
r = 89565891926547004231252920425935692360644145829622209833684329913297188986597
r = 0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
s = 111494102406131234412316855906883406462147064785892425994642684023738105619079
s = 0xf67f6cf81a19873091aa7c9578fa2e96490e9bfc78ae7e9798004e8252c06287
V = (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)

ECDSA(secp256k1)での署名と検証をrubyで行う(ecdsaライブラリ使用)

ECDSAでの署名と検証をrubyで行います。

ECDSAの仕様はこちらです。
ECDSA(楕円曲線デジタル署名アルゴリズム)の概要 - Pebble Coding

それではコードです。

#!/usr/bin/env ruby

require 'ecdsa'
require 'securerandom'
require 'digest'

def bin_to_hex(s)
  s.each_byte.map { |b| b.to_s(16) }.join
end

group = ECDSA::Group::Secp256k1
n = group.order
p "group order:#{n}"

#sk = 1 + SecureRandom.random_number(n-1)
sk = 3 
pk = group.generator.multiply_by_scalar(sk)

p "sk:"
p sk
p "pk:"
p pk

# sign
msg = "0000"
msg_digest = Digest::SHA256::digest(msg)
#nonce = 1 + SecureRandom.random_number(n-1)
nonce = 2 
sig = ECDSA.sign(group,sk,msg_digest,nonce)

p "msg_digest:#{msg_digest}"
p "nonce:#{nonce}"
p "sig.r:#{sig.r},#{sig.r.to_s(16)}"
p "sig.s:#{sig.s},#{sig.s.to_s(16)}"

sig_der = ECDSA::Format::SignatureDerString.encode(sig)

p "sig_der:#{sig_der}"
p "sig_def(hex):#{bin_to_hex(sig_der)}"

# verify
v_digest = OpenSSL::Digest::SHA256.digest(msg)
v_sig = ECDSA::Format::SignatureDerString.decode(sig_der)
result = ECDSA.valid_signature?(pk, v_digest, sig)
p "result:#{result}"

秘密鍵とNonceは通常ランダムに取りますが、ここでは他のライブラリの動作との確認を行いたいので、 秘密鍵は3,Nonceは2としています。

ECDSAの署名を行うライブラリはこちらになります。

https://github.com/DavidEGrayson/ruby_ecdsa

secp256k1のベースポイントGを秘密鍵分だけスカラー倍した点がpk(公開鍵)になります。
メッセージ文字列"0000"をSHA256したバイト列をBigEndianで数値としてものに対して署名します。
BigEndianであることは
AMERICAN NATIONAL STANDARD X9.62-1998
の 4.3.2 Octet-String-to-Integer Conversion の章や
SEC 1: Elliptic Curve Cryptography
の 2.3.8 Octet-String-to-Integer Conversion の章に記述があります。

(r, s)のペアである署名sigと公開鍵(点pk)を使って、メッセージと署名の組みが正しいことを確認しています。

"group order:115792089237316195423570985008687907852837564279074904382605163141518161494337"
"sk:"
3
"pk:"
#<ECDSA::Point: secp256k1, 0xf9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9, 0x388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672>
"msg_digest:\x9A\xF1[3nj\x96\x19\x92\x857\xDF0\xB2\xE6\xA27ei\xFC\xF9\xD7\xE7s\xEC\xCE\xDEe`e)\xA0"
"nonce:2"
"sig.r:89565891926547004231252920425935692360644145829622209833684329913297188986597,c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
"sig.s:111494102406131234412316855906883406462147064785892425994642684023738105619079,f67f6cf81a19873091aa7c9578fa2e96490e9bfc78ae7e9798004e8252c06287"
"sig_der:0F\x02!\x00\xC6\x04\x7F\x94A\xED}m0E@n\x95\xC0|\xD8\\w\x8EK\x8C\xEF<\xA7\xAB\xAC\t\xB9\\p\x9E\xE5\x02!\x00\xF6\x7Fl\xF8\x1A\x19\x870\x91\xAA|\x95x\xFA.\x96I\x0E\x9B\xFCx\xAE~\x97\x98\x00N\x82R\xC0b\x87"
"sig_def(hex):30462210c647f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac9b95c709ee52210f67f6cf81a19873091aa7c9578fa2e9649e9bfc78ae7e979804e8252c06287"
"result:true"

参考:
http://www.secg.org/sec1-v2.pdf

python3 で hmac-sha512を計算する

python3.6.4を用いています。

import hmac
import hashlib

sk = str("secret").encode('utf-8')
msg = str("message").encode('utf-8')
sig = hmac.new(sk, msg, hashlib.sha512).hexdigest()
print(sig)
print(len(sig))
1bba587c730eedba31f53abb0b6ca589e09de4e894ee455e6140807399759adaafa069eec7c01647bb173dcb17f55d22af49a18071b748c5c2edd7f7a829c632
128

結果は512ビット= 64バイト = 128文字 となります。

python素晴らしい。

bitcoin-cli コマンドまとめ その2

ウォレットのダンプ

$ bitcoin-cli dumpwallet a.txt
{
  "filename": "/Users/pebble8888/Library/a.txt"
}

第2引数には出力先のファイル名をいれます。

約4000行のファイル名が出力されました。先頭を少し加工して見てみます。
(ちなみにtestnet環境のものですのでそのままいじって遊ばないで下さい。)

# Wallet dump created by Bitcoin v0.16.99.0-d3f4dd313
# * Created on 2018-04-05T13:17:09Z
# * Best block at time of backup was 1291481 (000000000000024590e621b6362a3eea59d29b76d43423f2960651f226461ca0),
#   mined on 2018-04-05T13:14:36Z

# extended private masterkey: tprv8ZgxMBicQKsPe7smRumxgPZreT46s1BdRAsRpWHnmRKafcaXt7RshDhxJTGFoSgpbD9TNieGmrBiy2r6DzsSSJbG9d819eYw3yiYYb6Qo3C

cNzp...cnXy 2018-03-24T13:26:24Z reserve=1 # addr=2NDv...FYx9 hdkeypath=m/0'/0'/16'
cSYS...dUrH 2018-03-24T13:26:24Z label= # addr=2N5RhfHFhkUMaendEoT73qucyt8HQrkPNiQ hdkeypath=m/0'/0'/1'
cUyVUAraSKvFG7j9dEHqLGa8oV7EkgSDxR2xipXX6wXJyEcW2h8o 2018-03-24T13:26:24Z label=pebble8888 # addr=2N8KUQtPNutF1BpGnPL2se5ALoQGt1mpDzr hdkeypath=m/0'/0'/2'
cViF...DVZD 2018-03-24T13:26:24Z label= # addr=2MtyikCgwE51yhy8gUtSpH2oegHXTVny3qY hdkeypath=m/0'/0'/8'
cTUa...nUD9 2018-03-24T13:26:24Z reserve=1 # addr=2N6rCaedYJ5ZywQxxCsTaXkTxLktoRZCxfw hdkeypath=m/0'/0'/15'
cVRJ...HZtw 2018-03-24T13:26:24Z label=pebble9999 # addr=2Mvnh3VC18dnFDn1YztruJkR2zobPRBXhfQ hdkeypath=m/0'/0'/5'
cRZe...YSYZ 2018-03-24T13:26:24Z reserve=1 # addr=2Msn8SsDKQLWfvP8qK8qZDjKHqDGjxnC6zp hdkeypath=m/0'/0'/11'
cQWK...skZJ 2018-03-24T13:26:24Z label=pebble8888 # addr=2NC9peTSrrKPxxJWY4USSs2ZBrtbjJ3CX8c hdkeypath=m/0'/0'/3'
cUcN...KPS6 2018-03-24T13:26:24Z label=pebble9999 # addr=2N8qdpyMDGeqtoeE96qccSX8Ad5TbPce3QB hdkeypath=m/0'/0'/4'
cUmp...MSUS 2018-03-24T13:26:24Z label=-help # addr=2MzyD1VkGc1QvoiYEm5RyVYyTiMQcsAvcdz hdkeypath=m/0'/0'/7'
cTU4...BHS9 2018-03-24T13:26:24Z reserve=1 # addr=2NGGcSV4MD53qRumXtV4FkvM9r1BKxJJSGH hdkeypath=m/0'/0'/14'
cMzVC...MVVe 2018-03-24T13:26:24Z reserve=1 # addr=2N9EWeRooGsSiHS67S7QXniRGXeiggvKAwr hdkeypath=m/0'/0'/12'
cPk7...9kiT 2018-03-24T13:26:24Z label=--help # addr=2N4f2V2GVZJ3pnPaLjvkanGvDpwhssdknba hdkeypath=m/0'/0'/6'
cRuJ...Z86m 2018-03-24T13:26:24Z label= # addr=2N4x25SQPaYbVcvoQihznvGk4ktyJ537VRa hdkeypath=m/0'/0'/9'
cRt5j...iyme 2018-03-24T13:26:24Z label=pebble8888 # addr=2N1fdju35ciYDrfBtFVX1YhEqjPrcyrbCib hdkeypath=m/0'/0'/0'
cNSZ...7QTo 2018-03-24T13:26:24Z reserve=1 # addr=2NCrTNPirBUUW39TBJPjuyXg3Pjrx9mUFCQ hdkeypath=m/0'/0'/13'
cN9K88fgqVpupXHTtTcnHmbhjG3ivMt9mZPuzFREtdxStz2rmDqn 2018-03-24T13:26:24Z hdmaster=1 # addr=2NBuWcobChV5SfCaY99QvK7QLaAU5abcBLh hdkeypath=m
cScX...B2CE 2018-03-24T13:26:24Z reserve=1 # addr=2N5XwLTkR4q1LELVb5bSVpWSHvwFwgEKMsv hdkeypath=m/0'/0'/17'
cPf6...FSgQ 2018-03-24T13:26:24Z label=--help # addr=2My3kxrwBJh53prxewAnr1Jr9w43SkAsSP4 hdkeypath=m/0'/0'/10'
cPum...4jfk 2018-03-24T13:26:25Z reserve=1 # addr=2N75DgvPmps2Hapfpdb7hSzHcf4juq9eW3s hdkeypath=m/0'/1'/852'
cP7F...YPA7 2018-03-24T13:26:25Z reserve=1 # addr=2MyoRQ17mjY96pRDFQYWumNqwRyi8bhLqUL hdkeypath=m/0'/1'/822'

extended private master keyというのは111桁あります。 3行目の"cUyV"から始まる行のaddr=の部分 2N8KUQtPNutF1BpGnPL2se5ALoQGt1mpDzr は2で始まる35桁の文字列で、testnetのP2SH(pay to script hash)のビットコインアドレスです。
cUyVUAraSKvFG7j9dEHqLGa8oV7EkgSDxR2xipXX6wXJyEcW2h8o
の部分は52桁の文字列で、testnetでの圧縮WIF(Wallet Import Format)形式の秘密鍵です。
秘密鍵の先頭のprefixは以下の通りです。

種類 prefix base58 prefix
WIF 秘密鍵 mainnet 0x80 5
圧縮WIF 秘密鍵 mainnet 0x80 K or L
WIF 秘密鍵 testnet 0xef 9
圧縮WIF 秘密鍵 testnet 0xef c

hdkeypath=m/0'/0'/2' というのは、マスター秘密鍵mから生成された最初の強化された子供(m/0')の最初の強化された孫秘密鍵(m/0'/0')の3番目(m/0'/0'/2')の強化された曽孫秘密鍵であることを示しています。 階層的決定性ウォレット(Hierarchical Deterministic)の説明についてはMasteringBitcoinに詳しい説明があります。

ビットコインとブロックチェーン:暗号通貨を支える技術

ビットコインとブロックチェーン:暗号通貨を支える技術

extended private masterkeyというのは、111桁あり、tprvで始まっています。 BIP32 Extended Keyというものです。
これはtestnetの拡張秘密鍵です。 decodeするのにこちらにサイトを使ってみます。(間違ってもmainnetで利用しているものを突っ込まないように。)

BIP32 - JavaScript Deterministic Wallets

BIP32ドキュメントはこちら https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

拡張鍵はbase58エンコードした時に、人間が読み易い文字列になるように、先頭4バイトのバージョンバイトが定められており、以下のようになっています。

version byte base58
mainnet 公開鍵 0x0488B21E xpub
mainnet 秘密鍵 0x0488ADE4 xprv
testnet 公開鍵 0x043587CF tpub
testnet 秘密鍵 0x04358394 tprv