Ethereum HD-Wallet in JavaScript: Baumstruktur und Ableitungspfad [Teil 5]

In diesem Artikel schauen wir uns an, wie man anhand von mehreren Ableitungen von Kind-Schlüsseln eine Baumstruktur erstellt, die dem BIP 32 folgt und wie man innerhalb dieser Baumstruktur mittels eines Ableitungspfades gem. BIP 44 Schlüssel identifizieren bzw. wiederfinden kann. Zudem werden wir hierzu eine Implementierung mittels der bereits eingeführten JavaScript-Bibliothek hdkey vornehmen und unser konsolenbasiertes HD-Wallet entsprechend weiterentwickeln.

Schauen wir uns zunächst wieder unsere Übersichtsfolie an. Zuvor haben wir bereits unsere Master-Schlüssel generiert, weswegen dieser Teil hier bereits grün markiert ist. Die Ableitung der privaten Schlüssel oder genauer gesagt mehrerer privater Kind-Schlüssel anhand eines Ableitungspfades, wird in diesem Artikel behandelt. Wir benutzen weiterhin die JavaScript-Bibliothek hdkey, welche die BIP32 und 44 entsprechend umsetzt.

Die verwendeten BIPs und Bibliotheken zur Abbildung findet ihr am Ende des Artikels.

Baumstruktur

Ein HD-Wallet besteht nicht aus nur einer Ableitung von Kind-Schlüsseln, sondern hat eine Baumstruktur, wobei der erweiterte Masterschlüssel die Wurzel darstellt und ca. vier Milliarden Kind-Schlüssel haben kann.

Wir hatten bereits erwähnt, dass zur Ableitung eines Kind-Schlüssels so genannte Indizes genutzt werden, um eine Vielzahl an unterschiedlichen Schlüssel abzuleiten. Insgesamt können so von jedem Eltern-Schlüssel ca. 4 Milliarden Schlüssel abgeleitet werden. Genau genommen hatten wir im letzten Artikel davon gesprochen, dass für die normale Ableitung Indizes von 0 bis kleiner 2 hoch 31, also ca. 2 Milliarden verwendet werden. Für die gehärtete Ableitung werden Indizes ab 2 hoch 31 bis 2 hoch 32 -1 vergeben, also nochmal ca. 2 Milliarden Schlüssel.

Wichtig an dieser Stelle ist, dass der Index für normalen Schlüssel bei 0 startet und der Index für gehärteten Ableitung bei 2 hoch 31. Dies können wir auch dem BIP 32 an der entsprechenden Stelle unter “erweiterte Schlüssel” entnehmen.

Diese insgesamt vier Milliarden Kind-Schlüssel können dann wiederum als Eltern-Schlüssel fungieren und wieder jeweils vier Milliarden Kind-Schlüssel haben. Diesen Baum kann man endlos weiter in die Tiefe führen, sodass wir quasi eine endlose Zahl von Schlüsseln ableiten können.

Indizes

Werfen wir noch einmal einen Blick auf die Indizes. Bei der normalen Ableitung stellt sich das Ganze in der Praxis in der Regel einfach da: Man beginnt bei 0 und häufig benötigt man keine große Anzahl an verschiedenen Schlüsseln, sodass man den theoretischen Maximalwert von über zwei Milliarden auch nie erreichen wird. Anders sieht es bei der gehärteten Ableitung aus, da dort der Index bereits bei 2 hoch 31 beginnt.

Da man diese große Zahl nicht immer ausschreiben möchte, hat man sich darauf geeinigt, dass man diese Zahl stellvertretend mit einer 0 ’ (mit einfachen Anführungszeichen) kennzeichnet. Der Wert 0’ ist also letzten Endes für Entwickler und Anwender lediglich eine kürzere Schreibweise für den Wert 2 hoch 31. Intern wird 0’ natürlich von den verwendeten Software-Bibliotheken in den entsprechenden Wert umgewandelt. Zählt man die Werte dann weiter hoch, kennzeichnet man diese ebenfalls mit einem einfachen Anführungszeichen, also z.B. 1′ (mit einfachen Anführungszeichen). Wir kommen darauf später noch einmal anhand eines konkreten Beispiels zu sprechen.

Ableitungspfad

Um sich in der zuvor dargestellten Baumstruktur zurecht zu finden und Schlüssel zu identifizieren bzw. wiederzufinden, hat man sich bei den verschiedenen HD-Wallets über das BIP 44 auf einen Ableitungspfad hin zu den Schlüsseln geeinigt. Für Ethereum sieht ein solcher Pfad bspw. wie folgt aus:

m/44'/60'/0'/0/0

Schauen wir uns einmal genauer die Bedeutungen der einzelnen Ebenen an.

m /

