CrackedRuby logo

CrackedRuby

HMAC

Overview

Hash-based Message Authentication Code (HMAC) provides cryptographic authentication for messages using a secret key and a hash function. Ruby implements HMAC through the OpenSSL::HMAC class in the standard library, supporting various hash algorithms including SHA-256, SHA-512, and MD5.

The HMAC algorithm combines a secret key with message data through a specific construction that prevents length extension attacks and provides strong authentication guarantees. Ruby's implementation wraps OpenSSL's HMAC functionality, providing both one-shot computation methods and streaming interfaces for large datasets.

HMAC authentication appears frequently in API authentication, webhook verification, session token generation, and secure communication protocols. The algorithm's security depends on the hash function strength and key secrecy, making proper key management essential.

require 'openssl'

# Basic HMAC computation
key = 'secret_key'
message = 'Hello, World!'
hmac = OpenSSL::HMAC.hexdigest('SHA256', key, message)
# => "4f8b42c22dd3729b519ba6f68d2da7cc5b2d606d05daed5ad5128cc03e6c6358"

Ruby provides multiple interfaces for HMAC computation: class methods for simple operations, instance methods for complex scenarios, and streaming methods for large data processing. The digest output format includes both binary and hexadecimal representations.

# Binary digest output
binary_hmac = OpenSSL::HMAC.digest('SHA256', key, message)
# => "\x4F\x8B\x42\xC2..."

# Instance-based computation
hmac_instance = OpenSSL::HMAC.new(key, 'SHA256')
hmac_instance.update(message)
result = hmac_instance.hexdigest
# => "4f8b42c22dd3729b519ba6f68d2da7cc5b2d606d05daed5ad5128cc03e6c6358"

The OpenSSL::HMAC class supports all hash algorithms available in the underlying OpenSSL installation. Common algorithms include SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, and MD5, though SHA-256 or stronger algorithms are recommended for security applications.

Basic Usage

The OpenSSL::HMAC class provides both class methods for immediate computation and instance methods for incremental processing. Class methods handle simple scenarios where the entire message is available, while instance methods support streaming operations and complex message construction.

The primary class method OpenSSL::HMAC.hexdigest computes HMAC values directly from key, algorithm, and message parameters. This method returns hexadecimal-encoded strings suitable for storage, comparison, and transmission.

require 'openssl'

# Standard HMAC computation with different algorithms
key = 'my_secret_key'
message = 'authenticate this message'

sha256_hmac = OpenSSL::HMAC.hexdigest('SHA256', key, message)
sha512_hmac = OpenSSL::HMAC.hexdigest('SHA512', key, message)
md5_hmac = OpenSSL::HMAC.hexdigest('MD5', key, message)

puts sha256_hmac
# => "b8f5b4c0eab0b3c6a2d5e4f3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3"

For binary output requirements, OpenSSL::HMAC.digest returns raw bytes instead of hexadecimal strings. Binary output provides space efficiency and direct compatibility with protocols expecting binary HMAC values.

# Binary HMAC output
binary_hmac = OpenSSL::HMAC.digest('SHA256', key, message)
puts binary_hmac.length
# => 32 (SHA-256 produces 32-byte output)

# Convert binary to base64 for transport
base64_hmac = [binary_hmac].pack('m0')
# => "uPW0wOqws8ai1eT...="

Instance methods provide incremental HMAC computation for scenarios involving large messages, streaming data, or complex message construction. The instance approach allows building messages piece by piece while maintaining authentication state.

# Incremental HMAC computation
hmac = OpenSSL::HMAC.new(key, 'SHA256')
hmac.update('Part one: ')
hmac.update('Part two: ')
hmac.update('Final part.')

final_digest = hmac.hexdigest
# => "c5d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3"

String encoding affects HMAC computation, particularly when processing international text or binary data. Ruby converts strings to bytes using their current encoding, making encoding consistency important for reproducible results.

# Encoding affects HMAC results
utf8_message = 'café'.encode('UTF-8')
latin1_message = 'café'.encode('ISO-8859-1')

