HKDF Security-Focused API Documentation

Version: 1.0
Last Updated: 2025-07-05
**Security Classification:
PUBLIC
Author: Phantom (phantom@metamui.id)

Overview

HKDF (HMAC-based Extract-and-Expand Key Derivation Function) is a simple, efficient key derivation function specified in RFC 5869. It extracts randomness from various sources and expands it into cryptographically strong key material. HKDF follows the “extract-then-expand” paradigm, making it suitable for various key derivation scenarios.

Security Level: Depends on hash function
Recommended Hash: SHA-256 or SHA-512
Salt Size: Hash output length (optional)
Max Output: 255 × HashLen bytes

Security Warnings ⚠️

  1. Not for Passwords: HKDF is NOT a password hash - use Argon2 for passwords
  2. Entropy Preservation: Output entropy ≤ input entropy - cannot create randomness
  3. Info Parameter: Always use unique info strings for domain separation
  4. Salt Optional: But recommended for security properties
  5. Output Length: Requesting too much output reduces security per byte

API Functions

extract(salt: bytes, ikm: bytes) -> bytes[HashLen]

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Weak Input | ⚠️ | Output limited by input entropy | | Related Keys | ✅ | Different salts give independent PRKs | | Length Extension | ✅ | HMAC prevents | | Timing Attack | ✅ | HMAC is constant time | | Cross-protocol | ✅ | Salt provides separation |

Security Requirements:

Secure Usage Example:

import hashlib
import hmac
import secrets
from typing import Tuple, Optional

class SecureHKDF:
    def __init__(self, hash_algo=hashlib.sha256):
        self.hash_algo = hash_algo
        self.hash_len = hash_algo().digest_size
        
    def extract(self, salt: Optional[bytes], ikm: bytes) -> bytes:
        """Extract uniform PRK from diverse inputs"""
        
        if salt is None:
            # Use default salt of hash_len zeros
            salt = b'\x00' * self.hash_len
        
        # HMAC with salt as key
        prk = hmac.new(salt, ikm, self.hash_algo).digest()
        
        return prk
    
    def extract_from_dh(self, shared_secret: bytes, protocol_id: bytes) -> bytes:
        """Extract key from DH shared secret"""
        
        # Use protocol ID as salt for domain separation
        salt = b"HKDF-" + protocol_id
        
        # DH outputs may have biased bits, extract uniformity
        prk = self.extract(salt, shared_secret)
        
        return prk
    
    def extract_multi_source(self, *sources: bytes) -> bytes:
        """Extract from multiple entropy sources"""
        
        # Combine sources securely
        combined = b""
        for i, source in enumerate(sources):
            # Include length to prevent ambiguity
            combined += len(source).to_bytes(4, 'big')
            combined += source
        
        # Extract with application-specific salt
        salt = b"MultiSource-v1"
        prk = self.extract(salt, combined)
        
        # Clear combined material
        combined = b'\x00' * len(combined)
        
        return prk

# SECURE: Extract from ECDH key exchange
def process_ecdh_exchange(
    private_key: bytes, 
    peer_public_key: bytes,
    protocol_version: int
) -> Tuple[bytes, bytes]:
    """Secure key derivation from ECDH"""
    
    # Perform ECDH
    shared_point = ecdh_compute(private_key, peer_public_key)
    
    # Extract randomness (shared_point may have structure)
    hkdf = SecureHKDF(hashlib.sha256)
    
    # Include protocol version in salt
    salt = b"ECDHv" + protocol_version.to_bytes(2, 'big')
    prk = hkdf.extract(salt, shared_point)
    
    # Clear sensitive data
    secure_zero(shared_point)
    
    return prk

# SECURE: RNG output extraction
class EntropyExtractor:
    def __init__(self):
        self.hkdf = SecureHKDF(hashlib.sha512)
        self.generation = 0
        
    def extract_from_rng(self, rng_output: bytes, source_id: str) -> bytes:
        """Extract uniform randomness from RNG output"""
        
        # Even good RNGs may have slight bias
        # HKDF extract provides uniformity guarantee
        
        salt = f"RNG-{source_id}-Gen{self.generation}".encode()
        self.generation += 1
        
        prk = self.hkdf.extract(salt, rng_output)
        
        return prk
    
    def combine_entropy_sources(self, sources: dict) -> bytes:
        """Combine multiple entropy sources safely"""
        
        # Sources: {'os_random': bytes, 'hardware_rng': bytes, ...}
        
        # Build canonical combination
        combined = b""
        for source_name in sorted(sources.keys()):
            data = sources[source_name]
            combined += source_name.encode()
            combined += b":"
            combined += len(data).to_bytes(4, 'big')
            combined += data
        
        # Extract with timestamp salt
        import time
        salt = b"EntropyCombine-" + int(time.time()).to_bytes(8, 'big')
        
        prk = self.hkdf.extract(salt, combined)
        
        # Clear combined buffer
        secure_zero(combined)
        
        return prk

