Adresy a peněženky

Z klíče na adresu, seed fráze, HD peněženky, co je vlastně záloha.

Co se naučíš: Odvodíš adresu z veřejného klíče a pochopíš, proč je zálohou peněženky dvanáct slov a ne soubor.

17 min čtení + cvičeníNavazuje na:🔑 Klíče a podpisy

Veřejný klíč má 65 bajtů a nikdo si ho nepřepíše. Adresa, na kterou se posílají peníze, má 34 znaků a není to veřejný klíč, je to jeho hash s kontrolním součtem. Tahle kapitola vysvětluje, proč se to tak dělá, a proč je zálohou peněženky dvanáct slov, ne soubor.


▶ Spustitelné. Ulož jako adresy.py a pusť python3 adresy.py. Běží okamžitě.


Z klíče na adresu

"""Z veřejného klíče na adresu a zpět nikdy. Plus proč seed fráze funguje.
Pozor: hashlib.new('ripemd160') není zaručeně dostupný (OpenSSL 3 ho odsunul
mezi legacy), takže si ho napíšeme sami. Viz ripemd.py ve vedlejší kapitole."""
import hashlib, hmac
from ripemd import ripemd160

ABC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'   # base58, bez 0OIl

def base58(data: bytes) -> str:
    n = int.from_bytes(data, 'big')
    out = ''
    while n:
        n, zbytek = divmod(n, 58)
        out = ABC[zbytek] + out
    return '1' * (len(data) - len(data.lstrip(b'\x00'))) + out

def adresa(pub_bytes: bytes, verze=b'\x00') -> str:
    """Bitcoinový recept: SHA-256, pak RIPEMD-160, pak verze a kontrolní součet."""
    sha = hashlib.sha256(pub_bytes).digest()
    ripe = ripemd160(sha)                                  # 20 bajtů, odtud délka adresy
    telo = verze + ripe
    kontrola = hashlib.sha256(hashlib.sha256(telo).digest()).digest()[:4]
    return base58(telo + kontrola)

def zkontroluj(adr: str) -> bool:
    """Kontrolní součet odchytí překlep, takže peníze neodejdou do neexistující adresy."""
    n = 0
    for zn in adr:
        if zn not in ABC: return False
        n = n * 58 + ABC.index(zn)
    raw = n.to_bytes(25, 'big')
    telo, kontrola = raw[:21], raw[21:]
    return hashlib.sha256(hashlib.sha256(telo).digest()).digest()[:4] == kontrola

def seed_na_klic(fraze: str, cesta=b'm/0'):
    """Zjednodušené HD odvození: z jedné fráze vznikne libovolně mnoho klíčů."""
    seed = hashlib.pbkdf2_hmac('sha512', fraze.encode(), b'mnemonic', 2048)
    return hmac.new(seed, cesta, hashlib.sha512).digest()[:32]

if __name__ == '__main__':
    pub = bytes.fromhex('0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
                        '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8')
    a = adresa(pub)
    print(f'veřejný klíč (65 B) -> adresa: {a}')
    print(f'kontrolní součet v pořádku:   {zkontroluj(a)}')

    print('\n=== překlep v adrese se pozná ===')
    spatna = a[:-1] + ('X' if a[-1] != 'X' else 'Y')
    print(f'  původní: {a}  -> {zkontroluj(a)}')
    print(f'  s chybou: {spatna} -> {zkontroluj(spatna)}')

    print('\n=== jedna fráze, libovolně mnoho klíčů ===')
    fraze = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
    for i in range(4):
        k = seed_na_klic(fraze, f'm/0/{i}'.encode())
        print(f"  m/0/{i}: {k.hex()[:48]}...")
    print('\n  Proto stačí zapsat dvanáct slov. Klíče se z nich vždy odvodí stejně.')
    print('  A proto je ta fráze celý tvůj majetek: kdo ji má, má všechny klíče.')

Výstup:

veřejný klíč (65 B) -> adresa: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
kontrolní součet v pořádku:   True