utf8_hmac = OpenSSL::HMAC.hexdigest('SHA256', key, utf8_message)
latin1_hmac = OpenSSL::HMAC.hexdigest('SHA256', key, latin1_message)

puts utf8_hmac == latin1_hmac
# => false (different byte representations produce different HMACs)

Key material can include any binary data, though security considerations apply to key selection and management. Keys shorter than the hash output length get padded internally, while longer keys get hashed to the appropriate length.

# Different key types and lengths
short_key = 'key'
long_key = 'very_long_key_material_that_exceeds_block_size' * 10
binary_key = Random.bytes(32)

short_hmac = OpenSSL::HMAC.hexdigest('SHA256', short_key, message)
long_hmac = OpenSSL::HMAC.hexdigest('SHA256', long_key, message)
binary_hmac = OpenSSL::HMAC.hexdigest('SHA256', binary_key, message)

# All produce valid HMACs with different security properties

Error Handling & Debugging

HMAC operations can fail due to invalid algorithms, encoding issues, or OpenSSL library problems. Ruby raises specific exception types that applications should handle appropriately to maintain security and provide meaningful error messages.

The most common error occurs when specifying unsupported digest algorithms. OpenSSL installations may not include all algorithms, and algorithm names are case-sensitive. Ruby raises RuntimeError exceptions when encountering unknown digest types.

require 'openssl'

key = 'test_key'
message = 'test message'

begin
  # Invalid algorithm name
  hmac = OpenSSL::HMAC.hexdigest('INVALID_ALGO', key, message)
rescue RuntimeError => e
  puts "Algorithm error: #{e.message}"
  # => "Algorithm error: Unsupported digest algorithm (INVALID_ALGO)"
end

begin
  # Case-sensitive algorithm names
  hmac = OpenSSL::HMAC.hexdigest('sha256', key, message)  # lowercase
rescue RuntimeError => e
  puts "Case sensitivity error: #{e.message}"
  # Use 'SHA256' instead
end

String encoding problems manifest as inconsistent HMAC values across different environments or unexpected authentication failures. These issues often appear in applications processing user input, file data, or network protocols with varying encoding assumptions.

# Debugging encoding-related HMAC inconsistencies
def debug_hmac_encoding(key, message)
  puts "Message encoding: #{message.encoding}"
  puts "Message bytes: #{message.bytes.inspect}"
  
  # Force UTF-8 encoding for consistency
  normalized_message = message.encode('UTF-8')
  hmac = OpenSSL::HMAC.hexdigest('SHA256', key, normalized_message)
  
  puts "HMAC: #{hmac}"
  hmac
rescue Encoding::UndefinedConversionError => e
  puts "Encoding conversion failed: #{e.message}"
  # Handle binary data or invalid characters
  binary_hmac = OpenSSL::HMAC.hexdigest('SHA256', key, message.force_encoding('BINARY'))
  puts "Binary HMAC: #{binary_hmac}"
  binary_hmac
end

# Test with problematic input
problematic_input = "caf\xE9".force_encoding('ASCII-8BIT')
debug_hmac_encoding('key', problematic_input)

Nil or empty values require careful handling to prevent security vulnerabilities and application crashes. HMAC computation with nil keys or messages produces different behaviors depending on Ruby's type conversion rules.

# Handling nil and empty values safely
def safe_hmac(algorithm, key, message)
  # Validate inputs before computation
  raise ArgumentError, "Key cannot be nil" if key.nil?
  raise ArgumentError, "Message cannot be nil" if message.nil?
  raise ArgumentError, "Algorithm cannot be empty" if algorithm.to_s.empty?
  
  # Convert non-string keys to strings with explicit encoding
  key = key.to_s.encode('UTF-8') unless key.is_a?(String)
  message = message.to_s.encode('UTF-8') unless message.is_a?(String)
  
  OpenSSL::HMAC.hexdigest(algorithm, key, message)
rescue => e
  # Log error details for debugging
  puts "HMAC computation failed: #{e.class}: #{e.message}"
  puts "Algorithm: #{algorithm.inspect}"
  puts "Key length: #{key&.length || 'nil'}"
  puts "Message length: #{message&.length || 'nil'}"
  raise