Common Mistakes:

# INSECURE: Using HKDF for passwords
password_key = hkdf.extract(salt, password)  # NO! Use Argon2

# INSECURE: No salt
prk = hkdf.extract(None, shared_secret)  # Loses domain separation

# INSECURE: Using PRK directly
encryption_key = hkdf.extract(salt, secret)  # Should expand!

# INSECURE: Weak input entropy
weak_secret = int(time.time()).to_bytes(8, 'big')
key = hkdf.extract(salt, weak_secret)  # Only ~30 bits entropy!

expand(prk: bytes, info: bytes, length: int) -> bytes

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Related Keys | ✅ | Info parameter separates | | Key Prediction | ✅ | PRF security | | Length Extension | ✅ | HMAC construction | | Timing Attack | ✅ | Constant time | | Rollback | ✅ | One-way function |

Security Requirements:

Secure Usage Example:

use hkdf::Hkdf;
use sha2::Sha256;

/// Secure key expansion for protocol
pub struct ProtocolKeyDerivation {
    master_secret: Vec<u8>,
}

impl ProtocolKeyDerivation {
    pub fn new(master_secret: Vec<u8>) -> Self {
        Self { master_secret }
    }
    
    pub fn derive_session_keys(&self, session_id: &[u8]) -> SessionKeys {
        // Extract PRK from master secret
        let hk = Hkdf::<Sha256>::new(
            Some(b"MyProtocol-v2-Master"),
            &self.master_secret
        );
        
        // Derive specific keys with context
        let mut client_encrypt = [0u8; 32];
        hk.expand(
            &[b"client-encrypt-key", session_id].concat(),
            &mut client_encrypt
        ).expect("valid length");
        
        let mut server_encrypt = [0u8; 32];
        hk.expand(
            &[b"server-encrypt-key", session_id].concat(),
            &mut server_encrypt
        ).expect("valid length");
        
        let mut client_mac = [0u8; 32];
        hk.expand(
            &[b"client-mac-key", session_id].concat(),
            &mut client_mac
        ).expect("valid length");
        
        let mut server_mac = [0u8; 32];
        hk.expand(
            &[b"server-mac-key", session_id].concat(),
            &mut server_mac
        ).expect("valid length");
        
        SessionKeys {
            client_encrypt,
            server_encrypt,
            client_mac,
            server_mac,
        }
    }
    
    pub fn derive_subkey(&self, purpose: &str, context: &[u8]) -> [u8; 32] {
        let hk = Hkdf::<Sha256>::new(
            Some(b"MyProtocol-v2-Subkey"),
            &self.master_secret
        );
        
        // Build info with structure
        let info = format!(
            "Purpose:{}\nContext:{}\nTimestamp:{}",
            purpose,
            hex::encode(context),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs()
        );
        
        let mut subkey = [0u8; 32];
        hk.expand(info.as_bytes(), &mut subkey)
            .expect("valid length");
        
        subkey
    }
}

/// Key hierarchy with HKDF
pub struct KeyHierarchy {
    root: [u8; 32],
}

impl KeyHierarchy {
    pub fn derive_department_key(&self, dept: &str) -> [u8; 32] {
        let hk = Hkdf::<Sha256>::new(
            Some(b"CompanyKeyHierarchy-v1"),
            &self.root
        );
        
        let mut dept_key = [0u8; 32];
        let info = format!("department:{}", dept);
        hk.expand(info.as_bytes(), &mut dept_key)
            .expect("valid length");
        
        dept_key
    }
    
    pub fn derive_user_key(&self, dept: &str, user_id: u64) -> [u8; 32] {
        // First derive department key
        let dept_key = self.derive_department_key(dept);
        
        // Then derive user key from department key
        let hk = Hkdf::<Sha256>::new(
            Some(b"UserKeyDerivation-v1"),
            &dept_key
        );
        
        let mut user_key = [0u8; 32];
        let info = format!("user:{}", user_id);
        hk.expand(info.as_bytes(), &mut user_key)
            .expect("valid length");
        
        // Clear intermediate key
        dept_key.zeroize();
        
        user_key
    }
}

