towerops/lib/snmpkit/snmp_lib/security.ex

482 lines
15 KiB
Elixir

defmodule SnmpKit.SnmpLib.Security do
@moduledoc """
SNMPv3 Security Framework - Phase 5.1A Implementation
Provides comprehensive SNMPv3 User Security Model (USM) implementation including
authentication and privacy protocols for secure SNMP communications.
## Features
- **User Security Model (USM)** with complete RFC 3414 compliance
- **Authentication Protocols**: MD5, SHA-1, SHA-256, SHA-384, SHA-512
- **Privacy Protocols**: DES, AES-128, AES-192, AES-256
- **Key Derivation**: Password-based and localized key generation
- **Security Parameters**: Boot counter, time synchronization, message validation
- **Error Handling**: Comprehensive security error classification and recovery
## Architecture
The security framework is built on a modular architecture:
- `SnmpKit.SnmpLib.Security.USM` - User Security Model implementation
- `SnmpKit.SnmpLib.Security.Auth` - Authentication protocol handlers
- `SnmpKit.SnmpLib.Security.Priv` - Privacy protocol handlers
- `SnmpKit.SnmpLib.Security.Keys` - Key derivation and management
## Usage Examples
### Basic SNMPv3 Authentication
# Create authenticated user
{:ok, user} = SnmpKit.SnmpLib.Security.create_user("admin",
auth_protocol: :sha256,
auth_password: "secure_password",
engine_id: "engine123"
)
# Authenticate message
{:ok, auth_params} = SnmpKit.SnmpLib.Security.authenticate_message(user, message)
### Privacy (Encryption) Support
# Create user with privacy
{:ok, user} = SnmpKit.SnmpLib.Security.create_user("secure_admin",
auth_protocol: :sha256,
auth_password: "auth_password",
priv_protocol: :aes256,
priv_password: "priv_password"
)
# Encrypt message
{:ok, encrypted} = SnmpKit.SnmpLib.Security.encrypt_message(user, message)
### Engine ID Management
# Generate engine ID
engine_id = SnmpKit.SnmpLib.Security.generate_engine_id("192.168.1.1")
# Discover remote engine
{:ok, remote_engine} = SnmpKit.SnmpLib.Security.discover_engine("10.0.0.1")
## Security Considerations
- All key material is stored securely in memory
- Authentication and privacy keys are derived using RFC-compliant algorithms
- Time-based authentication prevents replay attacks
- Boot counter management ensures message freshness
- Comprehensive input validation prevents security bypasses
"""
alias SnmpKit.SnmpLib.Security.Auth
alias SnmpKit.SnmpLib.Security.Keys
alias SnmpKit.SnmpLib.Security.Priv
alias SnmpKit.SnmpLib.Security.USM
@type auth_protocol :: :none | :md5 | :sha1 | :sha256 | :sha384 | :sha512
@type priv_protocol :: :none | :des | :aes128 | :aes192 | :aes256
@type engine_id :: binary()
@type security_name :: binary()
@type security_level :: :no_auth_no_priv | :auth_no_priv | :auth_priv
@type user_config :: [
auth_protocol: auth_protocol(),
auth_password: binary(),
priv_protocol: priv_protocol(),
priv_password: binary(),
engine_id: engine_id()
]
@type security_user :: %{
security_name: security_name(),
auth_protocol: auth_protocol(),
priv_protocol: priv_protocol(),
auth_key: binary(),
priv_key: binary(),
engine_id: engine_id(),
engine_boots: non_neg_integer(),
engine_time: non_neg_integer()
}
@type security_params :: %{
authoritative_engine_id: engine_id(),
authoritative_engine_boots: non_neg_integer(),
authoritative_engine_time: non_neg_integer(),
user_name: security_name(),
authentication_parameters: binary(),
privacy_parameters: binary()
}
## User Management
@doc """
Creates a new SNMPv3 security user with specified authentication and privacy settings.
## Parameters
- `security_name`: Unique identifier for the user
- `config`: User configuration including protocols and passwords
## Returns
- `{:ok, user}`: Successfully created security user
- `{:error, reason}`: Creation failed
## Examples
# Authentication only user
{:ok, user} = SnmpKit.SnmpLib.Security.create_user("monitor_user",
auth_protocol: :sha256,
auth_password: "monitoring_secret",
engine_id: "local_engine"
)
# Full authentication and privacy user
{:ok, admin} = SnmpKit.SnmpLib.Security.create_user("admin_user",
auth_protocol: :sha512,
auth_password: "admin_auth_pass",
priv_protocol: :aes256,
priv_password: "admin_priv_pass",
engine_id: "management_engine"
)
"""
@spec create_user(security_name(), user_config()) :: {:ok, security_user()} | {:error, atom()}
def create_user(security_name, config) do
with {:ok, validated_config} <- validate_user_config(config),
{:ok, auth_key} <- derive_auth_key(validated_config),
{:ok, priv_key} <- derive_priv_key(validated_config) do
user = %{
security_name: security_name,
auth_protocol: validated_config[:auth_protocol] || :none,
priv_protocol: validated_config[:priv_protocol] || :none,
auth_key: auth_key,
priv_key: priv_key,
engine_id: validated_config[:engine_id],
engine_boots: 1,
engine_time: System.system_time(:second)
}
{:ok, user}
end
end
@doc """
Updates security user credentials and regenerates keys.
"""
@spec update_user(security_user(), user_config()) :: {:ok, security_user()} | {:error, atom()}
def update_user(user, new_config) do
updated_config = Map.merge(user_to_config(user), Map.new(new_config))
create_user(user.security_name, Map.to_list(updated_config))
end
@doc """
Validates user credentials against stored authentication data.
"""
@spec validate_user(security_user(), binary(), binary()) :: :ok | {:error, atom()}
def validate_user(user, auth_password, priv_password \\ "") do
with :ok <- validate_auth_password(user, auth_password) do
validate_priv_password(user, priv_password)
end
end
## Message Security
@doc """
Determines the security level for a message based on user configuration.
## Security Levels
- `:no_auth_no_priv` - No authentication, no privacy
- `:auth_no_priv` - Authentication only
- `:auth_priv` - Authentication and privacy
"""
@spec get_security_level(security_user()) :: security_level()
def get_security_level(user) do
case {user.auth_protocol, user.priv_protocol} do
{:none, :none} -> :no_auth_no_priv
{auth, :none} when auth != :none -> :auth_no_priv
{auth, priv} when auth != :none and priv != :none -> :auth_priv
_ -> :no_auth_no_priv
end
end
@doc """
Authenticates an SNMP message using the user's authentication protocol.
Returns authentication parameters that should be included in the message.
"""
@spec authenticate_message(security_user(), binary()) :: {:ok, binary()} | {:error, atom()}
def authenticate_message(user, message) do
case user.auth_protocol do
:none -> {:ok, <<>>}
protocol -> Auth.authenticate(protocol, user.auth_key, message)
end
end
@doc """
Verifies message authentication using provided authentication parameters.
"""
@spec verify_authentication(security_user(), binary(), binary()) :: :ok | {:error, atom()}
def verify_authentication(user, message, auth_params) do
case user.auth_protocol do
:none -> :ok
protocol -> Auth.verify(protocol, user.auth_key, message, auth_params)
end
end
@doc """
Encrypts message data using the user's privacy protocol.
"""
@spec encrypt_message(security_user(), binary()) ::
{:ok, {binary(), binary()}} | {:error, atom()}
def encrypt_message(user, plaintext) do
case user.priv_protocol do
:none -> {:ok, {plaintext, <<>>}}
protocol -> Priv.encrypt(protocol, user.priv_key, user.auth_key, plaintext)
end
end
@doc """
Decrypts message data using the user's privacy protocol.
"""
@spec decrypt_message(security_user(), binary(), binary()) :: {:ok, binary()} | {:error, atom()}
def decrypt_message(user, ciphertext, priv_params) do
case user.priv_protocol do
:none -> {:ok, ciphertext}
protocol -> Priv.decrypt(protocol, user.priv_key, user.auth_key, ciphertext, priv_params)
end
end
## Engine Management
@doc """
Generates a unique engine ID for an SNMP entity.
Engine IDs are used to uniquely identify SNMP engines and are required
for SNMPv3 security operations.
"""
@spec generate_engine_id(binary()) :: engine_id()
def generate_engine_id(_identifier) do
# RFC 3411 compliant engine ID generation
timestamp = System.system_time(:second)
random = :crypto.strong_rand_bytes(4)
# Format: enterprise_id(4) + format(1) + timestamp(4) + random(4)
<<0x00, 0x00, 0x00, 0x01, 0x02>> <>
<<timestamp::32>> <>
random
end
@doc """
Discovers the engine ID of a remote SNMP agent.
This is typically done during the first communication with a remote agent
to establish security context.
"""
@spec discover_engine(binary(), keyword()) :: {:ok, engine_id()} | {:error, atom()}
def discover_engine(host, opts \\ []) do
# Implementation delegates to USM module
USM.discover_engine(host, opts)
end
@doc """
Updates engine time and boot counter for time synchronization.
"""
@spec update_engine_time(security_user(), non_neg_integer(), non_neg_integer()) ::
security_user()
def update_engine_time(user, engine_boots, engine_time) do
%{user | engine_boots: engine_boots, engine_time: engine_time}
end
## Security Parameters
@doc """
Builds security parameters for inclusion in SNMPv3 messages.
"""
@spec build_security_params(security_user(), binary(), binary()) :: security_params()
def build_security_params(user, auth_params \\ <<>>, priv_params \\ <<>>) do
%{
authoritative_engine_id: user.engine_id,
authoritative_engine_boots: user.engine_boots,
authoritative_engine_time: user.engine_time,
user_name: user.security_name,
authentication_parameters: auth_params,
privacy_parameters: priv_params
}
end
@doc """
Validates security parameters from received messages.
"""
@spec validate_security_params(security_user(), security_params()) :: :ok | {:error, atom()}
def validate_security_params(user, params) do
with :ok <- validate_engine_id(user.engine_id, params.authoritative_engine_id),
:ok <- validate_time_window(user, params) do
validate_user_name(user.security_name, params.user_name)
end
end
## Configuration and Status
@doc """
Returns comprehensive information about security capabilities and status.
"""
@spec info() :: map()
def info do
%{
version: "5.1.0",
phase: "5.1A - Security Foundation",
supported_auth_protocols: [:md5, :sha1, :sha256, :sha384, :sha512],
supported_priv_protocols: [:des, :aes128, :aes192, :aes256],
rfc_compliance: ["RFC 3411", "RFC 3414", "RFC 3826"],
security_levels: [:no_auth_no_priv, :auth_no_priv, :auth_priv],
features: [
"User Security Model (USM)",
"Multiple authentication protocols",
"Multiple privacy protocols",
"Engine ID management",
"Time synchronization",
"Key derivation (RFC 3414)",
"Security parameter validation"
]
}
end
## Private Implementation
defp validate_user_config(config) do
auth_protocol = config[:auth_protocol] || :none
priv_protocol = config[:priv_protocol] || :none
with :ok <- validate_auth_protocol(auth_protocol),
:ok <- validate_priv_protocol(priv_protocol),
:ok <- validate_protocol_compatibility(auth_protocol, priv_protocol),
:ok <- validate_required_passwords(config, auth_protocol, priv_protocol),
:ok <- validate_engine_id(config) do
{:ok, config}
end
end
defp validate_auth_protocol(protocol) when protocol in [:none, :md5, :sha1, :sha256, :sha384, :sha512], do: :ok
defp validate_auth_protocol(_), do: {:error, :invalid_auth_protocol}
defp validate_priv_protocol(protocol) when protocol in [:none, :des, :aes128, :aes192, :aes256], do: :ok
defp validate_priv_protocol(_), do: {:error, :invalid_priv_protocol}
defp validate_protocol_compatibility(:none, priv) when priv != :none, do: {:error, :priv_requires_auth}
defp validate_protocol_compatibility(_, _), do: :ok
defp validate_required_passwords(config, auth_protocol, priv_protocol) do
cond do
auth_protocol != :none and is_nil(config[:auth_password]) -> {:error, :missing_auth_password}
priv_protocol != :none and is_nil(config[:priv_password]) -> {:error, :missing_priv_password}
true -> :ok
end
end
defp validate_engine_id(config) do
if is_nil(config[:engine_id]), do: {:error, :missing_engine_id}, else: :ok
end
defp derive_auth_key(config) do
case config[:auth_protocol] do
:none ->
{:ok, <<>>}
protocol ->
Keys.derive_auth_key(
protocol,
config[:auth_password],
config[:engine_id]
)
end
end
defp derive_priv_key(config) do
case config[:priv_protocol] do
:none ->
{:ok, <<>>}
nil ->
{:ok, <<>>}
protocol ->
Keys.derive_priv_key(
protocol,
config[:priv_password],
config[:engine_id]
)
end
end
defp user_to_config(user) do
%{
auth_protocol: user.auth_protocol,
priv_protocol: user.priv_protocol,
engine_id: user.engine_id
}
end
defp validate_auth_password(user, password) do
case user.auth_protocol do
:none ->
:ok
protocol ->
expected_key = Keys.derive_auth_key(protocol, password, user.engine_id)
case expected_key do
{:ok, key} when key == user.auth_key -> :ok
_ -> {:error, :invalid_auth_password}
end
end
end
defp validate_priv_password(user, password) do
case user.priv_protocol do
:none ->
:ok
protocol when password != "" ->
expected_key = Keys.derive_priv_key(protocol, password, user.engine_id)
case expected_key do
{:ok, key} when key == user.priv_key -> :ok
_ -> {:error, :invalid_priv_password}
end
_ ->
{:error, :missing_priv_password}
end
end
defp validate_engine_id(local_engine, remote_engine) do
if local_engine == remote_engine do
:ok
else
{:error, :engine_id_mismatch}
end
end
defp validate_time_window(user, params) do
# RFC 3414 time window validation (150 seconds)
current_time = System.system_time(:second)
time_diff = abs(current_time - params.authoritative_engine_time)
boot_diff = abs(user.engine_boots - params.authoritative_engine_boots)
cond do
boot_diff > 1 -> {:error, :engine_boots_mismatch}
boot_diff == 1 and time_diff > 150 -> {:error, :time_window_exceeded}
boot_diff == 0 and time_diff > 150 -> {:error, :time_window_exceeded}
true -> :ok
end
end
defp validate_user_name(local_name, remote_name) do
if local_name == remote_name do
:ok
else
{:error, :user_name_mismatch}
end
end
end