end

# Test error handling
begin
  safe_hmac('SHA256', nil, 'message')
rescue ArgumentError => e
  puts e.message
  # => "Key cannot be nil"
end

Instance method errors often occur during incremental updates, particularly when calling digest methods multiple times or updating after finalization. Ruby's HMAC instances become unusable after calling digest methods, requiring fresh instances for additional computations.

# Instance lifecycle error handling
def demonstrate_instance_errors
  key = 'test_key'
  hmac = OpenSSL::HMAC.new(key, 'SHA256')
  
  hmac.update('first part')
  first_digest = hmac.hexdigest
  
  begin
    # Attempting to use instance after finalization
    hmac.update('second part')
    second_digest = hmac.hexdigest
  rescue RuntimeError => e
    puts "Instance reuse error: #{e.message}"
    # Create new instance for additional computations
    new_hmac = OpenSSL::HMAC.new(key, 'SHA256')
    new_hmac.update('first part')
    new_hmac.update('second part')
    second_digest = new_hmac.hexdigest
  end
  
  [first_digest, second_digest]
end

Memory and resource constraints can affect HMAC operations with very large messages or high-frequency computations. Monitor memory usage and implement appropriate resource management for production applications.

# Resource monitoring for large HMAC operations
def monitor_hmac_resources(key, large_data_source)
  start_memory = `ps -o rss= -p #{Process.pid}`.to_i
  start_time = Time.now
  
  hmac = OpenSSL::HMAC.new(key, 'SHA256')
  
  large_data_source.each_chunk do |chunk|
    hmac.update(chunk)
    
    # Monitor memory growth
    current_memory = `ps -o rss= -p #{Process.pid}`.to_i
    if current_memory > start_memory * 2
      puts "Memory usage doubled: #{current_memory - start_memory} KB growth"
    end
  end
  
  result = hmac.hexdigest
  end_time = Time.now
  
  puts "HMAC computation completed in #{end_time - start_time} seconds"
  result
end

Production Patterns

Production HMAC usage requires careful consideration of security, performance, and integration patterns. Applications typically use HMAC for API authentication, webhook verification, session management, and secure token generation, each with specific implementation requirements.

API authentication commonly employs HMAC signatures to verify request integrity and authenticate clients. This pattern involves signing request components including method, URL, headers, and body content to prevent tampering and replay attacks.

class APIAuthenticator
  def initialize(secret_key)
    @secret_key = secret_key
  end
  
  def sign_request(method, url, headers, body)
    # Construct canonical request string
    canonical_headers = headers.sort.map { |k, v| "#{k.downcase}:#{v}" }.join("\n")
    string_to_sign = [
      method.upcase,
      url,
      canonical_headers,
      Digest::SHA256.hexdigest(body || '')
    ].join("\n")
    
    # Generate HMAC signature
    signature = OpenSSL::HMAC.hexdigest('SHA256', @secret_key, string_to_sign)
    
    # Return authorization header value
    "HMAC-SHA256 Credential=#{@secret_key[0..7]}, Signature=#{signature}"
  end
  
  def verify_request(method, url, headers, body, provided_signature)
    expected_signature = sign_request(method, url, headers, body)
    secure_compare(expected_signature, provided_signature)
  end
  
  private
  
  # Constant-time comparison to prevent timing attacks
  def secure_compare(a, b)
    return false unless a.length == b.length
    
    result = 0
    a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
    result == 0
  end
end

# Usage in Rails controller
authenticator = APIAuthenticator.new(ENV['API_SECRET'])
auth_header = request.headers['Authorization']
signature = auth_header&.match(/Signature=([a-f0-9]+)/)&.captures&.first

if authenticator.verify_request(request.method, request.url, request.headers, request.raw_post, signature)
  # Process authenticated request
else
  render json: { error: 'Invalid signature' }, status: 401
end

Webhook verification protects applications from forged webhook payloads by validating signatures provided by webhook services. Different services use varying signature formats and algorithms, requiring flexible verification implementations.