hkdf(salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes

Security Contract:

Secure Usage Example:

def derive_api_keys(master_key: bytes, client_id: str) -> dict:
    """Derive all API keys for a client"""
    
    # One-shot HKDF for multiple keys
    all_keys = hkdf(
        salt=b"APIKeys-v1",
        ikm=master_key,
        info=f"client:{client_id}".encode(),
        length=96  # 3 × 32-byte keys
    )
    
    return {
        'signing_key': all_keys[0:32],
        'encryption_key': all_keys[32:64],
        'storage_key': all_keys[64:96]
    }

# SECURE: Versioned key derivation
class VersionedKeyDerivation:
    def __init__(self, root_key: bytes):
        self.root_key = root_key
        self.current_version = 1
        
    def derive_key(self, purpose: str) -> bytes:
        """Derive key with version support"""
        
        info = f"{purpose}:v{self.current_version}".encode()
        
        key = hkdf(
            salt=b"VersionedKeys",
            ikm=self.root_key,
            info=info,
            length=32
        )
        
        return key
    
    def rotate_version(self):
        """Increment version for key rotation"""
        self.current_version += 1
        
        # Optionally derive new root
        self.root_key = hkdf(
            salt=b"RootRotation",
            ikm=self.root_key,
            info=f"v{self.current_version}".encode(),
            length=32
        )

Security Best Practices

Domain Separation

class DomainSeparatedHKDF:
    """HKDF with strong domain separation"""
    
    def __init__(self, master_secret: bytes, domain: str):
        self.master_secret = master_secret
        self.domain = domain
        self.hkdf = SecureHKDF(hashlib.sha256)
        
        # Extract domain-specific PRK
        domain_salt = f"Domain:{domain}:v1".encode()
        self.prk = self.hkdf.extract(domain_salt, master_secret)
    
    def derive_key(self, purpose: str, context: bytes = b"") -> bytes:
        """Derive key with full context"""
        
        # Build structured info
        info = self._build_info(purpose, context)
        
        # Expand to get key
        key = self.hkdf.expand(self.prk, info, 32)
        
        return key
    
    def _build_info(self, purpose: str, context: bytes) -> bytes:
        """Build canonical info string"""
        
        # Include all relevant context
        parts = [
            f"Domain:{self.domain}",
            f"Purpose:{purpose}",
            f"ContextLen:{len(context)}",
            f"Context:{context.hex()}",
            f"Timestamp:{int(time.time())}"
        ]
        
        return "\n".join(parts).encode()

# Example usage
auth_kdf = DomainSeparatedHKDF(master_secret, "Authentication")
api_key = auth_kdf.derive_key("APIKey", user_id.encode())
session_key = auth_kdf.derive_key("Session", session_id.encode())

enc_kdf = DomainSeparatedHKDF(master_secret, "Encryption")  
data_key = enc_kdf.derive_key("DataKey", file_id.encode())

Key Hierarchies

/// Complex key hierarchy with HKDF
pub struct EnterpriseKeyHierarchy {
    root_key: [u8; 32],
    hash_algo: Sha256,
}

impl EnterpriseKeyHierarchy {
    pub fn new(root_key: [u8; 32]) -> Self {
        Self { root_key, hash_algo: Sha256::new() }
    }
    
    /// Derive keys following hierarchy
    pub fn derive_path(&self, path: &[&str]) -> Result<[u8; 32], Error> {
        let mut current_key = self.root_key;
        
        for (level, component) in path.iter().enumerate() {
            // Use previous level as IKM
            let hk = Hkdf::<Sha256>::new(
                Some(format!("Level-{}", level).as_bytes()),
                &current_key
            );
            
            // Derive next level
            let info = format!("Component:{}", component);
            hk.expand(info.as_bytes(), &mut current_key)?;
        }
        
        Ok(current_key)
    }
    
    /// Example hierarchy
    pub fn get_encryption_key(
        &self,
        org: &str,
        dept: &str,
        user: &str,
    ) -> Result<[u8; 32], Error> {
        self.derive_path(&[
            "encryption",
            org,
            dept,
            user
        ])
    }
}

Rekeying Patterns

class RekeyingSystem:
    """Forward-secure rekeying with HKDF"""
    
    def __init__(self, initial_key: bytes):
        self.current_key = initial_key
        self.epoch = 0
        self.hkdf = SecureHKDF()
        
    def get_current_key(self) -> Tuple[bytes, int]:
        """Get current key and epoch"""
        return self.current_key, self.epoch
    
    def rekey(self) -> bytes:
        """Advance to next key (one-way)"""
        
        # Derive next key
        prk = self.hkdf.extract(
            b"Rekey-Forward-Secure",
            self.current_key
        )
        
        next_key = self.hkdf.expand(
            prk,
            f"epoch:{self.epoch + 1}".encode(),
            32
        )
        
        # Clear old key
        secure_zero(self.current_key)
        
        # Update state
        self.current_key = next_key
        self.epoch += 1
        
        return next_key
    
    def derive_message_key(self, message_id: int) -> bytes:
        """Derive key for specific message"""
        
        prk = self.hkdf.extract(
            f"MessageKey-Epoch{self.epoch}".encode(),
            self.current_key
        )
        
        return self.hkdf.expand(
            prk,
            f"msg:{message_id}".encode(),
            32
        )

Common Integration Patterns

TLS 1.3 Style Key Schedule

/// TLS 1.3 inspired key schedule
pub struct KeySchedule {
    transcript_hash: Vec<u8>,
    secret: Vec<u8>,
}

impl KeySchedule {
    pub fn new() -> Self {
        Self {
            transcript_hash: Vec::new(),
            secret: vec![0u8; 32],  // Initial secret
        }
    }
    
    pub fn update_transcript(&mut self, data: &[u8]) {
        let mut hasher = Sha256::new();
        hasher.update(&self.transcript_hash);
        hasher.update(data);
        self.transcript_hash = hasher.finalize().to_vec();
    }
    
    pub fn derive_secret(&self, label: &str) -> [u8; 32] {
        let hk = Hkdf::<Sha256>::new(None, &self.secret);
        
        let info = self.build_label(label, &self.transcript_hash, 32);
        let mut secret = [0u8; 32];
        hk.expand(&info, &mut secret).unwrap();
        
        secret
    }
    
    fn build_label(&self, label: &str, context: &[u8], length: u16) -> Vec<u8> {
        let mut info = Vec::new();
        
        // Length
        info.extend_from_slice(&length.to_be_bytes());
        
        // Label  
        let full_label = format!("tls13 {}", label);
        info.push(full_label.len() as u8);
        info.extend_from_slice(full_label.as_bytes());
        
        // Context
        info.push(context.len() as u8);
        info.extend_from_slice(context);
        
        info
    }
}

Secure Storage Key Derivation

class StorageKeyDerivation:
    """Derive keys for encrypted storage"""
    
    def __init__(self, master_key: bytes):
        self.master_key = master_key
        self.hkdf = SecureHKDF(hashlib.sha256)
        
    def derive_file_keys(self, file_id: str) -> dict:
        """Derive all keys needed for file encryption"""
        
        # Extract file-specific PRK
        prk = self.hkdf.extract(
            b"FileEncryption-v1",
            self.master_key + file_id.encode()
        )
        
        # Derive multiple keys
        keys = {}
        
        # Content encryption key
        keys['cek'] = self.hkdf.expand(
            prk,
            b"content-encryption-key",
            32  # AES-256
        )
        
        # Content authentication key
        keys['cak'] = self.hkdf.expand(
            prk,
            b"content-auth-key",
            32  # HMAC-SHA256
        )
        
        # Metadata encryption key
        keys['mek'] = self.hkdf.expand(
            prk,
            b"metadata-encryption-key",
            32
        )
        
        # Searchable encryption key
        keys['sek'] = self.hkdf.expand(
            prk,
            b"searchable-encryption-key",
            32
        )
        
        return keys

Performance Considerations

Operation Time Notes
Extract (SHA-256) ~5 μs Single HMAC operation
Expand (32 bytes) ~5 μs One HMAC operation
Expand (256 bytes) ~40 μs Eight HMAC operations
Full HKDF (32 bytes) ~10 μs Extract + Expand

Optimization Tips

Security Auditing

Verification Checklist

Common Vulnerabilities

# AUDIT: Look for these patterns

# ❌ Password key derivation
key = hkdf(salt, password, info, 32)  # Use Argon2!

# ❌ No info parameter
key1 = hkdf(salt, secret, b"", 32)
key2 = hkdf(salt, secret, b"", 32)  # Same as key1!

# ❌ Weak entropy
timestamp_key = hkdf(salt, str(time.time()).encode(), info, 32)

# ❌ Too much output
keys = hkdf(salt, secret, info, 10000)  # Reduces security

# ❌ Missing domain separation
# All apps use same salt/info
key = hkdf(b"salt", secret, b"key", 32)

Security Analysis

Threat Model: HKDF-SHA256 Threat Model

The comprehensive threat analysis covers:

For complete security analysis and risk assessment, see the dedicated threat model documentation.

References

  1. RFC 5869 - HKDF Specification
  2. HKDF Paper
  3. TLS 1.3 Key Schedule

Support

Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md


Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC