HMAC Provider¶
The HMAC Provider offers cryptographically secure message authentication and signature verification using HMAC-SHA256. It provides protection against replay attacks, timing attacks, and memory exhaustion DoS attacks.
Table of Contents¶
- Overview
- Architecture
- Security Features
- Quick Start
- API Reference
- Key Providers
- Nonce Stores
- Configuration Options
- HTTP Authentication
- Best Practices
- Examples
- Performance
- Troubleshooting
Overview¶
The HMAC Provider implements HMAC-SHA256 signatures with two operation modes:
- Simple Mode: Basic HMAC signatures without replay protection
- Secure Mode: HMAC signatures with nonces and timestamps for replay protection
Key Features¶
- Replay Attack Prevention: Nonce-based protection with atomic check-and-set (memory and Redis stores, and KV backends implementing
kv.AtomicSetter) - Timing Attack Resistance: Constant-time comparisons for security
- DoS Protection: Configurable input size limits (default: 32MB)
- Pluggable Storage: Memory, Redis, and generic KV backends
- Clock Drift Tolerance: Configurable timestamp validation windows
- Multi-tenant Support: Key provider interface for multiple secrets
Architecture¶
The HMAC Provider consists of several components working together:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ HMAC Provider │────▶│ Key Provider │ │ Nonce Store │
│ │ │ (Secret Mgmt) │ │ (Replay Prot) │
└────────┬────────┘ └──────────────────┘ └─────────────────┘
│ │
│ ┌──────────────────┐ │
└─────────────▶│ Secure │◀──────────────┘
│ Credential │
└──────────────────┘
Core Components¶
- HMACProvider: Main orchestrator for signature operations
- HMACKeyProvider: Interface for secret key management
- NonceStore: Interface for replay attack prevention
- Secure Credential: Encrypted secret storage with memory protection
Security Features¶
Implemented Protections¶
- Replay Protection: UUID-based nonces with TTL expiration
- Timing Attack Resistance: Constant-time HMAC verification
- Input Size Limits: Prevents memory exhaustion attacks
- Timestamp Validation: Configurable time windows for clock drift
- Atomic Operations: Thread-safe nonce consumption; the generic KV store is atomic only when the backend implements
kv.AtomicSetter - Secure Storage: Integration with encrypted credential system
- Fail-Safe Defaults: Secure configuration out of the box
Security Properties¶
- Cryptographic Integrity: HMAC-SHA256 ensures message authenticity
- Non-repudiation: Nonces prevent request replay
- Forward Secrecy: Time-limited validity of signatures
- Defense in Depth: Multiple layers of validation
Quick Start¶
Basic Usage¶
package main
import (
"strings"
"github.com/oddbit-project/blueprint/crypt/secure"
"github.com/oddbit-project/blueprint/provider/hmacprovider"
)
func main() {
// Generate encryption key
key, err := secure.GenerateKey()
if err != nil {
panic(err)
}
// Create credential from password
secret, err := secure.NewCredential([]byte("my-secret"), key, false)
if err != nil {
panic(err)
}
// create single key provider
keyProvider := hmacprovider.NewSingleKeyProvider("mykey", secret)
// Create HMAC provider
provider := hmacprovider.NewHmacProvider(keyProvider)
// Sign data with replay protection
data := "Hello, World!"
hash, timestamp, nonce, err := provider.Sign256("mykey", strings.NewReader(data))
if err != nil {
panic(err)
}
// Verify signature
keyId, valid, err := provider.Verify256(strings.NewReader(data), hash, timestamp, nonce)
if err != nil {
panic(err)
}
if valid {
println("Signature verified! Key ID:", keyId)
}
}
HTTP Authentication Integration¶
import (
"github.com/oddbit-project/blueprint/crypt/secure"
"github.com/oddbit-project/blueprint/provider/httpserver/auth"
"github.com/oddbit-project/blueprint/provider/hmacprovider"
)
// Generate encryption key
key, err := secure.GenerateKey()
if err != nil {
panic(err)
}
// Create credential from password
secret, err := secure.NewCredential([]byte("my-secret"), key, false)
if err != nil {
panic(err)
}
// create single key provider
keyProvider := hmacprovider.NewSingleKeyProvider("mykey", secret)
// Create HMAC provider
provider := hmacprovider.NewHmacProvider(keyProvider)
// Create HMAC auth provider
hmacAuth := auth.NewHMACAuthProvider(provider)
// Apply to routes
router.Use(auth.AuthMiddleware(hmacAuth))
// Access authentication info in handlers
func handler(c *gin.Context) {
keyId, ok := auth.GetHMACIdentity(c)
if ok {
// Use keyId for tenant identification
}
// Get full HMAC details
keyId, timestamp, nonce, ok := auth.GetHMACDetails(c)
if ok {
// Access all HMAC authentication data
}
}
API Reference¶
Note: keyId cannot contain dots ('.') as they are used for keyId+hash concatenation
Constructor¶
NewHmacProvider(keyProvider HMACKeyProvider, opts ...HMACProviderOption) *HMACProvider¶
Creates a new HMAC provider with the specified key provider and options.
Parameters:
- keyProvider: Implementation of HMACKeyProvider interface
- opts: Optional configuration functions
Returns: Configured HMAC provider instance
Simple HMAC Methods¶
SHA256Sign(keyId string, data io.Reader) (string, error)¶
Generates a simple HMAC-SHA256 signature without replay protection.
Parameters:
- keyId: Identifier for the key to use
- data: Input data to sign
Returns:
- string: Hex-encoded HMAC signature
- error: Any error that occurred
SHA256Verify(data io.Reader, hash string) (keyId string, valid bool, error)¶
Verifies a simple HMAC-SHA256 signature.
Parameters:
- data: Input data to verify
- hash: Hex-encoded HMAC signature to verify
Returns:
- keyId: Identifier of the key that validated the signature
- valid: True if signature is valid
- error: Any error that occurred
Secure HMAC Methods¶
Sign256(keyId string, data io.Reader) (hash, timestamp, nonce string, err error)¶
Generates a secure HMAC-SHA256 signature with replay protection.
Parameters:
- keyId: Identifier for the key to use
- data: Input data to sign
Returns:
- hash: Hex-encoded HMAC signature
- timestamp: RFC3339 timestamp
- nonce: UUID v4 nonce
- err: Any error that occurred
Verify256(data io.Reader, hash, timestamp, nonce string) (keyId string, valid bool, error)¶
Verifies a secure HMAC-SHA256 signature with replay protection.
Parameters:
- data: Input data to verify
- hash: Hex-encoded HMAC signature
- timestamp: RFC3339 timestamp from signing
- nonce: UUID nonce from signing
Returns:
- keyId: Identifier of the key that validated the signature
- valid: True if signature is valid and not replayed
- error: Any error that occurred
Key Providers¶
The HMAC system supports multiple key management strategies through the HMACKeyProvider interface:
Interface Definition¶
Single Key Provider¶
Note: the keyId can be an empty string
Simple provider for single-key applications:
// Create single key provider
provider := hmacprovider.NewSingleKeyProvider("myKeyId", credential)
// Always uses the same key regardless of keyId
hmac := hmacprovider.NewHmacProvider(provider)
Multi-Tenant Key Provider¶
For applications with multiple tenants or key rotation:
type MultiTenantKeyProvider struct {
keys map[string]*secure.Credential
m sync.RWMutex
}
func (m *MultiTenantKeyProvider) GetKey(keyId string) (*secure.Credential, error) {
m.m.RLock()
defer m.m.RUnlock()
key, exists := m.keys[keyId]
if !exists {
return nil, errors.New("unknown key ID")
}
return key, nil
}
func (m *MultiTenantKeyProvider) ListKeyIds() []string {
m.m.RLock()
defer m.m.RUnlock()
ids := make([]string, 0, len(m.keys))
for id := range m.keys {
ids = append(ids, id)
}
return ids
}
Nonce Stores¶
The HMAC provider supports multiple nonce store backends for replay protection:
Memory Store (Default)¶
In-memory storage with configurable TTL and eviction policies.
import "github.com/oddbit-project/blueprint/provider/hmacprovider/store"
// Create with custom options
memoryStore := store.NewMemoryNonceStore(
store.WithTTL(1*time.Hour),
store.WithMaxSize(1000000),
store.WithCleanupInterval(15*time.Minute),
store.WithEvictPolicy(store.EvictHalfLife()),
)
provider := hmacprovider.NewHmacProvider(keyProvider,
hmacprovider.WithNonceStore(memoryStore),
)
Eviction Policies:
- EvictNone(): No automatic eviction (default)
- EvictAll(): Remove all nonces when at capacity
- EvictHalfLife(): Remove nonces older than TTL/2. An evicted nonce can be replayed while its timestamp is still
accepted, so keep the TTL well above twice the timestamp tolerance (the defaults, 4h TTL and 5m tolerance, are safe)
Best For: Single-instance applications, development, low-traffic APIs
Redis Store¶
Redis-backed storage with atomic operations for distributed systems.
import (
"github.com/oddbit-project/blueprint/provider/redis"
"github.com/oddbit-project/blueprint/provider/hmacprovider/store"
)
// Configure Redis client
config := redis.NewConfig()
config.Address = "localhost:6379"
config.DB = 1
redisClient, err := redis.NewClient(config)
if err != nil {
panic(err)
}
// Create Redis nonce store
redisStore := store.NewRedisNonceStore(
redisClient,
1*time.Hour, // TTL
store.WithPrefix("hmac:nonce:"), // key prefix (default "nonce:")
store.WithTimeout(2*time.Second), // per-operation timeout (default 5s)
)
provider := hmacprovider.NewHmacProvider(keyProvider,
hmacprovider.WithNonceStore(redisStore),
)
Features:
- Atomic SetNX operations
- Configurable key prefix for namespacing (WithPrefix); the client's KeyPrefix is not applied to nonce keys
- Automatic TTL management
- Network timeout handling (WithTimeout); on a Redis error the nonce is rejected
Calling Close() on the nonce store closes the Redis client passed to NewRedisNonceStore.
Best For: Multi-instance deployments, high-traffic APIs, production systems
Generic KV Store¶
Adapter for any key-value backend implementing the KV interface.
import (
"github.com/oddbit-project/blueprint/provider/kv"
"github.com/oddbit-project/blueprint/provider/hmacprovider/store"
)
// Use any KV implementation
var kvBackend kv.KV = getYourKVBackend()
kvStore := store.NewKvStore(kvBackend, 1*time.Hour)
provider := hmacprovider.NewHmacProvider(keyProvider,
hmacprovider.WithNonceStore(kvStore),
)
If the backend implements kv.AtomicSetter (SetNX), the nonce check-and-set is a single atomic
operation; both kv.NewMemoryKV() and the Redis client implement it. Other backends fall back to a
Get followed by SetTTL, which is not atomic: two concurrent requests with the same nonce can both
be accepted.
Best For: Custom storage requirements, existing KV infrastructure
Configuration Options¶
WithNonceStore(store NonceStore)¶
Sets the nonce store backend for replay protection.
WithKeyInterval(interval time.Duration)¶
Sets the allowed timestamp deviation window. Default: 5 minutes.
provider := hmacprovider.NewHmacProvider(keyProvider,
hmacprovider.WithKeyInterval(10*time.Minute), // ±10 minutes
)
WithMaxInputSize(maxSize int)¶
Sets the maximum input size to prevent DoS attacks. Default: 32MB.
provider := hmacprovider.NewHmacProvider(keyProvider,
hmacprovider.WithMaxInputSize(1024*1024), // 1MB limit
)
HTTP Authentication¶
Required Headers¶
When using HMAC authentication with HTTP, the following headers are required:
X-HMAC-Hash: The HMAC-SHA256 signatureX-HMAC-Timestamp: RFC3339 formatted timestampX-HMAC-Nonce: UUID v4 nonce
Client Implementation¶
func makeAuthenticatedRequest(provider *hmacprovider.HMACProvider, url string, body []byte) error {
// Generate signature
bodyReader := bytes.NewReader(body)
hash, timestamp, nonce, err := provider.Sign256("client-key", bodyReader)
if err != nil {
return err
}
// Create request
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return err
}
// Add HMAC headers
req.Header.Set("X-HMAC-Hash", hash)
req.Header.Set("X-HMAC-Timestamp", timestamp)
req.Header.Set("X-HMAC-Nonce", nonce)
req.Header.Set("Content-Type", "application/json")
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
Server Integration¶
import (
"github.com/gin-gonic/gin"
"github.com/oddbit-project/blueprint/provider/httpserver/auth"
"github.com/oddbit-project/blueprint/crypt/secure"
"github.com/oddbit-project/blueprint/provider/hmacprovider"
)
func createHMACProvider() *hmacprovider.HMACProvider {
// Generate encryption key
key, err := secure.GenerateKey()
if err != nil {
panic(err)
}
// Create credential from password
secret, err := secure.NewCredential([]byte("my-secret"), key, false)
if err != nil {
panic(err)
}
// create single key provider
keyProvider := hmacprovider.NewSingleKeyProvider("client-key", secret)
// Create HMAC provider
return hmacprovider.NewHmacProvider(keyProvider)
}
func setupServer() {
router := gin.Default()
// Create HMAC provider
hmacProvider := createHMACProvider()
// Create auth middleware
hmacAuth := auth.NewHMACAuthProvider(hmacProvider)
// Protected routes
api := router.Group("/api")
api.Use(auth.AuthMiddleware(hmacAuth))
{
api.POST("/data", handleData)
api.PUT("/update", handleUpdate)
}
router.Run(":8080")
}
func handleData(c *gin.Context) {
// Get authenticated key ID
keyId, exists := auth.GetHMACIdentity(c)
if !exists {
c.JSON(500, gin.H{"error": "Authentication info missing"})
return
}
// Get full HMAC details
keyId, timestamp, nonce, ok := auth.GetHMACDetails(c)
if !ok {
c.JSON(500, gin.H{"error": "HMAC details missing"})
return
}
c.JSON(200, gin.H{
"message": "Authenticated request",
"tenant": keyId,
"timestamp": timestamp,
"nonce": nonce,
})
}
Best Practices¶
Security Recommendations¶
- Always Use Secure Mode: Use
Sign256/Verify256for replay protection - Strong Secrets: Generate cryptographically secure secrets (32+ bytes)
- Key Rotation: Implement regular key rotation policies
- Secure Storage: Use encrypted credential storage
- HTTPS Only: Always use TLS for transport security
- Input Validation: Set appropriate
MaxInputSizelimits - Clock Sync: Ensure server clocks are synchronized (NTP)
- Monitoring: Log and monitor authentication failures
Performance Optimization¶
- Choose Appropriate Backend:
- Memory: Single-instance, low-traffic
- Redis: Multi-instance, high-traffic
-
Custom KV: Specific requirements
-
Tune Configuration:
- Adjust TTL based on security requirements
- Set cleanup intervals based on traffic
-
Choose eviction policy based on memory
-
Connection Pooling:
- Use connection pools for Redis
- Configure appropriate timeouts
Error Handling¶
func handleHMACError(err error, clientIP string) {
if err != nil {
switch {
case strings.Contains(err.Error(), "invalid request"):
// Input validation failure
log.Warn("Invalid HMAC request", "ip", clientIP, "error", err)
case strings.Contains(err.Error(), "input too large"):
// Potential DoS attempt
log.Error("HMAC input too large", "ip", clientIP, "error", err)
case strings.Contains(err.Error(), "nonce already used"):
// Replay attack
log.Error("HMAC replay attack detected", "ip", clientIP, "error", err)
default:
// Other errors
log.Error("HMAC verification failed", "ip", clientIP, "error", err)
}
}
}
Examples¶
Multi-Tenant API¶
type TenantKeyProvider struct {
tenants map[string]*secure.Credential
mu sync.RWMutex
}
func (t *TenantKeyProvider) GetKey(tenantId string) (*secure.Credential, error) {
t.mu.RLock()
defer t.mu.RUnlock()
cred, exists := t.tenants[tenantId]
if !exists {
return nil, fmt.Errorf("unknown tenant: %s", tenantId)
}
return cred, nil
}
func (t *TenantKeyProvider) ListKeyIds() []string {
t.mu.RLock()
defer t.mu.RUnlock()
ids := make([]string, 0, len(t.tenants))
for id := range t.tenants {
ids = append(ids, id)
}
return ids
}
// Usage
tenantProvider := &TenantKeyProvider{
tenants: loadTenantKeys(),
}
hmacProvider := hmacprovider.NewHmacProvider(
tenantProvider,
hmacprovider.WithNonceStore(redisStore),
hmacprovider.WithKeyInterval(10*time.Minute),
)
Webhook Verification¶
func verifyWebhook(provider *hmacprovider.HMACProvider, r *http.Request) error {
// Extract headers
hash := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
nonce := r.Header.Get("X-Webhook-Id")
// Read body
body, err := io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("failed to read body: %w", err)
}
r.Body = io.NopCloser(bytes.NewReader(body))
// Verify signature
keyId, valid, err := provider.Verify256(
bytes.NewReader(body),
hash,
timestamp,
nonce,
)
if err != nil {
return fmt.Errorf("verification error: %w", err)
}
if !valid {
return errors.New("invalid webhook signature")
}
log.Info("Webhook verified", "source", keyId)
return nil
}
Performance¶
Benchmarks¶
Performance results on Intel Core i5-10400F @ 2.90GHz:
- SHA256Sign: ~2.1μs per operation (2,184 B/op, 18 allocs/op)
- SHA256Verify: ~2.0μs per operation (2,008 B/op, 15 allocs/op)
- Sign256 (with nonce): ~3.1μs per operation (2,344 B/op, 25 allocs/op)
- Verify256 (with nonce): ~2.9μs per operation (2,213 B/op, 19 allocs/op)
- Full Cycle (Sign256 + Verify256): ~6.1μs per operation (4,557 B/op, 44 allocs/op)
Optimization Tips¶
- Reuse Provider Instances: Create once, use many times
- Buffer Pool: Use sync.Pool for byte buffers
- Batch Operations: Process multiple items in sequence
- Connection Pooling: Configure Redis connection pools
- Async Processing: Use goroutines for independent verifications
Troubleshooting¶
Common Issues¶
"invalid request" Error¶
Cause: Input validation failure Solution: - Check all parameters are provided - Verify timestamp format (RFC3339) - Ensure nonce is valid UUID
"input too large" Error¶
Cause: Input exceeds MaxInputSize
Solution:
- Increase limit with WithMaxInputSize
- Reduce input size
- Check for erroneous large inputs
"nonce already used" Error¶
Cause: Replay attack or duplicate request Solution: - Ensure unique nonce generation - Check for request retry logic - Verify nonce store is working
Clock Drift Issues¶
Symptoms: Intermittent verification failures Solution: - Sync server clocks with NTP - Increase KeyInterval tolerance - Monitor timestamp differences
Constants¶
DefaultKeyInterval: 5 minutes (300 seconds)DefaultMaxInputSize: 32MB (33554432 bytes)DefaultTTL: 4 hours (nonce stores)DefaultMaxSize: 2,000,000 entries (memory store)DefaultCleanupInterval: 15 minutes (memory store)