=== překlep v adrese se pozná ===
  původní: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm  -> True
  s chybou: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZX -> False

Ta adresa není vymyšlená. Je to adresa odvozená z generátoru křivky secp256k1, tedy z nejznámějšího veřejného klíče na světě. Můžeš si ji vyhledat.


Odbočka: RIPEMD-160 si musíš napsat sám

V kódu je řádek from ripemd import ripemd160 a stojí za vysvětlení, proč tam není hashlib.new('ripemd160').

RIPEMD-160 není mezi zaručenými algoritmy Pythonu. Ověř si to:

import hashlib
print('ripemd160' in hashlib.algorithms_guaranteed)   # False

Jestli funguje, závisí na tom, jak je sestavené OpenSSL na daném systému. OpenSSL 3 ho odsunul mezi takzvané legacy algoritmy, které se ve výchozím nastavení nenačítají, takže na spoustě systémů skončí volání chybou unsupported hash type ripemd160.

Napsat si ho je šedesát řádků a odstraní to závislost na tom, co má kdo nainstalované:

"""RIPEMD-160 v čistém Pythonu. Ve standardní knihovně zaručený není:
hashlib.new('ripemd160') na části systémů spadne, protože OpenSSL 3 ho
přesunul mezi legacy algoritmy. Šedesát řádků to vyřeší jednou provždy."""

# vyber slova zpravy pro kazdy z 80 kroku, leva a prava linka
RL = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
      7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,
      3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,
      1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,
      4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13]
RR = [5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,
      6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,
      15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,
      8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,
      12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11]
SL = [11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,
      7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,
      11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,
      11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,
      9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6]
SR = [8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,
      9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,
      9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,
      15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,
      8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11]
KL = [0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]
KR = [0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]

M = 0xFFFFFFFF
def rol(x, n): return ((x << n) | (x >> (32 - n))) & M

def f(j, x, y, z):
    if j < 16:  return x ^ y ^ z
    if j < 32:  return (x & y) | (~x & z)
    if j < 48:  return (x | ~y & M) ^ z
    if j < 64:  return (x & z) | (y & ~z)
    return x ^ (y | ~z & M)

