499 lines
16 KiB
Elixir
499 lines
16 KiB
Elixir
defmodule SnmpKit.SnmpLib.SecurityTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
alias SnmpKit.SnmpLib.Security
|
|
|
|
@moduletag :snmpv3
|
|
doctest Security
|
|
|
|
@moduletag :security
|
|
@moduletag :phase5
|
|
|
|
describe "Security.create_user/2" do
|
|
test "creates user with authentication only" do
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
auth_password: "test_password_123",
|
|
engine_id: "test_engine_001"
|
|
]
|
|
|
|
assert {:ok, user} = Security.create_user("test_user", config)
|
|
assert user.security_name == "test_user"
|
|
assert user.auth_protocol == :sha256
|
|
assert user.priv_protocol == :none
|
|
assert byte_size(user.auth_key) > 0
|
|
assert user.priv_key == <<>>
|
|
assert user.engine_id == "test_engine_001"
|
|
end
|
|
|
|
test "creates user with authentication and privacy" do
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_password_456",
|
|
priv_protocol: :aes256,
|
|
priv_password: "priv_password_789",
|
|
engine_id: "secure_engine_002"
|
|
]
|
|
|
|
assert {:ok, user} = Security.create_user("secure_user", config)
|
|
assert user.security_name == "secure_user"
|
|
assert user.auth_protocol == :sha256
|
|
assert user.priv_protocol == :aes256
|
|
assert byte_size(user.auth_key) > 0
|
|
assert byte_size(user.priv_key) > 0
|
|
assert user.engine_id == "secure_engine_002"
|
|
end
|
|
|
|
test "rejects invalid configurations" do
|
|
# Missing auth password when auth protocol specified
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
engine_id: "test_engine"
|
|
]
|
|
|
|
assert {:error, :missing_auth_password} = Security.create_user("user", config)
|
|
|
|
# Privacy without authentication
|
|
config = [
|
|
priv_protocol: :aes128,
|
|
priv_password: "priv_pass",
|
|
engine_id: "test_engine"
|
|
]
|
|
|
|
assert {:error, :priv_requires_auth} = Security.create_user("user", config)
|
|
|
|
# Missing engine ID
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_pass"
|
|
]
|
|
|
|
assert {:error, :missing_engine_id} = Security.create_user("user", config)
|
|
|
|
# Unsupported protocol
|
|
config = [
|
|
auth_protocol: :invalid_proto,
|
|
auth_password: "auth_pass",
|
|
engine_id: "test_engine"
|
|
]
|
|
|
|
assert {:error, :invalid_auth_protocol} = Security.create_user("user", config)
|
|
end
|
|
end
|
|
|
|
describe "Security.get_security_level/1" do
|
|
test "determines correct security levels" do
|
|
# No auth, no priv
|
|
user = %{auth_protocol: :none, priv_protocol: :none}
|
|
assert Security.get_security_level(user) == :no_auth_no_priv
|
|
|
|
# Auth only
|
|
user = %{auth_protocol: :sha256, priv_protocol: :none}
|
|
assert Security.get_security_level(user) == :auth_no_priv
|
|
|
|
# Auth and priv
|
|
user = %{auth_protocol: :sha256, priv_protocol: :aes256}
|
|
assert Security.get_security_level(user) == :auth_priv
|
|
end
|
|
end
|
|
|
|
describe "Security.authenticate_message/2" do
|
|
test "authenticates message with SHA-256" do
|
|
{:ok, user} =
|
|
Security.create_user("auth_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "secure_auth_password",
|
|
engine_id: "auth_engine_123"
|
|
)
|
|
|
|
message = "test SNMP message data"
|
|
|
|
assert {:ok, auth_params} = Security.authenticate_message(user, message)
|
|
assert byte_size(auth_params) > 0
|
|
end
|
|
|
|
test "returns empty auth params for no auth" do
|
|
user = %{auth_protocol: :none}
|
|
message = "test message"
|
|
|
|
assert {:ok, <<>>} = Security.authenticate_message(user, message)
|
|
end
|
|
end
|
|
|
|
describe "Security.verify_authentication/3" do
|
|
test "verifies valid authentication" do
|
|
{:ok, user} =
|
|
Security.create_user("verify_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "verification_password",
|
|
engine_id: "verify_engine_456"
|
|
)
|
|
|
|
message = "message to verify"
|
|
{:ok, auth_params} = Security.authenticate_message(user, message)
|
|
|
|
assert :ok = Security.verify_authentication(user, message, auth_params)
|
|
end
|
|
|
|
test "rejects invalid authentication" do
|
|
{:ok, user} =
|
|
Security.create_user("verify_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "verification_password",
|
|
engine_id: "verify_engine_456"
|
|
)
|
|
|
|
message = "message to verify"
|
|
fake_auth_params = :crypto.strong_rand_bytes(12)
|
|
|
|
assert {:error, :authentication_mismatch} =
|
|
Security.verify_authentication(user, message, fake_auth_params)
|
|
end
|
|
end
|
|
|
|
describe "Security.encrypt_message/2" do
|
|
test "encrypts message with AES-256" do
|
|
{:ok, user} =
|
|
Security.create_user("encrypt_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_for_encryption",
|
|
priv_protocol: :aes256,
|
|
priv_password: "privacy_password_strong",
|
|
engine_id: "encrypt_engine_789"
|
|
)
|
|
|
|
plaintext = "confidential SNMP data that needs encryption"
|
|
|
|
assert {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user, plaintext)
|
|
assert byte_size(ciphertext) > 0
|
|
assert ciphertext != plaintext
|
|
assert byte_size(priv_params) > 0
|
|
end
|
|
|
|
test "returns plaintext for no privacy" do
|
|
user = %{priv_protocol: :none}
|
|
plaintext = "unencrypted message"
|
|
|
|
assert {:ok, {^plaintext, <<>>}} = Security.encrypt_message(user, plaintext)
|
|
end
|
|
end
|
|
|
|
describe "Security.decrypt_message/3" do
|
|
test "decrypts AES-256 encrypted message" do
|
|
{:ok, user} =
|
|
Security.create_user("decrypt_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_for_decryption",
|
|
priv_protocol: :aes256,
|
|
priv_password: "strong_privacy_password",
|
|
engine_id: "decrypt_engine_abc"
|
|
)
|
|
|
|
original_plaintext = "secret data that was encrypted"
|
|
|
|
# Encrypt first
|
|
{:ok, {ciphertext, priv_params}} = Security.encrypt_message(user, original_plaintext)
|
|
|
|
# Then decrypt
|
|
assert {:ok, decrypted_plaintext} = Security.decrypt_message(user, ciphertext, priv_params)
|
|
assert decrypted_plaintext == original_plaintext
|
|
end
|
|
|
|
test "handles decryption with wrong key" do
|
|
# Create two users with different keys
|
|
{:ok, user1} =
|
|
Security.create_user("user1",
|
|
auth_protocol: :sha256,
|
|
auth_password: "password1",
|
|
priv_protocol: :aes256,
|
|
priv_password: "privpass1",
|
|
engine_id: "engine1"
|
|
)
|
|
|
|
{:ok, user2} =
|
|
Security.create_user("user2",
|
|
auth_protocol: :sha256,
|
|
auth_password: "password2",
|
|
priv_protocol: :aes256,
|
|
priv_password: "privpass2",
|
|
engine_id: "engine2"
|
|
)
|
|
|
|
plaintext = "test encryption data"
|
|
|
|
# Encrypt with user1
|
|
{:ok, {ciphertext, priv_params}} = Security.encrypt_message(user1, plaintext)
|
|
|
|
# Try to decrypt with user2 (should fail due to wrong key)
|
|
# The wrong key will typically result in invalid padding after decryption
|
|
result = Security.decrypt_message(user2, ciphertext, priv_params)
|
|
|
|
case result do
|
|
{:error, _reason} ->
|
|
# Most common case - padding validation fails with wrong key
|
|
:ok
|
|
|
|
{:ok, decrypted_data} ->
|
|
# In rare cases decryption might succeed with garbage data
|
|
# Ensure it's different from the original
|
|
assert decrypted_data != plaintext,
|
|
"Decryption with wrong key should produce different data"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Security.generate_engine_id/1" do
|
|
test "generates valid engine IDs" do
|
|
identifier = "test.device.local"
|
|
engine_id = Security.generate_engine_id(identifier)
|
|
|
|
assert byte_size(engine_id) >= 5
|
|
assert byte_size(engine_id) <= 32
|
|
|
|
# Should be deterministic for same identifier
|
|
engine_id2 = Security.generate_engine_id(identifier)
|
|
# Actually includes timestamp, so different
|
|
assert engine_id != engine_id2
|
|
end
|
|
|
|
test "generates different engine IDs for different identifiers" do
|
|
engine_id1 = Security.generate_engine_id("device1")
|
|
engine_id2 = Security.generate_engine_id("device2")
|
|
|
|
assert engine_id1 != engine_id2
|
|
end
|
|
end
|
|
|
|
describe "Security.build_security_params/3" do
|
|
test "builds valid security parameters" do
|
|
{:ok, user} =
|
|
Security.create_user("param_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "param_password",
|
|
engine_id: "param_engine_def"
|
|
)
|
|
|
|
auth_params = :crypto.strong_rand_bytes(16)
|
|
priv_params = :crypto.strong_rand_bytes(16)
|
|
|
|
params = Security.build_security_params(user, auth_params, priv_params)
|
|
|
|
assert params.authoritative_engine_id == user.engine_id
|
|
assert params.user_name == user.security_name
|
|
assert params.authentication_parameters == auth_params
|
|
assert params.privacy_parameters == priv_params
|
|
assert is_integer(params.authoritative_engine_boots)
|
|
assert is_integer(params.authoritative_engine_time)
|
|
end
|
|
end
|
|
|
|
describe "Security.update_engine_time/3" do
|
|
test "updates engine time correctly" do
|
|
{:ok, user} =
|
|
Security.create_user("time_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "time_password",
|
|
engine_id: "time_engine_ghi"
|
|
)
|
|
|
|
new_boots = 5
|
|
new_time = 123_456
|
|
|
|
updated_user = Security.update_engine_time(user, new_boots, new_time)
|
|
|
|
assert updated_user.engine_boots == new_boots
|
|
assert updated_user.engine_time == new_time
|
|
assert updated_user.security_name == user.security_name
|
|
assert updated_user.auth_key == user.auth_key
|
|
end
|
|
end
|
|
|
|
describe "Security.validate_user/3" do
|
|
test "validates correct passwords" do
|
|
auth_password = "correct_auth_password"
|
|
priv_password = "correct_priv_password"
|
|
|
|
{:ok, user} =
|
|
Security.create_user("validate_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: auth_password,
|
|
priv_protocol: :aes256,
|
|
priv_password: priv_password,
|
|
engine_id: "validate_engine_jkl"
|
|
)
|
|
|
|
assert :ok = Security.validate_user(user, auth_password, priv_password)
|
|
end
|
|
|
|
test "rejects incorrect passwords" do
|
|
{:ok, user} =
|
|
Security.create_user("validate_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "correct_auth",
|
|
priv_protocol: :aes256,
|
|
priv_password: "correct_priv",
|
|
engine_id: "validate_engine_mno"
|
|
)
|
|
|
|
assert {:error, :invalid_auth_password} =
|
|
Security.validate_user(user, "wrong_auth", "correct_priv")
|
|
|
|
assert {:error, :invalid_priv_password} =
|
|
Security.validate_user(user, "correct_auth", "wrong_priv")
|
|
end
|
|
end
|
|
|
|
describe "Security.info/0" do
|
|
test "returns comprehensive security information" do
|
|
info = Security.info()
|
|
|
|
assert info.version == "5.1.0"
|
|
assert info.phase == "5.1A - Security Foundation"
|
|
# Check that modern protocols are supported
|
|
assert :sha256 in info.supported_auth_protocols
|
|
assert :aes256 in info.supported_priv_protocols
|
|
assert :auth_priv in info.security_levels
|
|
end
|
|
end
|
|
|
|
describe "integration tests" do
|
|
test "complete SNMPv3 security workflow" do
|
|
# Step 1: Create secure user
|
|
{:ok, user} =
|
|
Security.create_user("workflow_user",
|
|
auth_protocol: :sha256,
|
|
auth_password: "strong_authentication_password",
|
|
priv_protocol: :aes256,
|
|
priv_password: "very_strong_privacy_password",
|
|
engine_id: "workflow_engine_test"
|
|
)
|
|
|
|
# Step 2: Verify security level
|
|
assert Security.get_security_level(user) == :auth_priv
|
|
|
|
# Step 3: Authenticate a message
|
|
test_message = "This is a complete SNMPv3 test message with authentication and privacy"
|
|
{:ok, auth_params} = Security.authenticate_message(user, test_message)
|
|
|
|
# Step 4: Encrypt the message
|
|
{:ok, {encrypted_message, priv_params}} = Security.encrypt_message(user, test_message)
|
|
|
|
# Step 5: Build security parameters
|
|
security_params = Security.build_security_params(user, auth_params, priv_params)
|
|
|
|
# Step 6: Verify authentication (simulate receiving side)
|
|
assert :ok = Security.verify_authentication(user, test_message, auth_params)
|
|
|
|
# Step 7: Decrypt the message (simulate receiving side)
|
|
{:ok, decrypted_message} = Security.decrypt_message(user, encrypted_message, priv_params)
|
|
|
|
# Step 8: Verify complete round-trip
|
|
assert decrypted_message == test_message
|
|
assert security_params.user_name == user.security_name
|
|
assert security_params.authoritative_engine_id == user.engine_id
|
|
end
|
|
|
|
test "supports multiple security levels in same system" do
|
|
# No auth user
|
|
{:ok, no_auth_user} =
|
|
Security.create_user("no_auth",
|
|
auth_protocol: :none,
|
|
engine_id: "multi_engine_test"
|
|
)
|
|
|
|
# Auth only user
|
|
{:ok, auth_user} =
|
|
Security.create_user("auth_only",
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_password",
|
|
engine_id: "multi_engine_test"
|
|
)
|
|
|
|
# Auth + priv user
|
|
{:ok, full_user} =
|
|
Security.create_user("full_security",
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_password",
|
|
priv_protocol: :aes256,
|
|
priv_password: "priv_password",
|
|
engine_id: "multi_engine_test"
|
|
)
|
|
|
|
# Verify different security levels
|
|
assert Security.get_security_level(no_auth_user) == :no_auth_no_priv
|
|
assert Security.get_security_level(auth_user) == :auth_no_priv
|
|
assert Security.get_security_level(full_user) == :auth_priv
|
|
|
|
# All should work with appropriate operations
|
|
message = "test message for all users"
|
|
|
|
# No auth user
|
|
{:ok, <<>>} = Security.authenticate_message(no_auth_user, message)
|
|
{:ok, {^message, <<>>}} = Security.encrypt_message(no_auth_user, message)
|
|
|
|
# Auth user
|
|
{:ok, auth_params} = Security.authenticate_message(auth_user, message)
|
|
assert byte_size(auth_params) > 0
|
|
{:ok, {^message, <<>>}} = Security.encrypt_message(auth_user, message)
|
|
|
|
# Full user
|
|
{:ok, auth_params} = Security.authenticate_message(full_user, message)
|
|
{:ok, {encrypted, priv_params}} = Security.encrypt_message(full_user, message)
|
|
assert byte_size(auth_params) > 0
|
|
assert byte_size(priv_params) > 0
|
|
assert encrypted != message
|
|
end
|
|
end
|
|
|
|
describe "error handling and edge cases" do
|
|
test "handles empty and invalid inputs gracefully" do
|
|
# Empty passwords
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
auth_password: "",
|
|
engine_id: "test_engine"
|
|
]
|
|
|
|
assert {:error, _reason} = Security.create_user("test", config)
|
|
|
|
# Invalid engine ID
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
auth_password: "valid_password",
|
|
engine_id: ""
|
|
]
|
|
|
|
assert {:error, _reason} = Security.create_user("test", config)
|
|
end
|
|
|
|
test "handles protocol edge cases" do
|
|
# All supported auth protocols should work
|
|
auth_protocols = [:md5, :sha1, :sha256, :sha384, :sha512]
|
|
|
|
for protocol <- auth_protocols do
|
|
config = [
|
|
auth_protocol: protocol,
|
|
auth_password: "test_password_for_#{protocol}",
|
|
engine_id: "edge_case_engine"
|
|
]
|
|
|
|
assert {:ok, _user} = Security.create_user("user_#{protocol}", config)
|
|
end
|
|
|
|
# All supported priv protocols should work
|
|
priv_protocols = [:des, :aes128, :aes192, :aes256]
|
|
|
|
for protocol <- priv_protocols do
|
|
config = [
|
|
auth_protocol: :sha256,
|
|
auth_password: "auth_password",
|
|
priv_protocol: protocol,
|
|
priv_password: "priv_password_for_#{protocol}",
|
|
engine_id: "edge_case_engine"
|
|
]
|
|
|
|
assert {:ok, _user} = Security.create_user("user_#{protocol}", config)
|
|
end
|
|
end
|
|
end
|
|
end
|