Skip to content

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

The HMAC Provider implements HMAC-SHA256 signatures with two operation modes:

  1. Simple Mode: Basic HMAC signatures without replay protection
  2. 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

  1. HMACProvider: Main orchestrator for signature operations
  2. HMACKeyProvider: Interface for secret key management
  3. NonceStore: Interface for replay attack prevention
  4. 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

type HMACKeyProvider interface {
    GetKey(keyId string) (*secure.Credential, error)
}

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.

provider := hmacprovider.NewHmacProvider(keyProvider,
    hmacprovider.WithNonceStore(customStore),
)

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 signature
  • X-HMAC-Timestamp: RFC3339 formatted timestamp
  • X-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

  1. Always Use Secure Mode: Use Sign256/Verify256 for replay protection
  2. Strong Secrets: Generate cryptographically secure secrets (32+ bytes)
  3. Key Rotation: Implement regular key rotation policies
  4. Secure Storage: Use encrypted credential storage
  5. HTTPS Only: Always use TLS for transport security
  6. Input Validation: Set appropriate MaxInputSize limits
  7. Clock Sync: Ensure server clocks are synchronized (NTP)
  8. Monitoring: Log and monitor authentication failures

Performance Optimization

  1. Choose Appropriate Backend:
  2. Memory: Single-instance, low-traffic
  3. Redis: Multi-instance, high-traffic
  4. Custom KV: Specific requirements

  5. Tune Configuration:

  6. Adjust TTL based on security requirements
  7. Set cleanup intervals based on traffic
  8. Choose eviction policy based on memory

  9. Connection Pooling:

  10. Use connection pools for Redis
  11. 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

  1. Reuse Provider Instances: Create once, use many times
  2. Buffer Pool: Use sync.Pool for byte buffers
  3. Batch Operations: Process multiple items in sequence
  4. Connection Pooling: Configure Redis connection pools
  5. 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)