482 lines
15 KiB
Elixir
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
|