defmodule SnmpKit.SnmpLib.Security.Auth do @moduledoc """ Authentication protocols for SNMPv3 User Security Model. Implements HMAC-based authentication protocols as specified in RFC 3414 and RFC 7860, providing message integrity and authentication for SNMPv3 communications. ## Supported Protocols - **HMAC-MD5** (RFC 3414) - 16-byte digest, legacy support - **HMAC-SHA-1** (RFC 3414) - 20-byte digest, legacy support - **HMAC-SHA-224** (RFC 7860) - 28-byte digest - **HMAC-SHA-256** (RFC 7860) - 32-byte digest, recommended - **HMAC-SHA-384** (RFC 7860) - 48-byte digest - **HMAC-SHA-512** (RFC 7860) - 64-byte digest, highest security ## Security Considerations - MD5 and SHA-1 are deprecated for new implementations - SHA-256 or higher is recommended for production use - Authentication keys must be properly derived using key derivation functions - Truncated MACs maintain security properties when properly implemented ## Protocol Selection Guidelines - **SHA-256**: Recommended for most deployments (good security/performance balance) - **SHA-512**: High security environments with adequate processing power - **SHA-384**: Alternative to SHA-512 with smaller digest size - **MD5/SHA-1**: Legacy compatibility only, not recommended for new deployments ## Usage Examples ### Message Authentication # Authenticate outgoing message auth_key = derived_authentication_key message = snmp_message_data {:ok, auth_params} = SnmpKit.SnmpLib.Security.Auth.authenticate(:sha256, auth_key, message) # Verify incoming message :ok = SnmpKit.SnmpLib.Security.Auth.verify(:sha256, auth_key, message, auth_params) ### Protocol Capabilities # Get protocol information info = SnmpKit.SnmpLib.Security.Auth.protocol_info(:sha256) # Returns: %{digest_size: 32, truncated_size: 12, secure: true, ...} # List all supported protocols protocols = SnmpKit.SnmpLib.Security.Auth.supported_protocols() """ require Logger @type auth_protocol :: :none | :md5 | :sha1 | :sha224 | :sha256 | :sha384 | :sha512 @type auth_key :: binary() @type auth_params :: binary() @type message_data :: binary() # Protocol specifications per RFC 3414 and RFC 7860 @protocol_specs %{ none: %{ algorithm: :none, digest_size: 0, truncated_size: 0, max_key_size: 0, secure: false, rfc: "N/A" }, md5: %{ algorithm: :md5, digest_size: 16, truncated_size: 12, max_key_size: 64, # Deprecated secure: false, rfc: "RFC 3414" }, sha1: %{ algorithm: :sha, digest_size: 20, truncated_size: 12, max_key_size: 64, # Deprecated secure: false, rfc: "RFC 3414" }, sha224: %{ algorithm: :sha224, digest_size: 28, truncated_size: 16, max_key_size: 64, secure: true, rfc: "RFC 7860" }, sha256: %{ algorithm: :sha256, digest_size: 32, truncated_size: 16, max_key_size: 64, secure: true, rfc: "RFC 7860" }, sha384: %{ algorithm: :sha384, digest_size: 48, truncated_size: 24, max_key_size: 64, secure: true, rfc: "RFC 7860" }, sha512: %{ algorithm: :sha512, digest_size: 64, truncated_size: 32, max_key_size: 64, secure: true, rfc: "RFC 7860" } } ## Protocol Information @doc """ Returns information about a specific authentication protocol. ## Examples iex> SnmpKit.SnmpLib.Security.Auth.protocol_info(:sha256) %{algorithm: :sha256, digest_size: 32, truncated_size: 16, secure: true, rfc: "RFC 7860"} iex> SnmpKit.SnmpLib.Security.Auth.protocol_info(:md5) %{algorithm: :md5, digest_size: 16, truncated_size: 12, secure: false, rfc: "RFC 3414"} """ @spec protocol_info(auth_protocol()) :: map() | nil def protocol_info(protocol) do Map.get(@protocol_specs, protocol) end @doc """ Returns list of all supported authentication protocols. ## Examples iex> SnmpKit.SnmpLib.Security.Auth.supported_protocols() [:none, :md5, :sha1, :sha224, :sha256, :sha384, :sha512] """ @spec supported_protocols() :: [auth_protocol()] def supported_protocols do Map.keys(@protocol_specs) end @doc """ Returns list of cryptographically secure protocols (excludes deprecated ones). ## Examples iex> SnmpKit.SnmpLib.Security.Auth.secure_protocols() [:sha224, :sha256, :sha384, :sha512] """ @spec secure_protocols() :: [auth_protocol()] def secure_protocols do @protocol_specs |> Enum.filter(fn {_protocol, spec} -> spec.secure end) |> Enum.map(fn {protocol, _spec} -> protocol end) end @doc """ Checks if a protocol is considered cryptographically secure. ## Examples iex> SnmpKit.SnmpLib.Security.Auth.secure_protocol?(:sha256) true iex> SnmpKit.SnmpLib.Security.Auth.secure_protocol?(:md5) false """ @spec secure_protocol?(auth_protocol()) :: boolean() def secure_protocol?(protocol) do case protocol_info(protocol) do %{secure: secure} -> secure nil -> false end end ## Authentication Operations @doc """ Authenticates a message using the specified protocol and key. Generates authentication parameters (truncated HMAC) for inclusion in the SNMPv3 message security parameters. ## Parameters - `protocol`: Authentication protocol to use - `auth_key`: Localized authentication key (derived from password) - `message`: Complete message data to authenticate ## Returns - `{:ok, auth_params}`: Authentication parameters for message - `{:error, reason}`: Authentication failed ## Examples # SHA-256 authentication (recommended) {:ok, auth_params} = SnmpKit.SnmpLib.Security.Auth.authenticate(:sha256, auth_key, message) # Legacy MD5 authentication {:ok, auth_params} = SnmpKit.SnmpLib.Security.Auth.authenticate(:md5, auth_key, message) """ @spec authenticate(auth_protocol(), auth_key(), message_data()) :: {:ok, auth_params()} | {:error, atom()} def authenticate(:none, _auth_key, _message) do {:ok, <<>>} end def authenticate(protocol, auth_key, message) when is_atom(protocol) and is_binary(auth_key) do case protocol_info(protocol) do nil -> Logger.error("Unsupported authentication protocol: #{protocol}") {:error, :unsupported_protocol} _spec when byte_size(auth_key) == 0 -> Logger.error("Empty authentication key for protocol: #{protocol}") {:error, :empty_auth_key} spec -> try do # Generate HMAC digest full_digest = :crypto.mac(:hmac, spec.algorithm, auth_key, message) # Truncate to protocol-specified length auth_params = binary_part(full_digest, 0, spec.truncated_size) Logger.debug("Authentication successful with #{protocol}, digest size: #{byte_size(auth_params)}") {:ok, auth_params} rescue error -> Logger.error("Authentication failed for #{protocol}: #{inspect(error)}") {:error, :authentication_failed} end end end def authenticate(protocol, auth_key, _message) when is_atom(protocol) do Logger.error("Invalid authentication key type: #{inspect(auth_key)}") {:error, :invalid_key_type} end def authenticate(protocol, _auth_key, _message) do Logger.error("Invalid authentication protocol type: #{inspect(protocol)}") {:error, :invalid_protocol_type} end @doc """ Verifies message authentication using provided authentication parameters. Recomputes the expected authentication parameters and compares them with the provided parameters using constant-time comparison. ## Parameters - `protocol`: Authentication protocol used - `auth_key`: Localized authentication key - `message`: Message data that was authenticated - `provided_params`: Authentication parameters from received message ## Returns - `:ok`: Authentication verification successful - `{:error, reason}`: Verification failed ## Examples # Verify SHA-256 authentication :ok = SnmpKit.SnmpLib.Security.Auth.verify(:sha256, auth_key, message, auth_params) # Failed verification {:error, :authentication_mismatch} = SnmpKit.SnmpLib.Security.Auth.verify(:md5, wrong_key, message, auth_params) """ @spec verify(auth_protocol(), auth_key(), message_data(), auth_params()) :: :ok | {:error, atom()} def verify(:none, _auth_key, _message, _provided_params) do :ok end def verify(protocol, auth_key, message, provided_params) when is_atom(protocol) do case authenticate(protocol, auth_key, message) do {:ok, expected_params} -> if secure_compare(expected_params, provided_params) do Logger.debug("Authentication verification successful for #{protocol}") :ok else Logger.warning("Authentication mismatch for #{protocol}") {:error, :authentication_mismatch} end {:error, reason} -> Logger.error("Authentication verification failed for #{protocol}: #{reason}") {:error, reason} end end def verify(protocol, _auth_key, _message, _provided_params) do Logger.error("Invalid authentication protocol type: #{inspect(protocol)}") {:error, :invalid_protocol_type} end ## Key Validation @doc """ Validates that an authentication key is appropriate for the specified protocol. Checks key length requirements and provides warnings for weak protocols. ## Examples :ok = SnmpKit.SnmpLib.Security.Auth.validate_key(:sha256, auth_key) {:error, :key_too_short} = SnmpKit.SnmpLib.Security.Auth.validate_key(:sha512, short_key) """ @spec validate_key(auth_protocol(), auth_key()) :: :ok | {:error, atom()} def validate_key(:none, _key) do :ok end def validate_key(protocol, key) when is_atom(protocol) and is_binary(key) do case protocol_info(protocol) do nil -> {:error, :unsupported_protocol} spec -> key_length = byte_size(key) min_length = spec.digest_size cond do key_length == 0 -> {:error, :empty_key} key_length < min_length -> Logger.warning("Authentication key shorter than recommended for #{protocol}: #{key_length} < #{min_length}") {:error, :key_too_short} key_length > spec.max_key_size -> Logger.error("Authentication key too long for #{protocol}: #{key_length} > #{spec.max_key_size}") {:error, :invalid_key_length} not spec.secure -> Logger.warning("Using deprecated authentication protocol: #{protocol}") :ok true -> :ok end end end def validate_key(_protocol, _key) do {:error, :invalid_parameters} end ## Batch Operations @doc """ Authenticates multiple messages using the same protocol and key. More efficient than individual authentication calls when processing multiple messages with the same authentication configuration. ## Examples messages = [msg1, msg2, msg3] {:ok, auth_params_list} = SnmpKit.SnmpLib.Security.Auth.authenticate_batch(:sha256, auth_key, messages) """ @spec authenticate_batch(auth_protocol(), auth_key(), [message_data()]) :: {:ok, [auth_params()]} | {:error, atom()} def authenticate_batch(protocol, auth_key, messages) when is_list(messages) do case protocol_info(protocol) do nil -> {:error, :unsupported_protocol} _spec -> try do auth_params_list = Enum.map(messages, fn message -> {:ok, params} = authenticate(protocol, auth_key, message) params end) {:ok, auth_params_list} rescue _error -> {:error, :batch_authentication_failed} end end end @doc """ Verifies authentication for multiple messages in batch. ## Examples results = SnmpKit.SnmpLib.Security.Auth.verify_batch(:sha256, auth_key, messages, auth_params_list) # Returns: [:ok, :ok, {:error, :authentication_mismatch}] """ @spec verify_batch(auth_protocol(), auth_key(), [message_data()], [auth_params()]) :: [:ok | {:error, atom()}] def verify_batch(protocol, auth_key, messages, auth_params_list) when is_list(messages) and is_list(auth_params_list) do if length(messages) == length(auth_params_list) do messages |> Enum.zip(auth_params_list) |> Enum.map(fn {message, auth_params} -> verify(protocol, auth_key, message, auth_params) end) else List.duplicate({:error, :parameter_length_mismatch}, length(messages)) end end ## Performance and Statistics @doc """ Measures authentication performance for a given protocol. Useful for performance tuning and protocol selection in high-throughput environments. ## Examples stats = SnmpKit.SnmpLib.Security.Auth.benchmark_protocol(:sha256, test_key, test_message, 1000) # Returns timing and throughput statistics """ @spec benchmark_protocol(auth_protocol(), auth_key(), message_data(), pos_integer()) :: map() def benchmark_protocol(protocol, auth_key, test_message, iterations \\ 1000) do Logger.info("Benchmarking #{protocol} authentication with #{iterations} iterations") # Warm up authenticate(protocol, auth_key, test_message) # Time authentication operations {auth_time, _} = :timer.tc(fn -> Enum.each(1..iterations, fn _i -> authenticate(protocol, auth_key, test_message) end) end) # Time verification operations {:ok, auth_params} = authenticate(protocol, auth_key, test_message) {verify_time, _} = :timer.tc(fn -> Enum.each(1..iterations, fn _i -> verify(protocol, auth_key, test_message, auth_params) end) end) %{ protocol: protocol, iterations: iterations, auth_time_microseconds: auth_time, verify_time_microseconds: verify_time, auth_ops_per_second: round(iterations / (auth_time / 1_000_000)), verify_ops_per_second: round(iterations / (verify_time / 1_000_000)), avg_auth_microseconds: round(auth_time / iterations), avg_verify_microseconds: round(verify_time / iterations) } end ## Private Helper Functions # Constant-time comparison to prevent timing attacks defp secure_compare(a, b) when byte_size(a) != byte_size(b) do false end defp secure_compare(a, b) do :crypto.hash_equals(a, b) end end