Das kleine m kennzeichnet eine Ableitung vom erweiterten privaten Master-Schlüssel und die einzelnen Schrägstriche kennzeichnen die verschiedenen Ebenen der zuvor angesprochenen Baumstruktur.

Zunächst einmal fällt das einfache Anführungszeichen an einigen Stellen des Pfades auf. Da der Bereich für die gehärteten Kind-Schlüssel bei 2^31 beginnt, hat man sich, zwecks einfacher Lesbarkeit darauf geeinigt, den Bereich für gehärtete Kind-Schlüssel mit einem einfachen Anführungszeichen ‘ darzustellen.

/ purpose’

Das BIP 43 schlägt vor, dass die erste Ebene der Baumstruktur von gehärteten Kind-Schlüsseln dazu genutzt wird, den Zweck bzw. die BIP-Nummer, an welche man sich hält, anzugeben und somit nur einen Zweig dieser Ebene zu verwenden. Benutzt man in einem HD-Wallet bspw. einen Pfad, wie er im BIP 44 beschrieben wird, so gibt man auf der ersten Ebene der Baumstruktur 44’ an.

/ coin_type’ /

Die nächste Ebene gibt den Coin-Typ, den wir verwenden, an. In unserem Fall geben wir an dieser Stelle 60’ ein, da wir Schlüssel für Ethereum generieren möchten. Die verschiedenen Coin-Typen werden in den so genannten SatoshiLabs Improvement Proposals, welche eine Erweiterung der BIPs darstellen und Informationen enhalten, die nicht im direkten Zusammenhang mit Bitcoin stehen. Das SLIP 44 [8] hat alle dort registrierten Coins aufgelistet. Wir sehen hier, dass Ethereum den Wert 60’ hat.

/ account’ /

Diese Ebene des Baums ist für organisatorische Zwecke vorgesehen, sodass man seine Schlüssel bspw. in Schlüssel für Geschäftszwecke und Schlüssel für private Zwecke unterteilen kann.

/ change /

Die Vorletzte Ebene des Ableitungspfades ist nur für Bitcoin relevant, da Bitcoin Wechselgeld-Adressen benötig. Ethereum hingegen rechnet in Konten, sodass keine Wechselgeld-Adressen benötigt werden.

/ address_index /

Die letzte Ebene erlaubt es uns durch einfaches Hochzählen neue private Schlüssel und damit letzten Endes neue Ethereum-Adressen zu generieren.

Ethereum-Ableitungspfad im Detail

Gucken wir uns diesen hieraus resultierenden Pfad bzw. die hieraus resultierenden Indizes dieses Pfades noch einmal genauer an.

Der erste gelbe Schlüssel stellt den bereits besprochenen erweiterten privaten Master-Schlüssel dar. Die erste Ableitung benutzt eine gehärtete Ableitung. Dies ist durch das einfache Anführungszeichen ersichtlich. Konkret bedeutet dies, dass der Schlüssel 44’ hier einen Index von 44 + 2^31 hat. Die darauffolgende Ebene hat den Wert 60’, was bedeutet, dass der Index dieses Schlüssels 60 + 2 hoch 31 entspricht. Anschließend kommt noch die dritte Ableitung, welche dann entsprechend bei 2 hoch 31 beginnt. Sodann folgen nur noch normale Ableitungen, welche entsprechend bei dem Index 0 beginnen.

Hierdurch ist es auf diesen Ebenen möglich, erweiterte öffentliche Schlüssel zu exportieren, um diese bspw. in unsicheren Umgebungen, wie einen Webserver zur Generierung neuer öffentlicher Schlüssel zu verwenden. Durch die Verwendung einer gehärteten Ableitung oberhalb der Ebene, auf der erweiterte öffentliche Schlüssel (mit Chain-Code) exportiert werden können, sind die privaten Eltern-Schlüssel der darüberliegenden Ebene entsprechend geschützt.

Implementierung

Schauen wir zunächst einmal, wie man in der Praxis einen Kind-Schlüssel mittels des zuvor beschriebenen Ableitungspfades erzeugt. Wir werden dann anschließend auch die Berechnungen der Indizes, welche wir soeben für die gehärtete Ableitung durchgeführt haben, einmal ausgeben lassen.

Bevor wir diese Indizes mit unserer verwendeten Bibliothek jedoch validieren, müssen wir mittels der derive-Funktion der Bibliothek einen Kind-Schlüssel erstellen, indem wir dieser Funktion einen solchen Pfad übergeben.

Um das Ganze zu demonstrieren fangen wir mit einem kleinen Beispiel an, indem wir als Pfad lediglich den Buchstaben klein m übergeben und somit wieder unseren Master-Schlüssel den wir bereits haben, zurück geliefert bekommen – hier findet also tatsächlich noch keine Ableitung statt.

    let childKey = myHDKey.derive("m");
    
    console.log('0x'+ childKey.privateKey.toString('hex'))
    console.log('0x'+ childKey.publicKey.toString('hex'))