class WebhookVerifier
  GITHUB_SIGNATURE_PREFIX = 'sha256='
  STRIPE_SIGNATURE_PREFIX = 't='
  
  def self.verify_github(payload, signature, secret)
    expected_signature = GITHUB_SIGNATURE_PREFIX + 
      OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
    secure_compare(signature, expected_signature)
  end
  
  def self.verify_stripe(payload, signature_header, secret)
    # Parse Stripe's signature format: t=timestamp,v1=signature
    elements = signature_header.split(',').map { |pair| pair.split('=', 2) }.to_h
    timestamp = elements['t']
    signature = elements['v1']
    
    return false unless timestamp && signature
    
    # Stripe signs: timestamp.payload
    signed_payload = "#{timestamp}.#{payload}"
    expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
    
    secure_compare(signature, expected_signature)
  end
  
  def self.verify_generic(payload, signature, secret, algorithm = 'SHA256')
    expected_signature = OpenSSL::HMAC.hexdigest(algorithm, secret, payload)
    secure_compare(signature, expected_signature)
  end
  
  private
  
  def self.secure_compare(a, b)
    Rack::Utils.secure_compare(a, b) if defined?(Rack::Utils)
    # Fallback implementation for environments without Rack
    return false unless a.bytesize == b.bytesize
    OpenSSL.fixed_length_secure_compare(a, b)
  end
end

# Rails webhook controller
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def github
    signature = request.headers['X-Hub-Signature-256']
    payload = request.raw_post
    
    if WebhookVerifier.verify_github(payload, signature, ENV['GITHUB_WEBHOOK_SECRET'])
      process_github_webhook(JSON.parse(payload))
    else
      head :unauthorized
    end
  end
  
  def stripe
    signature = request.headers['Stripe-Signature']
    payload = request.raw_post
    
    if WebhookVerifier.verify_stripe(payload, signature, ENV['STRIPE_WEBHOOK_SECRET'])
      process_stripe_webhook(JSON.parse(payload))
    else
      head :unauthorized
    end
  end
end

Session token generation uses HMAC to create tamper-evident tokens containing user data, expiration times, and security flags. This pattern enables stateless session management while maintaining security guarantees.

class SecureTokenManager
  TOKEN_SEPARATOR = '.'
  
  def initialize(secret_key, algorithm = 'SHA256')
    @secret_key = secret_key
    @algorithm = algorithm
  end
  
  def generate_token(payload, expires_at = nil)
    # Add timestamp and expiration
    token_data = {
      payload: payload,
      issued_at: Time.now.to_i,
      expires_at: expires_at&.to_i
    }
    
    # Encode payload as base64
    encoded_payload = Base64.urlsafe_encode64(JSON.generate(token_data))
    
    # Generate HMAC signature
    signature = OpenSSL::HMAC.hexdigest(@algorithm, @secret_key, encoded_payload)
    
    # Combine payload and signature
    "#{encoded_payload}#{TOKEN_SEPARATOR}#{signature}"
  end
  
  def verify_token(token)
    parts = token.split(TOKEN_SEPARATOR, 2)
    return nil unless parts.length == 2
    
    encoded_payload, provided_signature = parts
    
    # Verify signature
    expected_signature = OpenSSL::HMAC.hexdigest(@algorithm, @secret_key, encoded_payload)
    return nil unless secure_compare(provided_signature, expected_signature)
    
    # Decode and parse payload
    begin
      decoded_payload = Base64.urlsafe_decode64(encoded_payload)
      token_data = JSON.parse(decoded_payload, symbolize_names: true)
    rescue JSON::ParserError, ArgumentError
      return nil
    end
    
    # Check expiration
    if token_data[:expires_at] && Time.now.to_i > token_data[:expires_at]
      return nil
    end
    
    token_data[:payload]
  end
  
  private
  
  def secure_compare(a, b)
    return false unless a.length == b.length
    result = 0
    a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
    result == 0
  end
end

# Usage in application
token_manager = SecureTokenManager.new(ENV['SESSION_SECRET'])

# Generate token with 1 hour expiration
user_token = token_manager.generate_token(
  { user_id: 123, role: 'admin' },
  Time.now + 3600
)

