HMAC-SHA512: Bitcoin's HD Wallet Key Derivation
Complete guide with Theory, Mathematics, and Code โ understand how BIP32 creates hierarchical deterministic wallets
1.1 What is HMAC?
HMAC (Hash-based Message Authentication Code) is a mechanism that combines a cryptographic hash function (like SHA-512) with a secret key. It allows two parties who share a secret key to verify that a message hasn't been tampered with and that it came from a legitimate source.
Think of HMAC as a signature with a shared password. If you and I share a secret key, I can compute an HMAC (like a tamper-proof tag) for any message. You can compute the same HMAC and compare โ if they match, you know the message is authentic and unaltered.
- HMAC: Uses a shared secret key (symmetric). Both parties need the same key.
- ECDSA: Uses public/private key pairs (asymmetric). Only the signer needs the private key; anyone can verify.
- HMAC is faster than ECDSA (no elliptic curve math).
- Bitcoin uses BOTH: ECDSA for transaction signatures (public verification), HMAC for HD wallet derivation (only the wallet owner needs it).
1.2 Why HMAC-SHA512 in Bitcoin?
Bitcoin uses HMAC-SHA512 specifically in BIP32 (Hierarchical Deterministic Wallets) to derive child keys from parent keys. Here's why:
When you enter a 12 or 24-word seed phrase into a wallet, the wallet uses HMAC-SHA512 thousands of times to derive all your addresses. The seed becomes the master key, and each derivation step uses HMAC-SHA512 to create a deterministic tree of keys โ all from one root!
Why 512 bits specifically?
The 512-bit output is perfectly split into:
- First 256 bits (32 bytes): Becomes the child private key (or public key for public derivation)
- Last 256 bits (32 bytes): Becomes the new chain code (entropy for further derivations)
This 256+256 split is perfect because Bitcoin uses 256-bit private keys (secp256k1).
1.3 HMAC Properties
| Property | What It Means | Why It Matters for Bitcoin |
|---|---|---|
| Message Authentication | The recipient can verify the message came from someone with the key | Child derivation ensures only the wallet owner can derive the next keys |
| Integrity | Any change to the message invalidates the HMAC | Changing the index even by 1 produces completely different keys |
| Pseudorandomness | Output is indistinguishable from random | Derived keys are unpredictable without the parent key/chain code |
| One-way (preimage resistance) | Can't recover the key from the HMAC output | Even if someone sees child keys, they can't find the parent |
| Collision resistance | Two different inputs won't produce the same HMAC | Different derivation paths produce distinct keys |
1.4 HMAC vs. Hashing โ Critical Difference
An attacker could compute SHA-512 of guesses without needing any key. HMAC requires the secret key, making brute force infeasible.
child_key = sha512(parent_key + index)
# RIGHT โ only someone with the parent chain code can compute
child_key = hmac_sha512(chain_code, parent_key + index)
1.5 BIP32: The Actual Bitcoin Use Case
BIP32 defines two types of HMAC-SHA512 derivations:
1.5.1 Normal Derivation (index < 2ยณยน)
Used for creating standard child keys that anyone can derive if they know the parent public key.
1.5.2 Hardened Derivation (index โฅ 2ยณยน)
Used for creating "hardened" children that CANNOT be derived from just the public key โ essential for security.
- Normal (non-hardened): Anyone with parent PUBLIC key can derive child PUBLIC keys. Good for watch-only wallets.
- Hardened: Requires the parent PRIVATE key. More secure โ even if an attacker gets a hardened child private key, they cannot work backwards to find the parent.
- Bitcoin HD wallets use hardened derivation for the first few levels (e.g., m/44'/0'/0' โ all hardened!)
1.6 Real BIP32 Example
m (master key from seed)
โ
โโโ 44' (hardened โ BIP44 purpose)
โ โ
โ โโโ 0' (hardened โ Bitcoin mainnet)
โ โ โ
โ โ โโโ 0' (hardened โ account 0)
โ โ โ โ
โ โ โ โโโ 0 (normal โ external chain)
โ โ โ โ โโโ 0 (first receiving address)
โ โ โ โ โโโ 1 (second receiving address)
โ โ โ โ โโโ ...
โ โ โ โ
โ โ โ โโโ 1 (normal โ change chain)
โ โ โ โโโ 0 (first change address)
โ โ โ โโโ ...
โ โ โ
โ โ โโโ 1' (hardened โ account 1)
โ โ
โ โโโ 1' (hardened โ Testnet)
โ
โโโ 49' (hardened โ BIP49 SegWit)
โโโ ...
1.7 Security Analysis
| Attack Type | Resistance | Explanation |
|---|---|---|
| Key recovery from HMAC output | โ Secure (2ยฒโตโถ work) | HMAC is one-way; can't recover the key from the digest |
| Length extension attacks | โ Resistant | HMAC's design specifically prevents length extension (unlike raw SHA-2!) |
| Related-key attacks | โ Resistant | Even similar keys produce completely different HMACs |
| Side-channel (timing) | โ ๏ธ Implementation-dependent | Constant-time implementations are crucial |
| Quantum computer (Grover's) | โ ๏ธ Reduced to 2ยนยฒโธ | Still secure, but weaker than classical 2ยฒโตโถ |
2.1 The HMAC Construction Formula
HMAC is defined by this formula (from RFC 2104):
Where:
- H = The hash function (SHA-512 in our case)
- K = Original secret key (any length)
- K' = Key padded to block size (128 bytes for SHA-512)
- ipad = Inner padding byte:
0x36repeated - opad = Outer padding byte:
0x5Crepeated - || = Concatenation
- โ = XOR (bitwise exclusive or)
- m = The message
2.2 Step-by-Step HMAC-SHA512 Computation
2.3 Step 1: Key Preparation (K')
SHA-512 has a block size of 128 bytes (1024 bits). The key must be exactly 128 bytes:
K' = SHA-512(K) (reduces to 64 bytes)
Then pad with zeros to 128 bytes
If |K| < 128 bytes:
K' = K || (0x00 repeated to 128 bytes)
If |K| = 128 bytes:
K' = K (unchanged)
In BIP32, the "chain code" is exactly 32 bytes (256 bits). So it's padded with 96 zeros to reach 128 bytes:
2.4 Step 2: Inner Padding (ipad XOR)
K'_inner = K' โ ipad (XOR each byte)
| Byte Value | 0x36 (ipad) | XOR Result | Meaning |
|---|---|---|---|
| 0x5A (example) | 0x36 | 0x6C | Both bits flipped โ looks random |
| 0x00 (padding zero) | 0x36 | 0x36 | Zero padding becomes ipad value |
| 0xFF (all ones) | 0x36 | 0xC9 | XOR flips specific bits |
2.5 Step 3: Inner Hash (First SHA-512)
inner_hash = SHA-512(inner_data)
The inner hash processes the entire message and produces a 64-byte digest.
2.6 Step 4: Outer Padding (opad XOR)
K'_outer = K' โ opad
| Byte Value | 0x5C (opad) | XOR Result |
|---|---|---|
| 0x5A | 0x5C | 0x06 |
| 0x00 | 0x5C | 0x5C |
| 0xFF | 0x5C | 0xA3 |
2.7 Step 5: Outer Hash (Final SHA-512)
HMAC = SHA-512(outer_data)
The "inner hash" processes the actual message. The "outer hash" protects the key from length extension attacks. This construction is provably secure if the underlying hash function is collision-resistant and the key is secret.
2.8 Why Not Just SHA-512(key || message)?
Length extension attack: If you know SHA-512(key || message), you can compute SHA-512(key || message || padding || extra) WITHOUT knowing the key.
2.9 Cryptographic Strength
For HMAC-SHA512 with 256-bit key: min(256, 1024) = 256 bits
| Attack | Work Factor | Feasibility |
|---|---|---|
| Brute force key (2ยฒโตโถ possibilities) | 2ยฒโตโถ 2ยนยฒโธ with quantum (Grover) | Impossible |
| Birthday collision on HMAC | 2ยฒโตโถ (output is 512 bits) 2ยฒโตโถ is impossible | Impossible |
| Recover key from known message-HMAC pairs | 2ยฒโตโถ (exhaustive) | Impossible |
2.10 Complete Example with Small Numbers (Educational)
Let's walk through a simplified HMAC with a tiny block size (8 bytes) to see the math in action:
# Setup (simplified for education)
block_size = 8 bytes
hash_output = 4 bytes (normally 64 for SHA-512)
key = [0x01, 0x02, 0x03, 0x04] # 4 bytes (pad with zeros)
message = b"Hi"
# Step 1: Pad key to block size
K_prime = [0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00]
# Step 2: XOR with ipad (0x36)
ipad = [0x36] * 8
K_inner = [k ^ 0x36 for k in K_prime]
# Result: [0x37, 0x34, 0x35, 0x32, 0x36, 0x36, 0x36, 0x36]
# Step 3: Append message and hash
inner_data = bytes(K_inner) + message # 8 + 2 = 10 bytes
inner_hash = simple_hash(inner_data) # 4-byte output
# Step 4: XOR with opad (0x5C)
opad = [0x5C] * 8
K_outer = [k ^ 0x5C for k in K_prime]
# Result: [0x5D, 0x5E, 0x5F, 0x58, 0x5C, 0x5C, 0x5C, 0x5C]
# Step 5: Hash again
outer_data = bytes(K_outer) + inner_hash
final_hmac = simple_hash(outer_data)
3.1 Installation
Python's standard library includes HMAC โ no extra packages needed!
import hmac
import hashlib
3.2 Basic HMAC-SHA512 Usage
import hmac
import hashlib
# Secret key (32 bytes = 256 bits, Bitcoin standard)
key = b"my_super_secret_key_for_hd_wallet_32bytes!"
# Message to authenticate
message = b"derive child key with index 0"
# Compute HMAC-SHA512
hmac_result = hmac.new(key, message, hashlib.sha512).digest()
print(f"HMAC-SHA512 (64 bytes): {hmac_result.hex()}")
print(f"Length: {len(hmac_result)} bytes")
print(f"Left 256 bits (private key): {hmac_result[:32].hex()}")
print(f"Right 256 bits (chain code): {hmac_result[32:].hex()}")
3.3 BIP32 HD Wallet Derivation (The Real Bitcoin Use Case)
import hmac
import hashlib
import struct
def hmac_sha512(key, message):
"""Compute HMAC-SHA512 exactly as used in BIP32"""
return hmac.new(key, message, hashlib.sha512).digest()
def derive_child_key(parent_private_key, parent_chain_code, index, hardened=False):
"""
Derive a child key following BIP32 specification.
Args:
parent_private_key: 32-byte parent private key
parent_chain_code: 32-byte parent chain code
index: 32-bit integer (0 to 2^31-1 for normal, 2^31 to 2^32-1 for hardened)
hardened: If True, index is ORed with 0x80000000
Returns:
child_private_key: 32-byte child private key
child_chain_code: 32-byte new chain code
"""
# Hardened derivation uses private key + index
if hardened:
# Hardened keys use index >= 0x80000000 (2^31)
actual_index = index | 0x80000000
data = b'\x00' + parent_private_key + struct.pack('>I', actual_index)
else:
# Normal derivation uses public key + index
# For simplicity, we'll use the private key's public key here
# In real BIP32, you'd need point multiplication
actual_index = index
# Placeholder: normally you'd compute public key from private key
# For this example, we'll use the private key with a marker
data = parent_private_key + struct.pack('>I', actual_index)
# Compute HMAC-SHA512
hmac_result = hmac_sha512(parent_chain_code, data)
# Split into two 256-bit halves
child_private_key = hmac_result[:32]
child_chain_code = hmac_result[32:]
return child_private_key, child_chain_code
def mnemonic_to_seed(mnemonic, passphrase=""):
"""
Convert a BIP39 mnemonic to a 512-bit seed using PBKDF2-HMAC-SHA512.
This is how your recovery phrase becomes your master private key!
"""
from hashlib import pbkdf2_hmac
salt = "mnemonic" + passphrase
# 2048 rounds of HMAC-SHA512
seed = pbkdf2_hmac('sha512', mnemonic.encode('utf-8'),
salt.encode('utf-8'), 2048, 64)
return seed
# Example usage
if __name__ == '__main__':
# In a real wallet, this would come from your seed phrase
# For demo, we use a test private key (NEVER use this for real funds!)
test_private_key = bytes.fromhex(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
)
test_chain_code = bytes.fromhex(
"f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c"
)
print("BIP32 Key Derivation Example:")
print("=" * 50)
# Derive a normal child (index 0)
child_key, child_cc = derive_child_key(
test_private_key, test_chain_code, 0, hardened=False
)
print(f"Normal child (index 0):")
print(f" Private key: {child_key.hex()[:32]}...")
print(f" Chain code: {child_cc.hex()[:32]}...")
# Derive a hardened child (index 0 hardened = 0x80000000)
hardened_key, hardened_cc = derive_child_key(
test_private_key, test_chain_code, 0, hardened=True
)
print(f"\nHardened child (index 0'):")
print(f" Private key: {hardened_key.hex()[:32]}...")
print(f" Chain code: {hardened_cc.hex()[:32]}...")
# Derive from the child (grandchild)
grandchild_key, grandchild_cc = derive_child_key(
child_key, child_cc, 5, hardened=False
)
print(f"\nGrandchild (index 5 of normal child):")
print(f" Private key: {grandchild_key.hex()[:32]}...")
# Mnemonic to seed example
print("\n" + "=" * 50)
print("BIP39 Mnemonic to Seed:")
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
seed = mnemonic_to_seed(mnemonic)
print(f"Seed (512 bits): {seed.hex()[:32]}... (truncated)")
print(f"Seed length: {len(seed)} bytes (512 bits)")
3.4 Error Handling with HMAC
import hmac
import hashlib
def safe_verify_hmac(key, message, received_hmac):
"""
Securely verify an HMAC (constant-time comparison!)
"""
try:
# Compute expected HMAC
expected = hmac.new(key, message, hashlib.sha512).digest()
# CRITICAL: Use compare_digest() NOT ==
# compare_digest prevents timing attacks
if hmac.compare_digest(expected, received_hmac):
return True, "HMAC valid"
else:
return False, "HMAC invalid - message may be tampered"
except TypeError as e:
return False, f"Invalid key or message type: {e}"
except Exception as e:
return False, f"Unexpected error: {e}"
# Examples of what can go wrong
key = b"my_secret_key"
message = b"important data"
correct_hmac = hmac.new(key, message, hashlib.sha512).digest()
# Wrong key
wrong_key = b"wrong_key"
valid, msg = safe_verify_hmac(wrong_key, message, correct_hmac)
print(f"Wrong key: {msg}") # "HMAC invalid"
# Wrong message
wrong_message = b"different data"
valid, msg = safe_verify_hmac(key, wrong_message, correct_hmac)
print(f"Wrong message: {msg}") # "HMAC invalid"
# Empty key (works but UNSECURE - don't do this!)
empty_key = b""
weak_hmac = hmac.new(empty_key, message, hashlib.sha512).digest()
valid, msg = safe_verify_hmac(empty_key, message, weak_hmac)
print(f"Empty key: {msg}") # "HMAC valid" (but it's trivial to forge!)
# Correct verification
valid, msg = safe_verify_hmac(key, message, correct_hmac)
print(f"Correct: {msg}")
Never compare HMACs with == or !=. Use hmac.compare_digest(a, b) instead:
if received_hmac == expected_hmac: ...
# RIGHT โ constant-time comparison
if hmac.compare_digest(received_hmac, expected_hmac): ...
A timing attack could reveal the correct HMAC one bit at a time by measuring how long the comparison takes!
3.5 Test Vectors (RFC 4231)
| Key (hex) | Message | HMAC-SHA512 (first 32 bytes) |
|---|---|---|
| 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b | "Hi There" | b936cee86c9f87aa5d3c6f2e84cb5a4239... (full length 64 bytes) |
| 4a656665 | "what do ya want for nothing?" | 164b7a7bfcf819e2e395fbe73b56e0a387... (full length 64 bytes) |
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 0xdd repeated 50 times | fa73b0089d56a284efb0f0756c890be9b1... (full length 64 bytes) |
import hmac
import hashlib
# Test vector 1 from RFC 4231
key = bytes.fromhex("0b" * 20)
message = b"Hi There"
expected_first_32 = "b936cee86c9f87aa5d3c6f2e84cb5a4239"
result = hmac.new(key, message, hashlib.sha512).digest()
print(f"Test Vector 1:")
print(f" Got: {result.hex()[:32]}")
print(f" Expected: {expected_first_32}")
print(f" Match: {result.hex()[:32] == expected_first_32}")
3.6 Running the Code
- Copy any code block into a
.pyfile - Run with:
python filename.py - No external dependencies needed โ uses Python's built-in
hmacandhashlib
- HMAC is not a hash function โ it's a keyed message authentication code
- Bitcoin uses HMAC-SHA512 for BIP32 HD wallet derivation
- The 512-bit output splits into 256-bit private key + 256-bit chain code
- Hardened derivation (
index | 0x80000000) uses parent private key - Normal derivation uses parent public key (enables watch-only wallets)
- Always use
hmac.compare_digest()for verification (prevents timing attacks)