def ripemd160(data: bytes) -> bytes:
    h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]
    delka = len(data) * 8
    data += b'\x80' + b'\x00' * ((55 - len(data)) % 64) + delka.to_bytes(8, 'little')
    for off in range(0, len(data), 64):
        X = [int.from_bytes(data[off + 4*i:off + 4*i + 4], 'little') for i in range(16)]
        al, bl, cl, dl, el = h                      # leva linka
        ar, br, cr, dr, er = h                      # prava linka
        for j in range(80):
            t = (rol((al + f(j, bl, cl, dl) + X[RL[j]] + KL[j // 16]) & M, SL[j]) + el) & M
            al, bl, cl, dl, el = el, t, bl, rol(cl, 10), dl
            t = (rol((ar + f(79 - j, br, cr, dr) + X[RR[j]] + KR[j // 16]) & M, SR[j]) + er) & M
            ar, br, cr, dr, er = er, t, br, rol(cr, 10), dr
        h = [(h[1] + cl + dr) & M, (h[2] + dl + er) & M, (h[3] + el + ar) & M,
             (h[4] + al + br) & M, (h[0] + bl + cr) & M]
    return b''.join(x.to_bytes(4, 'little') for x in h)

if __name__ == '__main__':
    # oficialni testovaci vektory ze specifikace
    for vstup, ocekavano in [
        (b'',        '9c1185a5c5e9fc54612808977ee8f548b2258d31'),
        (b'a',       '0bdc9d2d256b3ee9daae347be6f4dc835a467ffe'),
        (b'abc',     '8eb208f7e05d987a9b044a8e98c6b087f15a0bfc'),
        (b'message digest', '5d0689ef49d2fae572b881b123a85ffa21595f36'),
        (b'abcdefghijklmnopqrstuvwxyz', 'f71c27109c692c1b56bbdceb5b9d2865b3708dbc'),
        (b'a' * 1000000, '52783243c1697bdbe16d37f97f68f08325dc1528'),
    ]:
        vysledek = ripemd160(vstup).hex()
        popis = vstup[:20].decode('ascii', 'replace') + ('...' if len(vstup) > 20 else '')
        print(f"  {'OK ' if vysledek == ocekavano else 'CHYBA'} {popis!r:30s} {vysledek}")

Spusť ho samostatně a ověří se proti oficiálním testovacím vektorům ze specifikace, včetně milionu písmen a:

  OK  ''                             9c1185a5c5e9fc54612808977ee8f548b2258d31
  OK  'abc'                          8eb208f7e05d987a9b044a8e98c6b087f15a0bfc
  OK  'message digest'               5d0689ef49d2fae572b881b123a85ffa21595f36
  OK  'aaaaaaaaaaaaaaaaaaaa...'      52783243c1697bdbe16d37f97f68f08325dc1528

Mimochodem, tohle je dobrý návyk obecně: u kryptografického kódu si vždycky ověř testovací vektory ze specifikace, ne jen že to něco vrátí. Při psaní téhle implementace jsem měl překlep v jedné z pěti rundovních funkcí a hash vypadal naprosto normálně, jen byl celý špatně. Bez vektorů by to nikdo nepoznal.


Proč se veřejný klíč hashuje

Když bys posílal na veřejný klíč přímo, fungovalo by to. Hashuje se ze tří důvodů:

  1. Kratší. Dvacet bajtů místo pětašedesáti se dá napsat, přečíst do telefonu i vytisknout jako QR kód.
  2. Kontrolní součet. Ty čtyři bajty na konci znamenají, že překlep se pozná dřív, než pošleš peníze do nicoty. Šance, že špatná adresa projde, je asi jedna ku čtyřem miliardám.
  3. Rezerva do budoucna. Dokud z adresy neutratíš, veřejný klíč není zveřejněný, jen jeho hash. Kdyby někdo prolomil eliptické křivky (třeba kvantovým počítačem), nevyužité adresy by měly ještě jednu vrstvu obrany. Adresa, ze které jsi už poslal, tuhle ochranu nemá.

Ten třetí bod je zároveň důvod, proč se doporučuje nepoužívat adresu opakovaně.


Base58: abeceda navržená proti překlepům

Všimni si abecedy v kódu: chybí v ní 0, O, I a l. Není to náhoda. Nula a velké O se v mnoha fontech nedají rozeznat, stejně jako velké I a malé l. Vypuštěním čtyř znaků se odstranila celá kategorie chyb při přepisování.

Je to drobnost, ale je to dobrá ukázka toho, že návrh formátu je taky bezpečnostní práce.


Peněženka není soubor, je to jedna fráze

Nejdůležitější praktická věc celé kapitoly:

=== jedna fráze, libovolně mnoho klíčů ===
  m/0/0: 5ad07e5932d00295b0f290d1a35320975d201a6e55030959...
  m/0/1: 4e77d0146b690b30af822f11c73b9f4c2f1d3f7c65e9976c...
  m/0/2: 277a5fa703dd720a7fb7a18ec878a5c6e845170337e44146...
  m/0/3: e715574cdd688434f01ad3aa30555301c4d112afe91c3dc0...

Z jedné fráze se deterministicky odvodí libovolně mnoho klíčů. Tomu se říká hierarchická deterministická peněženka. Důsledky jsou dva a oba jsou zásadní:

  • Záloha je ta fráze, ne soubor s klíči. Zapsané slova obnoví celou peněženku včetně adres, které v době zápisu ještě neexistovaly.
  • Kdo má frázi, má všechno. Ne jednu adresu, všechny. Proto se fráze nikdy nefotí, nepíše do poznámek v telefonu a nezadává na stránce, která o ni požádá. Legitimní služba tě o seed fázi nikdy nepožádá, protože ji nepotřebuje.

Ta dvanáctislovná fráze není zkratka ani přezdívka. Je to zápis náhodného čísla ve slovníku o 2048 slovech, kde každé slovo nese jedenáct bitů. Dvanáct slov je 132 bitů, z toho čtyři jsou kontrolní součet, takže 128 bitů skutečné náhody.


Cvičení

  1. Kolik různých dvanáctislovných frází existuje? A kolik by jich musel útočník zkusit, aby měl padesátiprocentní šanci?
  2. Kontrolní součet adresy má čtyři bajty. Jaká je šance, že náhodný překlep projde?
  3. Proč legitimní peněženka nikdy nepotřebuje tvoji seed frázi na serveru?
Náčrt řešení: rozbal, až si cvičení zkusíš sám
  1. Existuje jich 2¹²⁸, tedy asi 3,4 × 10³⁸, a zkusit by musel polovinu. Dvanáct slov nese 132 bitů, ale čtyři z nich jsou kontrolní součet, takže skutečné entropie je 128 bitů. Kdyby útočník zkoušel bilion frází za sekundu, trvalo by mu to řádově 10¹⁹ let. Hádání fráze tedy není hrozba. Hrozba je, že ji někde vyfotíš.
  2. Asi jedna ku 2³² = 4 294 967 296. Čtyři bajty jsou 32 bitů a chybná adresa projde jen tehdy, když jí náhodou vyjde správný kontrolní součet. To je dost na to, aby se překlep spolehlivě zachytil, ale pozor: neochrání tě to před adresou, kterou ti podvrhl útočník, protože ta má kontrolní součet platný. Vždycky kontroluj i začátek a konec adresy očima.
  3. Protože z fráze se odvozují privátní klíče a ty nikdy neopouštějí tvoje zařízení. Peněženka potřebuje k zobrazení zůstatku jen veřejné adresy a k odeslání platby podpis, který umí vyrobit lokálně. Server ti může posílat data o blockchainu, ale klíč k tomu nepotřebuje. Každá služba, která o seed frázi požádá, je proto buď scam, nebo tak špatně navržená, že na tom nezáleží.

Shrnutí

  • Adresa je hash veřejného klíče plus verze a kontrolní součet, zapsaný v base58.
  • Hashuje se kvůli délce, kontrole překlepů a rezervě proti prolomení křivek.
  • Base58 vynechává znaky 0OIl, aby se nedaly zaměnit při přepisu.
  • HD peněženka odvodí z jedné fráze libovolně mnoho klíčů, takže zálohou je ta fráze.
  • Dvanáct slov je 128 bitů náhody. Uhádnout to nejde, vyfotit ano.
Proč je adresa hash veřejného klíče, a ne veřejný klíč samotný?

Kvůli třem věcem: je kratší a přepisovatelná, obsahuje kontrolní součet, který odchytí překlep, a dokud z adresy neutratíš, není veřejný klíč vůbec zveřejněný, jen jeho hash. To je jedna vrstva obrany navíc, pokud by někdo v budoucnu prolomil eliptické křivky. Proto se taky nedoporučuje používat jednu adresu opakovaně.

Co je seed fráze a proč je zálohou právě ona?

Je to zápis náhodného 128bitového čísla pomocí dvanácti slov ze slovníku o 2048 položkách. Z toho čísla se deterministicky odvozují všechny klíče peněženky, takže fráze obnoví i adresy, které při jejím zápisu ještě neexistovaly. Zároveň platí, že kdo frázi má, má celou peněženku, ne jednu adresu.

Ochrání tě kontrolní součet v adrese před tím, že ti někdo adresu podstrčí?

Ne. Kontrolní součet zachytí jen náhodný překlep, protože podvržená adresa od útočníka má kontrolní součet samozřejmě platný. Proti podstrčení pomáhá jen ověření adresy jiným kanálem a vizuální kontrola začátku i konce, případně použití jmenných služeb s vlastním ověřením.