# Verify token in middleware
user_data = token_manager.verify_token(user_token)
if user_data
  # Token valid, proceed with user data
  puts "Authenticated user: #{user_data[:user_id]}"
else
  # Token invalid or expired
  puts "Authentication failed"
end

Common Pitfalls

HMAC implementations often contain subtle security vulnerabilities and logic errors that compromise authentication effectiveness. These issues typically involve timing attacks, weak key management, encoding problems, or incorrect signature verification logic.

Timing attacks exploit variations in comparison time to gradually discover valid signatures. Standard string comparison methods return immediately upon finding the first different character, leaking information about signature correctness through execution time measurement.

# VULNERABLE: Timing attack susceptible comparison
def vulnerable_verify(expected_hmac, provided_hmac)
  expected_hmac == provided_hmac  # Returns immediately on first difference
end

# SECURE: Constant-time comparison
def secure_verify(expected_hmac, provided_hmac)
  return false unless expected_hmac.length == provided_hmac.length
  
  result = 0
  expected_hmac.bytes.zip(provided_hmac.bytes) do |a, b|
    result |= a ^ b
  end
  result == 0
end

# Demonstration of timing difference
require 'benchmark'

correct_hmac = 'a' * 64
almost_correct = 'b' + 'a' * 63  # First character wrong
completely_wrong = 'b' * 64      # All characters wrong

# Time vulnerable comparison
time_almost = Benchmark.realtime { 1000.times { vulnerable_verify(correct_hmac, almost_correct) } }
time_completely = Benchmark.realtime { 1000.times { vulnerable_verify(correct_hmac, completely_wrong) } }

puts "Almost correct: #{time_almost}s"
puts "Completely wrong: #{time_completely}s"
# Shows timing difference revealing signature structure

Key management errors include using weak keys, hardcoded secrets, or predictable key generation. Applications must use cryptographically strong random keys and implement secure key rotation procedures.

# BAD: Weak and predictable keys
weak_key = 'password123'
hardcoded_key = 'my_secret_key_do_not_change'
predictable_key = "#{Time.now.to_i}"  # Timestamp-based

# GOOD: Strong random key generation
require 'securerandom'

strong_key = SecureRandom.hex(32)  # 64-character hex string
binary_key = SecureRandom.bytes(32)  # 32 random bytes
uuid_key = SecureRandom.uuid  # UUID format

# Key rotation implementation
class RotatingKeyManager
  def initialize(current_key, previous_key = nil)
    @current_key = current_key
    @previous_key = previous_key
  end
  
  def sign(message)
    OpenSSL::HMAC.hexdigest('SHA256', @current_key, message)
  end
  
  def verify(message, signature)
    # Try current key first
    current_signature = OpenSSL::HMAC.hexdigest('SHA256', @current_key, message)
    return true if secure_compare(signature, current_signature)
    
    # Fall back to previous key during rotation period
    if @previous_key
      previous_signature = OpenSSL::HMAC.hexdigest('SHA256', @previous_key, message)
      return true if secure_compare(signature, previous_signature)
    end
    
    false
  end
  
  def rotate_key(new_key)
    @previous_key = @current_key
    @current_key = new_key
  end
  
  private
  
  def secure_compare(a, b)
    return false unless a.length == b.length
    result = 0
    a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
    result == 0
  end
end

Encoding inconsistencies create authentication failures when different parts of an application handle string encoding differently. This problem frequently occurs in applications processing international text, binary data, or data from multiple sources.

# Problematic encoding handling
def problematic_hmac_generation
  user_input = "café"  # UTF-8 encoded
  file_data = File.read('binary_file.dat', encoding: 'ASCII-8BIT')
  
  # These produce different HMACs due to encoding differences
  hmac1 = OpenSSL::HMAC.hexdigest('SHA256', 'key', user_input)
  hmac2 = OpenSSL::HMAC.hexdigest('SHA256', 'key', user_input.encode('ISO-8859-1'))
  
  puts hmac1 == hmac2  # => false
end