Das Ergebnis der derive-Funktion speichern wir uns in der Variable childKey ab. Über ein entsprechendes Console.log können wir uns dann wieder unseren bereits bekannten Master-Schlüssel und auch den zugehörigen öffentlichen Master-Schlüssel ausgeben lassen. Dieses Beispiel leitet natürlich noch keinen kein Kind-Schlüssel ab und sollte lediglich nochmal verdeutlichen, dass derive-Funktion abhängig vom übergeben Ableitungspfad ist, welcher in diesem Fall eine Ableitung gar nicht erst nötig macht.

Um einen Schlüssel für Ethereum zu erhalten, passen wir den Pfad entsprechend unserer obigen Folie an:

let childKey = hdKey.derive("m/44'/60'/0'/0/0");

Nun erhalten wir einen privaten Schlüssel, welchen wir auch für Transaktionen auf der Ethereum-Blockchain nutzen könnten.

Weiterführende Details zur Implementierung in der hdKey-Bibliothek

Zunächst überprüfen wir die Indizes auf unserer Folie, indem wir uns den Code der verwendeten Bibliothek genauer anschauen und überprüfen, ob insbesondere die Werte der gehärteten Ableitungen mit den Berechnungen in unserer Folie übereinstimmen.

var HARDENED_OFFSET = 0x80000000

Die verwendete Bibliothek hat hierfür eine Konstante namens HARDENED_OFFSET festgelegt und den entsprechenden Wert im Hex Format gespeichert. Diesen können wir einfach in einen Dezimalwert umrechnen (2.147.483.648). Jetzt müssen wir nur noch die 44 addieren und schon erhalten wir den gleichen Index für die erste gehärtete Ableitung wie auf unserer Folie (2.147.483.692).

Um das Ganze noch einmal zu verdeutlichen, können wir eine entsprechende Ausgabe in der Bibliothek anlegen, welche jeweils bei der Ableitung eines Kind-Schlüssels zeigen, auf welcher Ebene des Baums sich die Ableitungsfunktion gerade befindet und ob es sich um eine gehärtete oder normale Ableitung handelt (Zeile 18 + Zeile 25).

let countLevel = 1;
HDKey.prototype.deriveChild = function (index) {
  var isHardened = index >= HARDENED_OFFSET
  var indexBuffer = Buffer.allocUnsafe(4)
  indexBuffer.writeUInt32BE(index, 0)

  var data
  
  if (isHardened) { // Hardened child
    assert(this.privateKey, 'Could not derive hardened child key')

    var pk = this.privateKey
    var zb = Buffer.alloc(1, 0)
    pk = Buffer.concat([zb, pk])

    // data = 0x00 || ser256(kpar) || ser32(index)
    data = Buffer.concat([pk, indexBuffer])
    console.log("Gehärtete Ableitung auf Ebene: " + countLevel + " Dieser Schlüssel hat folgenden Index: " + index)

    countLevel++;
  } else { // Normal child
    // data = serP(point(kpar)) || ser32(index)
    //      = serP(Kpar) || ser32(index)
    data = Buffer.concat([this.publicKey, indexBuffer])
    console.log("Normale Ableitung auf Ebene: "+ countLevel + " Dieser Schlüssel hat folgenden Index: " + index)
    countLevel++;
  }

  var I = crypto.createHmac('sha512', this.chainCode).update(data).digest()
  var IL = I.slice(0, 32)
  var IR = I.slice(32)

  var hd = new HDKey(this.versions)

  // Private parent key -> private child key
  if (this.privateKey) {
    // ki = parse256(IL) + kpar (mod n)
    try {
      hd.privateKey = Buffer.from(secp256k1.privateKeyTweakAdd(Buffer.from(this.privateKey), IL))
      // throw if IL >= n || (privateKey + IL) === 0
    } catch (err) {
      // In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i
      return this.deriveChild(index + 1)
    }
  // Public parent key -> public child key
  } else {
    // Ki = point(parse256(IL)) + Kpar
    //    = G*IL + Kpar
    try {
      hd.publicKey = Buffer.from(secp256k1.publicKeyTweakAdd(Buffer.from(this.publicKey), IL, true))
      // throw if IL >= n || (g**IL + publicKey) is infinity
    } catch (err) {
      // In case parse256(IL) >= n or Ki is the point at infinity, one should proceed with the next value for i
      return this.deriveChild(index + 1)
    }
  }

  hd.chainCode = IR
  hd.depth = this.depth + 1
  hd.parentFingerprint = this.fingerprint// .readUInt32BE(0)
  hd.index = index

  return hd
}

Zusätzlich werden hier die entsprechenden Indizes noch einmal ausgegeben, sodass wir sehen, dass diese identisch mit den Berechnungen in unserer Folie sind. Das Ergebnis sieht dann wie folgt aus:

