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 ⚠️
- Not for Passwords: HKDF is NOT a password hash - use Argon2 for passwords
- Entropy Preservation: Output entropy ≤ input entropy - cannot create randomness
- Info Parameter: Always use unique info strings for domain separation
- Salt Optional: But recommended for security properties
- Output Length: Requesting too much output reduces security per byte
API Functions
extract(salt: bytes, ikm: bytes) -> bytes[HashLen]
Security Contract:
- Preconditions:
ikm(input key material) should have sufficient entropysaltis optional but recommended (can be public)- Hash function must be cryptographically secure
- Postconditions:
- Returns pseudorandom key (PRK) of hash length
- PRK suitable for expand step
- Output uniformly distributed if input has entropy
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:
- Input must have at least 128 bits entropy
- Salt should be unique per application
- Never skip extract for low-entropy inputs
- PRK should not be used directly as key
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:
- Preconditions:
prkmust be from extract step (or equivalent PRF output)infoshould be unique for each use caselength≤ 255 × HashLen
- Postconditions:
- Returns
lengthbytes of key material - Different info values give independent outputs
- Deterministic: same inputs = same output
- Returns
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:
- Always use meaningful info strings
- Include version/context in info
- Don’t request excessive output length
- Consider separate PRK for different purposes
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:
- Preconditions:
- Combined extract-expand operation
- Same requirements as extract + expand
- Convenience function only
- Postconditions:
- Equivalent to extract then expand
- Returns
lengthbytes of key material
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()),
¤t_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
- Cache PRK for multiple expand operations
- Use appropriate hash function (SHA-256 vs SHA-512)
- Batch key derivations when possible
- Consider BLAKE2 for performance
Security Auditing
Verification Checklist
- Never using HKDF for passwords
- Always using unique info strings
- Input has sufficient entropy
- Salt is application-specific
- Not requesting excessive output
- PRK not used directly
- Version included in derivation
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:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
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