# Consistent encoding approach
class EncodingConsistentHMAC
  def initialize(key, algorithm = 'SHA256', encoding = 'UTF-8')
    @key = key.encode(encoding)
    @algorithm = algorithm
    @encoding = encoding
  end
  
  def compute(message)
    normalized_message = case message.encoding
    when Encoding::ASCII_8BIT
      # Handle binary data without conversion
      message
    else
      # Convert text to consistent encoding
      message.encode(@encoding)
    end
    
    OpenSSL::HMAC.hexdigest(@algorithm, @key, normalized_message)
  rescue Encoding::UndefinedConversionError
    # Fall back to binary handling for problematic characters
    binary_message = message.force_encoding('ASCII-8BIT')
    OpenSSL::HMAC.hexdigest(@algorithm, @key, binary_message)
  end
end

# Usage with consistent encoding
hmac_generator = EncodingConsistentHMAC.new('secret_key')
result1 = hmac_generator.compute("café")
result2 = hmac_generator.compute("café".encode('UTF-8'))
puts result1 == result2  # => true

Replay attack vulnerabilities occur when applications accept the same HMAC signature multiple times without implementing proper nonce or timestamp validation. Attackers can capture and replay valid requests to perform unauthorized actions.

# VULNERABLE: No replay protection
class VulnerableAPIEndpoint
  def process_request(payload, signature)
    expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
    
    if secure_compare(signature, expected_signature)
      # Process request - vulnerable to replay attacks
      execute_action(payload)
    else
      reject_request
    end
  end
end

# SECURE: Replay protection with timestamps and nonces
class ReplayProtectedEndpoint
  TIMESTAMP_TOLERANCE = 300  # 5 minutes
  
  def initialize
    @used_nonces = Set.new
    @nonce_timestamps = {}
    cleanup_old_nonces
  end
  
  def process_request(payload, signature, timestamp, nonce)
    # Validate timestamp freshness
    current_time = Time.now.to_i
    request_time = timestamp.to_i
    
    if (current_time - request_time).abs > TIMESTAMP_TOLERANCE
      return reject_request("Request too old or from future")
    end
    
    # Check nonce uniqueness
    if @used_nonces.include?(nonce)
      return reject_request("Nonce already used")
    end
    
    # Construct signed message including timestamp and nonce
    signed_data = "#{timestamp}:#{nonce}:#{payload}"
    expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, signed_data)
    
    if secure_compare(signature, expected_signature)
      @used_nonces.add(nonce)
      @nonce_timestamps[nonce] = current_time
      execute_action(payload)
    else
      reject_request("Invalid signature")
    end
  end
  
  private
  
  def cleanup_old_nonces
    # Periodically remove old nonces to prevent memory growth
    Thread.new do
      loop do
        sleep 300  # Clean up every 5 minutes
        cutoff_time = Time.now.to_i - TIMESTAMP_TOLERANCE
        
        @nonce_timestamps.each do |nonce, timestamp|
          if timestamp < cutoff_time
            @used_nonces.delete(nonce)
            @nonce_timestamps.delete(nonce)
          end
        end
      end
    end
  end
  
  def secure_compare(a, b)
    return false unless a.length == b.length
    result = 0
    a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
    result == 0
  end
end

Algorithm downgrade attacks exploit applications that accept multiple HMAC algorithms without proper validation. Attackers can force the use of weaker algorithms or exploit implementation differences between algorithms.

# VULNERABLE: Accepts any algorithm from user input
def vulnerable_verify(algorithm, key, message, signature)
  expected = OpenSSL::HMAC.hexdigest(algorithm, key, message)
  expected == signature  # Also vulnerable to timing attacks
end