Gehärtete Ableitung auf Ebene: 1 Dieser Schlüssel hat folgenden Index: 2147483692
Gehärtete Ableitung auf Ebene: 2 Dieser Schlüssel hat folgenden Index: 2147483708
Gehärtete Ableitung auf Ebene: 3 Dieser Schlüssel hat folgenden Index: 2147483648
Normale Ableitung auf Ebene: 4 Dieser Schlüssel hat folgenden Index: 0
Normale Ableitung auf Ebene: 5 Dieser Schlüssel hat folgenden Index: 0

Implementierungsbeispiel für einen erweiterten öffentlichen Schlüssel

Bevor der Artikel endet, gucken wir uns noch einen kleines – für das Gesamtprojekt zwar nicht relevantes – interessantes Implementierungsbeispiel für den hier bislang nur kurz angesprochenen erweiterten öffentlichen Schlüssel zum besseren Verständnis an.

Wir legen hierfür eine neue Datei an und nennen diese „fromExtendedPublicKey.js„.

Nun kopieren wir uns die relevanten Passagen zur Erstellung eines Seeds herein und legen dieses Mal unsere Mnemonic selber fest, damit wir stets die gleichen Keys erhalten.

import {generateMnemonic, mnemonicToSeed} from 'bip39'
import HDKey from 'hdkey';

const mnemonic = "grace merge wheel bone truth denial phrase today flock program payment chaos";

const seed = await mnemonicToSeed(mnemonic);
const hdKey = HDKey.fromMasterSeed(Buffer.from(seed));

let childKey = hdKey.derive("m/44'/60'/0'/0/0");
console.log('0x' + childKey.privateKey.toString('hex'));
console.log('0x' + childKey.publicKey.toString('hex'));

Anschließend erzeugen wir wieder, wie zuvor einen Seed und einen HDKey, leiten uns einen entsprechenden Kind-Schlüssel über den bereits bekannten Pfad ab und geben hierzu sowohl den privaten als auch den öffentlichen Schlüssel aus. Bis hierhin bleibt also alles beim alt bekannten. Jetzt möchten wir jedoch anhand eines zuvor exportierten erweiterten öffentlichen Schlüssels den gleichen öffentlichen Kind-Schlüssel generieren, wie der zuvor ausgegebene.

Hierzu exportieren wir uns zunächst auf einer Ebene darüber den erweiterten öffentlichen Schlüssel:

    const otherChild = myHDKey.derive("m/44'/60'/0'/0");
    const otherChildExtend = otherChild.publicExtendedKey;

Wichtig ist hier, dass wir den Ableitungspfad entsprechend verkürzen. Nun können wir uns von diesen erweiterten öffentliche Schlüssel den gleichen öffentlichen Kind-Schlüssel ableiten, den wir oben bereits erzeugt haben, indem wir einfach von diesen erweiterten öffentlichen Schlüssel einen Kind-Schlüssel mittels der normalen Ableitung ableiten.

const hdkeyFromExtendedPubKey = HDKey.fromExtendedKey(otherChildExtended);
const derivedPublicHDKey = hdkeyFromExtendedPubKey.derive("M/0");

console.log('Aus erweiterten öffentlichen Schlüssel abgeleitet: ' + '0x' + derivedPublicHDKey.publicKey.toString('hex'));

Danach generieren wir unter Zuhilfenahme dieses erweiterten öffentliche Schlüssels mittels der derive-Funktion einen neuen erweiterten öffentlichen Kind-Schlüssel. Hierzu verwenden wir jedoch den Ableitungspfad „M/0“ um nur eine Ableitung von diesem erweiterten öffentlichen Eltern-Schlüssel zu erzeugen.

Vergleichen wir nun das Ergebnis, so erhalten wir denselben öffentlichen Schlüssel wie zuvor – nur das wir hierfür keinen privaten Schlüssel benötigt haben. Dies war jedoch nur ein kurzes Beispiel, um die Funktion eines erweiterten öffentlichen Schlüssels zu demonstrieren.

Wir benötigen diese Implementierung in den folgenden Artikeln nicht mehr, sondern machen im nächsten Artikel bei den unterschiedlichen Darstellungsformen von öffentlichen Schlüssel weiter und werden aus einem öffentlichen Schlüssel eine Ethereum-Adresse erzeugen.

BIPs und verwendete Bibliotheken

BIPs:
[1] https://github.com/bitcoin/bips
[2] https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
[3] https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[4] https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

Genutzte Bibliotheken:
[5] https://www.npmjs.com/package/bip39
[6] https://www.npmjs.com/package/hdkey
[7] https://www.npmjs.com/package/ethereumjs-util

SLIPs:

[8] https://github.com/satoshilabs/slips/blob/master/slip-0044.md

Nach oben scrollen