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.

๐Ÿ’ก In Simple Terms

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 vs. Digital Signature (ECDSA)
  • 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:

Parent Private Key
Chain Code
Index Number (i)
โ–ผ
HMAC-SHA512
โ–ผ
512-bit Output
โ–ผ
Left 256 bits โ†’ New Private Key
Right 256 bits โ†’ New Chain Code
๐Ÿ’ฐ Real-World Example: Your Wallet's Recovery Phrase

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

PropertyWhat It MeansWhy 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

Message + No Key โ†’ Hash โ†’ Digest
โ†”
Message + Secret Key โ†’ HMAC โ†’ Digest
โš ๏ธ NEVER use raw SHA-512 for key derivation!

An attacker could compute SHA-512 of guesses without needing any key. HMAC requires the secret key, making brute force infeasible.

# WRONG โ€” attacker can compute this easily
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ยณยน)

HMAC-SHA512(chain_code, parent_public_key || index)

Used for creating standard child keys that anyone can derive if they know the parent public key.

1.5.2 Hardened Derivation (index โ‰ฅ 2ยณยน)

HMAC-SHA512(chain_code, parent_private_key || index)

Used for creating "hardened" children that CANNOT be derived from just the public key โ€” essential for security.

๐Ÿ”’ Hardened vs. Normal Derivation
  • 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

BIP32 derivation path: m/44'/0'/0'/0/0
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)
    โ””โ”€โ”€ ...
๐Ÿ’ก Each apostrophe (') means hardened derivation โ€” uses parent private key. No apostrophe means normal derivation โ€” uses parent public key.

1.7 Security Analysis

Attack TypeResistanceExplanation
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):

HMAC(K, m) = H((K' โŠ• opad) || H((K' โŠ• ipad) || m))

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: 0x36 repeated
  • opad = Outer padding byte: 0x5C repeated
  • || = Concatenation
  • โŠ• = XOR (bitwise exclusive or)
  • m = The message

2.2 Step-by-Step HMAC-SHA512 Computation

Step 1: Prepare the key
โ–ผ
Step 2: XOR with ipad
โ–ผ
Step 3: Append message and hash (inner hash)
โ–ผ
Step 4: XOR the key with opad
โ–ผ
Step 5: Append inner hash and hash again (outer hash)
โ–ผ
Output: 512-bit HMAC

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:

If |K| > 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)
๐Ÿ”ง Practical Example

In BIP32, the "chain code" is exactly 32 bytes (256 bits). So it's padded with 96 zeros to reach 128 bytes:

chain_code (32 bytes) + 0x00 ร— 96 = 128-byte K'

2.4 Step 2: Inner Padding (ipad XOR)

ipad = 0x36 repeated 128 times (the block size)
K'_inner = K' โŠ• ipad (XOR each byte)
Byte Value0x36 (ipad)XOR ResultMeaning
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_data = K'_inner || message
inner_hash = SHA-512(inner_data)

The inner hash processes the entire message and produces a 64-byte digest.

๐Ÿ’ก This is where the message is hashed โ€” but the key is mixed in via XOR with ipad first. Without the correct key, the inner hash will be completely different.

2.6 Step 4: Outer Padding (opad XOR)

opad = 0x5C repeated 128 times
K'_outer = K' โŠ• opad
Byte Value0x5C (opad)XOR Result
0x5A0x5C0x06
0x000x5C0x5C
0xFF0x5C0xA3

2.7 Step 5: Outer Hash (Final SHA-512)

outer_data = K'_outer || inner_hash
HMAC = SHA-512(outer_data)
โœ… Why Two Hashes?

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)?

โŒ Weak: SHA-512(key || message) is vulnerable to length extension attacks!

Length extension attack: If you know SHA-512(key || message), you can compute SHA-512(key || message || padding || extra) WITHOUT knowing the key.

โœ… HMAC resists length extension by hashing twice with different keys.
๐Ÿ’ก Analogy: Raw SHA-512(key || message) is like sealing an envelope with wax โ€” someone can add more pages to the end without breaking the seal! HMAC puts the message inside two locked boxes, so you can't add anything without the key.

2.9 Cryptographic Strength

Security = min(|key|, 2 ร— output_length)
For HMAC-SHA512 with 256-bit key: min(256, 1024) = 256 bits
AttackWork FactorFeasibility
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:

Simplified HMAC example (8-byte blocks, 4-byte hash)
# 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!

Built-in modules
import hmac
import hashlib

3.2 Basic HMAC-SHA512 Usage

Simple HMAC-SHA512 example
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)

bip32_hmac.py โ€” Complete BIP32 derivation
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

Common HMAC errors and debugging
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}")
โš ๏ธ CRITICAL: Always use compare_digest()!

Never compare HMACs with == or !=. Use hmac.compare_digest(a, b) instead:

# WRONG โ€” vulnerable to timing attack
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)MessageHMAC-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)
Verify against RFC 4231 test vectors
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

๐Ÿ’ป How to Run
  • Copy any code block into a .py file
  • Run with: python filename.py
  • No external dependencies needed โ€” uses Python's built-in hmac and hashlib
๐Ÿ“š Key Takeaways
  • 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)