# SECURE: Algorithm whitelist and validation
class SecureHMACValidator
  ALLOWED_ALGORITHMS = %w[SHA256 SHA384 SHA512].freeze
  MINIMUM_KEY_LENGTH = 32
  
  def initialize(allowed_algorithms = ALLOWED_ALGORITHMS)
    @allowed_algorithms = allowed_algorithms
  end
  
  def validate_and_compute(algorithm, key, message)
    # Validate algorithm
    unless @allowed_algorithms.include?(algorithm)
      raise ArgumentError, "Algorithm #{algorithm} not allowed. Use: #{@allowed_algorithms.join(', ')}"
    end
    
    # Validate key strength
    if key.length < MINIMUM_KEY_LENGTH
      raise ArgumentError, "Key too short. Minimum length: #{MINIMUM_KEY_LENGTH} characters"
    end
    
    # Compute HMAC with validated inputs
    OpenSSL::HMAC.hexdigest(algorithm, key, message)
  rescue RuntimeError => e
    raise ArgumentError, "Invalid algorithm or OpenSSL error: #{e.message}"
  end
  
  def verify(algorithm, key, message, provided_signature)
    expected_signature = validate_and_compute(algorithm, key, message)
    secure_compare(expected_signature, provided_signature)
  end
  
  private
  
  def secure_compare(a, b)
    return false unless a.length == b.length
    result = 0
    a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
    result == 0
  end
end

# Usage with algorithm restrictions
validator = SecureHMACValidator.new(['SHA256', 'SHA512'])

begin
  # This will be rejected
  validator.verify('MD5', strong_key, message, signature)
rescue ArgumentError => e
  puts "Algorithm rejected: #{e.message}"
  # => "Algorithm MD5 not allowed. Use: SHA256, SHA512"
end

Reference

Class Methods

Method Parameters Returns Description
OpenSSL::HMAC.hexdigest(algorithm, key, data) algorithm (String), key (String), data (String) String Computes HMAC and returns hexadecimal string
OpenSSL::HMAC.digest(algorithm, key, data) algorithm (String), key (String), data (String) String Computes HMAC and returns binary string
OpenSSL::HMAC.base64digest(algorithm, key, data) algorithm (String), key (String), data (String) String Computes HMAC and returns Base64-encoded string

Instance Methods

Method Parameters Returns Description
OpenSSL::HMAC.new(key, algorithm) key (String), algorithm (String) OpenSSL::HMAC Creates new HMAC instance
#update(data) data (String) self Adds data to HMAC computation
#<<(data) data (String) self Alias for update method
#hexdigest None String Returns hexadecimal digest and finalizes
#digest None String Returns binary digest and finalizes
#base64digest None String Returns Base64 digest and finalizes
#reset None self Resets HMAC state to initial condition

Supported Hash Algorithms

Algorithm Output Size (bytes) Security Status Recommended Use
SHA256 32 Strong General purpose, API authentication
SHA384 48 Strong High security applications
SHA512 64 Strong Maximum security requirements
SHA224 28 Adequate Legacy compatibility
SHA1 20 Weak Deprecated, avoid for new applications
MD5 16 Broken Legacy only, not recommended

Common Digest Output Formats

Format Method Example Output Use Case
Hexadecimal hexdigest a1b2c3d4e5f6... Web APIs, debugging, storage
Binary digest \xA1\xB2\xC3\xD4... Network protocols, space efficiency
Base64 base64digest obLD1OX2... HTTP headers, JSON payloads

Error Types and Causes

Exception Common Causes Prevention Strategy
RuntimeError Invalid algorithm name, OpenSSL errors Validate algorithms against whitelist
ArgumentError Nil parameters, wrong types Input validation and type checking
Encoding::UndefinedConversionError Incompatible character encodings Normalize encoding or handle binary data
NoMemoryError Extremely large data processing Implement streaming for large datasets

Key Length Recommendations

Algorithm Minimum Key Length Recommended Length Maximum Effective Length
SHA256 32 bytes 32 bytes 64 bytes (block size)
SHA384 48 bytes 48 bytes 128 bytes (block size)
SHA512 64 bytes 64 bytes 128 bytes (block size)
SHA1 20 bytes Not recommended 64 bytes (block size)

Performance Characteristics

Operation Type Performance Order Memory Usage Streaming Support
Class methods Fastest Low No
Instance methods Moderate Low to moderate Yes
Large data (>1MB) Use streaming Constant with streaming Yes
Multiple operations Instance reuse Higher with reuse Yes