hmac
hmac (silly.crypto.hmac)
The silly.crypto.hmac module provides Hash-based Message Authentication Code (HMAC) functionality. HMAC is an algorithm that uses a key and hash function to generate message authentication codes, widely used to verify message integrity and authenticity.
To use this module, you must first require it:
local hmac = require "silly.crypto.hmac"Compilation Requirements
This module requires OpenSSL support at compile time. Please compile the Silly framework with make OPENSSL=ON.
Core Concepts
What is HMAC?
HMAC (Hash-based Message Authentication Code) is an algorithm that uses a key and hash function to verify message integrity and authenticity. It has the following characteristics:
- Key Protection: Uses a key to generate authentication code, only parties with the same key can verify
- Anti-Tampering: Any modification to the message will change the HMAC value
- One-Way: Cannot derive original message or key from HMAC value
- Deterministic: Same key and message always generate the same HMAC value
HMAC vs Hash
| Feature | Hash | HMAC (Message Authentication Code) |
|---|---|---|
| Input | Only needs message | Needs key + message |
| Purpose | Data fingerprint, deduplication | Message authentication, anti-tampering |
| Security | Can be computed by anyone | Only verifiable with key |
| Typical Applications | File checking, data deduplication | API signature, JWT, Cookie signature |
How It Works
HMAC calculation process:
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))Where:
K: Keym: MessageH: Hash function (such as SHA-256)K': Padded keyopadandipad: Fixed padding values||: String concatenation⊕: XOR operation
Complete Example
local hmac = require "silly.crypto.hmac"
-- 1. Basic HMAC computation
local key = "my-secret-key"
local message = "Hello, World!"
local mac = hmac.digest(key, message, "sha256")
print(string.format("HMAC-SHA256: %s", string.gsub(mac, ".", function(c)
return string.format("%02x", string.byte(c))
end)))
-- 2. API request signature
local function sign_api_request(secret, method, path, body)
local data = method .. path .. body
return hmac.digest(secret, data, "sha256")
end
local api_secret = "api-secret-12345"
local signature = sign_api_request(api_secret, "POST", "/api/users", '{"name":"Alice"}')
print("API signature generated successfully")
-- 3. Message verification
local function verify_message(key, message, expected_mac)
local computed_mac = hmac.digest(key, message, "sha256")
return computed_mac == expected_mac
end
local original_message = "Important data"
local mac_value = hmac.digest("verification-key", original_message, "sha256")
local is_valid = verify_message("verification-key", original_message, mac_value)
print("Message verification result:", is_valid and "Passed" or "Failed")
-- 4. Different hash algorithms
local key = "test-key"
local data = "test data"
local sha1_mac = hmac.digest(key, data, "sha1")
local sha256_mac = hmac.digest(key, data, "sha256")
local sha512_mac = hmac.digest(key, data, "sha512")
print("SHA-1 HMAC length:", #sha1_mac, "bytes")
print("SHA-256 HMAC length:", #sha256_mac, "bytes")
print("SHA-512 HMAC length:", #sha512_mac, "bytes")
-- 5. Cookie signature
local function sign_cookie(secret, cookie_value)
local timestamp = tostring(os.time())
local data = cookie_value .. "|" .. timestamp
local signature = hmac.digest(secret, data, "sha256")
-- Convert to hex
local hex_sig = string.gsub(signature, ".", function(c)
return string.format("%02x", string.byte(c))
end)
return data .. "|" .. hex_sig
end
local cookie_secret = "cookie-secret-xyz"
local signed_cookie = sign_cookie(cookie_secret, "user_id=12345")
print("Signed Cookie:", signed_cookie)
-- 6. Binary data processing
local binary_key = string.char(0x01, 0x02, 0x03, 0x04)
local binary_msg = string.char(0xFF, 0xFE, 0xFD, 0xFC)
local binary_mac = hmac.digest(binary_key, binary_msg, "sha256")
print("Binary HMAC length:", #binary_mac, "bytes")
-- 7. Empty data handling
local empty_msg_mac = hmac.digest("key", "", "sha256")
local empty_key_mac = hmac.digest("", "message", "sha256")
print("Empty message HMAC computation successful")
print("Empty key HMAC computation successful")API Reference
hmac.digest(key, data, algorithm)
Computes HMAC value of message.
Parameters:
key(string): Key. Can be any length string, supports binary data.data(string): Message data to compute HMAC. Supports binary data.algorithm(string): Hash algorithm name.
Returns:
- Returns computed HMAC value as binary string.
- If computation fails, throws Lua error.
Supported Hash Algorithms:
"md5": MD5 (not recommended for security scenarios, 16 byte output)"sha1": SHA-1 (not recommended for security scenarios, 20 byte output)"sha224": SHA-224 (28 byte output)"sha256": SHA-256 (recommended, 32 byte output)"sha384": SHA-384 (48 byte output)"sha512": SHA-512 (64 byte output)"sha3-224","sha3-256","sha3-384","sha3-512": SHA-3 series"sm3": SM3 algorithm (32 byte output)
Example:
local hmac = require "silly.crypto.hmac"
-- Basic usage
local key = "my-secret-key"
local message = "Hello, World!"
local mac = hmac.digest(key, message, "sha256")
print("HMAC length:", #mac, "bytes")
-- Convert to hex for display
local function to_hex(str)
return (string.gsub(str, ".", function(c)
return string.format("%02x", string.byte(c))
end))
end
print("HMAC (hex):", to_hex(mac))
-- Compare different algorithms
local algorithms = {"sha1", "sha256", "sha512"}
for _, alg in ipairs(algorithms) do
local result = hmac.digest("key", "data", alg)
print(string.format("%s: %d bytes", alg, #result))
end
-- Binary safe
local binary_key = string.char(0, 255, 128, 127)
local binary_msg = string.char(1, 2, 3, 4)
local binary_mac = hmac.digest(binary_key, binary_msg, "sha256")
print("Binary data HMAC computation successful")Usage Examples
API Signature Verification
Most common HMAC use case in Web APIs:
local hmac = require "silly.crypto.hmac"
-- API signature tool
local api_signer = {}
-- Generate signature
function api_signer.sign(secret, method, path, body, timestamp)
-- Build signature string
local sign_string = string.format("%s\n%s\n%s\n%s",
method, path, body or "", timestamp)
-- Compute HMAC
local signature = hmac.digest(secret, sign_string, "sha256")
-- Convert to Base64 encoding (using simple hex instead)
return string.gsub(signature, ".", function(c)
return string.format("%02x", string.byte(c))
end)
end
-- Verify signature
function api_signer.verify(secret, method, path, body, timestamp, signature)
local expected = api_signer.sign(secret, method, path, body, timestamp)
return expected == signature
end
-- Usage example
local api_secret = "my-api-secret-key-123"
local timestamp = "1634567890"
-- Client: Generate signature
local method = "POST"
local path = "/api/v1/users"
local body = '{"name":"Alice","age":30}'
local signature = api_signer.sign(api_secret, method, path, body, timestamp)
print("Generated signature:", signature:sub(1, 32) .. "...")
-- Server: Verify signature
local is_valid = api_signer.verify(api_secret, method, path, body, timestamp, signature)
print("Signature verification:", is_valid and "Passed" or "Failed")
-- Tampering detection
local tampered_body = '{"name":"Bob","age":30}'
local is_tampered = api_signer.verify(api_secret, method, path, tampered_body, timestamp, signature)
print("Tampered data verification:", is_tampered and "Passed" or "Failed")JWT Token Signing
Implement simple JWT HMAC signing:
local hmac = require "silly.crypto.hmac"
local jwt = {}
-- Base64URL encoding (simplified)
local function base64url_encode(str)
local b64 = ""
-- Simplified implementation: convert to hex
for i = 1, #str do
b64 = b64 .. string.format("%02x", string.byte(str, i))
end
return b64
end
-- Create JWT
function jwt.sign(payload, secret)
-- Header (simplified)
local header = '{"alg":"HS256","typ":"JWT"}'
local header_b64 = base64url_encode(header)
-- Payload
local payload_json = string.format('{"sub":"%s","exp":%d}',
payload.sub or "", payload.exp or 0)
local payload_b64 = base64url_encode(payload_json)
-- Signature part
local sign_input = header_b64 .. "." .. payload_b64
local signature = hmac.digest(secret, sign_input, "sha256")
local signature_b64 = base64url_encode(signature)
-- Combine JWT
return sign_input .. "." .. signature_b64
end
-- Verify JWT
function jwt.verify(token, secret)
local parts = {}
for part in string.gmatch(token, "[^.]+") do
table.insert(parts, part)
end
if #parts ~= 3 then
return false, "invalid token format"
end
-- Verify signature
local sign_input = parts[1] .. "." .. parts[2]
local expected_sig = hmac.digest(secret, sign_input, "sha256")
local expected_sig_b64 = base64url_encode(expected_sig)
return parts[3] == expected_sig_b64, "signature verified"
end
-- Usage example
local jwt_secret = "jwt-secret-key-xyz"
local payload = {
sub = "user123",
exp = os.time() + 3600
}
-- Issue Token
local token = jwt.sign(payload, jwt_secret)
print("JWT Token length:", #token)
-- Verify Token
local valid, msg = jwt.verify(token, jwt_secret)
print("Token verification:", valid and "Success" or "Failed")
-- Verify with wrong key
local invalid, err = jwt.verify(token, "wrong-secret")
print("Wrong key verification:", invalid and "Success" or "Failed")Webhook Signature
Verify authenticity of Webhook callbacks:
local hmac = require "silly.crypto.hmac"
local webhook = {}
-- Compute Webhook signature
function webhook.sign(secret, payload, timestamp)
local signed_payload = timestamp .. "." .. payload
local signature = hmac.digest(secret, signed_payload, "sha256")
-- Convert to hex
return string.gsub(signature, ".", function(c)
return string.format("%02x", string.byte(c))
end)
end
-- Verify Webhook signature
function webhook.verify(secret, payload, timestamp, received_signature)
local expected = webhook.sign(secret, payload, timestamp)
-- Prevent timing attacks: use constant-time comparison
if #expected ~= #received_signature then
return false
end
local result = 0
for i = 1, #expected do
local a = string.byte(expected, i)
local b = string.byte(received_signature, i)
result = result | (a ~ b)
end
return result == 0
end
-- Usage example
local webhook_secret = "whsec_abcdef123456"
-- Simulate received Webhook
local timestamp = tostring(os.time())
local payload = '{"event":"payment.success","amount":100}'
-- Sender: Generate signature
local signature = webhook.sign(webhook_secret, payload, timestamp)
print("Webhook signature:", signature:sub(1, 32) .. "...")
-- Receiver: Verify signature
local is_valid = webhook.verify(webhook_secret, payload, timestamp, signature)
print("Webhook verification:", is_valid and "Passed" or "Failed")
-- Replay attack prevention: Check timestamp
local current_time = os.time()
local webhook_time = tonumber(timestamp)
local time_diff = math.abs(current_time - webhook_time)
if time_diff > 300 then -- 5 minute validity
print("Warning: Webhook timestamp expired")
else
print("Timestamp verification: Passed")
end(The rest of the examples continue in similar fashion with Password Storage HMAC, Session Cookie Signing, File Integrity Verification, Message Queue Signing, etc.)
Notes
1. Key Security
Key is the core of HMAC security:
- Key Length: Recommend using at least 32 bytes (256 bits) random key
- Key Storage: Don't hardcode keys in code, use environment variables or key management services
- Key Rotation: Regularly change keys, especially when leakage is suspected
- Key Separation: Use different keys for different purposes (API, Cookie, files, etc.)
-- ❌ Bad practice
local key = "123456" -- Too weak
-- ✅ Good practice
local key = os.getenv("HMAC_SECRET_KEY") -- Read from environment variable
if not key or #key < 32 then
error("HMAC secret key must be at least 32 bytes")
end2. Algorithm Selection
Choose appropriate hash algorithm for different scenarios:
| Algorithm | Security | Performance | Recommended Scenarios |
|---|---|---|---|
| MD5 | ❌ Weak | Fast | Not recommended |
| SHA-1 | ⚠️ Deprecated | Faster | Not recommended for new projects |
| SHA-256 | ✅ Strong | Medium | Recommended (general scenarios) |
| SHA-512 | ✅ Very Strong | Slower | High security requirements |
| SM3 | ✅ Strong | Medium | SM compliance scenarios |
3. Timing Attack Protection
Use constant-time comparison when verifying HMAC:
-- ❌ Unsafe: Early exit leaks information
local function unsafe_compare(a, b)
if #a ~= #b then return false end
for i = 1, #a do
if string.byte(a, i) ~= string.byte(b, i) then
return false -- Early exit!
end
end
return true
end
-- ✅ Safe: Constant-time comparison
local function safe_compare(a, b)
if #a ~= #b then return false end
local result = 0
for i = 1, #a do
result = result | (string.byte(a, i) ~ string.byte(b, i))
end
return result == 0
end4. Difference from Hash
HMAC and Hash cannot be mixed:
local hmac = require "silly.crypto.hmac"
local hash = require "silly.crypto.hash"
local key = "secret"
local data = "message"
-- These two results are completely different!
local hmac_result = hmac.digest(key, data, "sha256")
local hash_result = hash.digest(data, "sha256")
-- ❌ Wrong: Use Hash instead of HMAC
-- Anyone can compute Hash, cannot verify message source
-- ✅ Correct: Use HMAC for authentication
-- Only those with the key can generate valid HMAC5. Binary Safety
HMAC result is binary data:
local hmac = require "silly.crypto.hmac"
local mac = hmac.digest("key", "data", "sha256")
-- ❌ Don't print or store binary data directly
-- print(mac) -- May produce invisible characters
-- ✅ Convert to hex or Base64
local hex = string.gsub(mac, ".", function(c)
return string.format("%02x", string.byte(c))
end)
print("HMAC (hex):", hex)6. Performance Considerations
HMAC computation has performance overhead:
- Cache Results: Cache HMAC values for static data
- Batch Computation: Avoid repeatedly computing HMAC for same data in loops
- Algorithm Selection: SHA-256 is usually the best balance of performance and security
7. Replay Attack Prevention
Verifying HMAC alone is not enough to prevent replay attacks:
-- ✅ Include timestamp or nonce
local function sign_with_timestamp(key, data)
local timestamp = tostring(os.time())
local sign_data = data .. "|" .. timestamp
local mac = hmac.digest(key, sign_data, "sha256")
return mac, timestamp
end
-- Verify and check timestamp
local function verify_with_timestamp(key, data, timestamp, mac)
local sign_data = data .. "|" .. timestamp
local expected = hmac.digest(key, sign_data, "sha256")
if expected ~= mac then
return false, "invalid signature"
end
local current_time = os.time()
local msg_time = tonumber(timestamp)
if math.abs(current_time - msg_time) > 300 then -- 5 minute validity
return false, "expired"
end
return true
end8. Empty Key and Empty Data
Although empty key and empty data are supported, not recommended:
-- ⚠️ Although it works, not recommended
local empty_key_mac = hmac.digest("", "message", "sha256")
local empty_data_mac = hmac.digest("key", "", "sha256")
-- ✅ Always use strong key and non-empty data
local strong_mac = hmac.digest("strong-secret-key", "message", "sha256")See Also
- silly.crypto.hash: Hash functions (for data fingerprints, not for authentication)
- silly.crypto.cipher: Symmetric encryption algorithms (for confidentiality, not integrity)
- silly.security.jwt: JWT Token implementation (uses HMAC signing internally)
- silly.net.http: HTTP server (commonly used for API signature verification)
Standards Reference
- RFC 2104: HMAC: Keyed-Hashing for Message Authentication
- RFC 4231: Identifiers and Test Vectors for HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512
- FIPS 198-1: The Keyed-Hash Message Authentication Code (HMAC)
Security Recommendations
- Use SHA-256 or stronger: Avoid using MD5 and SHA-1
- Key Length: At least 32 bytes random key
- Constant-time Comparison: Prevent timing attacks
- Include Timestamp: Prevent replay attacks
- Key Isolation: Use different keys for different purposes
- Regular Rotation: Regularly change keys
- Secure Storage: Don't hardcode keys
- HTTPS Transport: Signature values should be transmitted over encrypted channels