652 lines
20 KiB
Elixir
652 lines
20 KiB
Elixir
defmodule SnmpKit.SnmpLib.SNMPv3EdgeCasesTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
alias SnmpKit.SnmpLib.PDU.V3Encoder
|
|
alias SnmpKit.SnmpLib.Security.Auth
|
|
alias SnmpKit.SnmpLib.Security.Priv
|
|
|
|
@moduletag :snmpv3
|
|
|
|
@moduletag :unit
|
|
@moduletag :snmpv3
|
|
@moduletag :edge_cases
|
|
|
|
describe "Message boundary conditions" do
|
|
test "minimum valid message size" do
|
|
# Smallest possible SNMPv3 message
|
|
minimal_pdu = %{
|
|
type: :get_request,
|
|
request_id: 1,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1], :null, :null}]
|
|
}
|
|
|
|
minimal_msg = %{
|
|
version: 3,
|
|
msg_id: 1,
|
|
# RFC minimum
|
|
msg_max_size: 484,
|
|
msg_flags: %{auth: false, priv: false, reportable: false},
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: "",
|
|
context_name: "",
|
|
pdu: minimal_pdu
|
|
}
|
|
}
|
|
|
|
assert {:ok, encoded} = V3Encoder.encode_message(minimal_msg, nil)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, nil)
|
|
assert decoded.version == 3
|
|
end
|
|
|
|
test "maximum message ID values" do
|
|
max_values = [
|
|
# RFC maximum
|
|
2_147_483_647,
|
|
# Minimum
|
|
0,
|
|
# Edge case
|
|
1,
|
|
# 32-bit max (should be handled gracefully)
|
|
4_294_967_295
|
|
]
|
|
|
|
for msg_id <- max_values do
|
|
discovery_msg = V3Encoder.create_discovery_message(msg_id)
|
|
assert {:ok, encoded} = V3Encoder.encode_message(discovery_msg, nil)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, nil)
|
|
|
|
# Should handle large values gracefully
|
|
assert is_integer(decoded.msg_id)
|
|
end
|
|
end
|
|
|
|
test "extreme message sizes" do
|
|
# Test with maximum allowed message size
|
|
large_msg = %{
|
|
version: 3,
|
|
msg_id: 12_345,
|
|
# RFC maximum
|
|
msg_max_size: 2_147_483_647,
|
|
msg_flags: %{auth: false, priv: false, reportable: true},
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: "",
|
|
context_name: "",
|
|
pdu: %{
|
|
type: :get_request,
|
|
request_id: 12_345,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
}
|
|
}
|
|
|
|
assert {:ok, encoded} = V3Encoder.encode_message(large_msg, nil)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, nil)
|
|
assert decoded.msg_max_size == 2_147_483_647
|
|
end
|
|
|
|
test "empty and minimal string values" do
|
|
# Including Unicode
|
|
test_strings = ["", " ", "\n", "\t", "a", "🔒"]
|
|
|
|
for test_string <- test_strings do
|
|
msg = %{
|
|
version: 3,
|
|
msg_id: 54_321,
|
|
msg_max_size: 65_507,
|
|
msg_flags: %{auth: false, priv: false, reportable: true},
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: test_string,
|
|
context_name: test_string,
|
|
pdu: %{
|
|
type: :get_request,
|
|
request_id: 54_321,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
}
|
|
}
|
|
|
|
assert {:ok, encoded} = V3Encoder.encode_message(msg, nil)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, nil)
|
|
assert decoded.msg_data.context_engine_id == test_string
|
|
assert decoded.msg_data.context_name == test_string
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Malformed message handling" do
|
|
test "truncated messages at various points" do
|
|
original_msg = V3Encoder.create_discovery_message(98_765)
|
|
{:ok, complete_packet} = V3Encoder.encode_message(original_msg, nil)
|
|
|
|
# Test truncation at different percentages
|
|
truncation_points = [10, 25, 50, 75, 90, 99]
|
|
|
|
for percentage <- truncation_points do
|
|
truncate_size = div(byte_size(complete_packet) * percentage, 100)
|
|
truncated = binary_part(complete_packet, 0, truncate_size)
|
|
|
|
# Should fail gracefully
|
|
assert {:error, _reason} = V3Encoder.decode_message(truncated, nil)
|
|
end
|
|
end
|
|
|
|
test "corrupted message data" do
|
|
original_msg = V3Encoder.create_discovery_message(11_111)
|
|
{:ok, complete_packet} = V3Encoder.encode_message(original_msg, nil)
|
|
|
|
# Test specific types of corruption that should definitely fail
|
|
corruption_tests = [
|
|
# Truncate message
|
|
binary_part(complete_packet, 0, 10),
|
|
# Invalid tag at start
|
|
<<0xFF>> <> binary_part(complete_packet, 1, byte_size(complete_packet) - 1),
|
|
# Invalid length encoding
|
|
<<0x30, 0xFF, 0xFF, 0xFF, 0xFF>>,
|
|
# Empty data
|
|
<<>>
|
|
]
|
|
|
|
for corrupted <- corruption_tests do
|
|
# Should handle corruption gracefully - either error or valid decode
|
|
result = V3Encoder.decode_message(corrupted, nil)
|
|
# Decoder should not crash - either succeeds or fails gracefully
|
|
valid? = match?({:ok, _}, result) or match?({:error, _}, result)
|
|
assert valid?
|
|
end
|
|
end
|
|
|
|
test "invalid ASN.1 structures" do
|
|
invalid_asn1_packets = [
|
|
# Invalid tag
|
|
<<0xFF, 0x10, 0x01, 0x02, 0x03>>,
|
|
# Invalid length encoding
|
|
<<0x30, 0xFF, 0xFF, 0xFF, 0xFF>>,
|
|
# Premature end
|
|
<<0x30, 0x50>>,
|
|
# Invalid sequence
|
|
<<0x30, 0x03, 0x01, 0x02>>
|
|
]
|
|
|
|
for invalid_packet <- invalid_asn1_packets do
|
|
assert {:error, _reason} = V3Encoder.decode_message(invalid_packet, nil)
|
|
end
|
|
end
|
|
|
|
test "inconsistent message flags and security parameters" do
|
|
user = create_test_user(:auth_priv)
|
|
|
|
# Message claims encryption but no privacy key
|
|
inconsistent_msg = %{
|
|
version: 3,
|
|
msg_id: 22_222,
|
|
msg_max_size: 65_507,
|
|
msg_flags: %{auth: true, priv: true, reportable: true},
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: "test_engine",
|
|
context_name: "",
|
|
pdu: %{
|
|
type: :get_request,
|
|
request_id: 22_222,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
}
|
|
}
|
|
|
|
# Should still encode (flags are just flags)
|
|
assert {:ok, _encoded} = V3Encoder.encode_message(inconsistent_msg, user)
|
|
end
|
|
end
|
|
|
|
describe "Security edge cases" do
|
|
test "zero-length keys" do
|
|
assert {:error, :empty_auth_key} = Auth.authenticate(:sha256, <<>>, "test message")
|
|
assert {:error, :empty_key} = Auth.validate_key(:sha256, <<>>)
|
|
assert {:error, :invalid_key_size} = Priv.validate_key(:aes128, <<>>)
|
|
end
|
|
|
|
test "oversized keys" do
|
|
# Keys that are too large
|
|
oversized_auth_key = :crypto.strong_rand_bytes(100)
|
|
oversized_priv_key = :crypto.strong_rand_bytes(100)
|
|
|
|
assert {:error, :invalid_key_length} = Auth.validate_key(:sha256, oversized_auth_key)
|
|
assert {:error, :invalid_key_size} = Priv.validate_key(:aes128, oversized_priv_key)
|
|
end
|
|
|
|
test "non-binary key types" do
|
|
invalid_keys = [
|
|
123,
|
|
[:list, :key],
|
|
%{map: "key"},
|
|
nil
|
|
]
|
|
|
|
for invalid_key <- invalid_keys do
|
|
assert {:error, _} = Auth.authenticate(:sha256, invalid_key, "test")
|
|
assert {:error, _} = Auth.validate_key(:sha256, invalid_key)
|
|
assert {:error, _} = Priv.validate_key(:aes128, invalid_key)
|
|
end
|
|
|
|
# Strings are valid binary data but should be tested separately for completeness
|
|
assert {:ok, _} = Auth.authenticate(:sha256, "string_key", "test")
|
|
end
|
|
|
|
test "authentication with extremely large messages" do
|
|
key = :crypto.strong_rand_bytes(32)
|
|
# 1MB message
|
|
huge_message = String.duplicate("X", 1_000_000)
|
|
|
|
# Should handle large messages efficiently
|
|
start_time = System.monotonic_time(:microsecond)
|
|
assert {:ok, auth_params} = Auth.authenticate(:sha256, key, huge_message)
|
|
end_time = System.monotonic_time(:microsecond)
|
|
|
|
# Should complete within reasonable time (1 second)
|
|
assert end_time - start_time < 1_000_000
|
|
assert byte_size(auth_params) == 16
|
|
end
|
|
|
|
test "encryption with non-standard block sizes" do
|
|
priv_key = :crypto.strong_rand_bytes(16)
|
|
auth_key = :crypto.strong_rand_bytes(16)
|
|
|
|
# Test various plaintext sizes around block boundaries
|
|
test_sizes = [0, 1, 15, 16, 17, 31, 32, 33, 63, 64, 65]
|
|
|
|
for size <- test_sizes do
|
|
plaintext = String.duplicate("A", size)
|
|
|
|
assert {:ok, {ciphertext, priv_params}} =
|
|
Priv.encrypt(:aes128, priv_key, auth_key, plaintext)
|
|
|
|
assert {:ok, decrypted} =
|
|
Priv.decrypt(:aes128, priv_key, auth_key, ciphertext, priv_params)
|
|
|
|
assert decrypted == plaintext
|
|
end
|
|
end
|
|
|
|
test "invalid protocol combinations" do
|
|
invalid_user = %{
|
|
security_name: "invalid_user",
|
|
auth_protocol: :invalid_auth,
|
|
priv_protocol: :invalid_priv,
|
|
auth_key: :crypto.strong_rand_bytes(32),
|
|
priv_key: :crypto.strong_rand_bytes(16),
|
|
engine_id: "test_engine",
|
|
engine_boots: 1,
|
|
engine_time: System.system_time(:second)
|
|
}
|
|
|
|
test_msg = create_test_v3_message(33_333, invalid_user)
|
|
|
|
assert {:error, _} = V3Encoder.encode_message(test_msg, invalid_user)
|
|
end
|
|
|
|
test "time rollover and edge cases" do
|
|
# Test with various time values
|
|
time_values = [
|
|
# Epoch
|
|
0,
|
|
# Minimum positive
|
|
1,
|
|
# Max 32-bit signed
|
|
2_147_483_647,
|
|
# Max 32-bit unsigned
|
|
4_294_967_295
|
|
]
|
|
|
|
for time_val <- time_values do
|
|
user = %{
|
|
security_name: "time_user",
|
|
auth_protocol: :sha256,
|
|
priv_protocol: :none,
|
|
auth_key: :crypto.strong_rand_bytes(32),
|
|
priv_key: <<>>,
|
|
engine_id: "time_engine",
|
|
engine_boots: 1,
|
|
engine_time: time_val
|
|
}
|
|
|
|
test_msg = create_test_v3_message(44_444, user)
|
|
assert {:ok, encoded} = V3Encoder.encode_message(test_msg, user)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, user)
|
|
assert decoded.version == 3
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Memory and performance edge cases" do
|
|
test "extremely long OID lists" do
|
|
# Create OID with 100 components (reduced from 1000 for practical limits)
|
|
long_oid = Enum.to_list(1..100)
|
|
|
|
long_pdu = %{
|
|
type: :get_request,
|
|
request_id: 55_555,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{long_oid, :null, :null}]
|
|
}
|
|
|
|
user = create_test_user(:auth_priv)
|
|
|
|
long_msg = %{
|
|
version: 3,
|
|
msg_id: 55_555,
|
|
msg_max_size: 65_507,
|
|
msg_flags: %{auth: true, priv: true, reportable: true},
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: "long_engine",
|
|
context_name: "",
|
|
pdu: long_pdu
|
|
}
|
|
}
|
|
|
|
case V3Encoder.encode_message(long_msg, user) do
|
|
{:ok, encoded} ->
|
|
case V3Encoder.decode_message(encoded, user) do
|
|
{:ok, decoded} ->
|
|
# Verify we have at least one varbind and it's reasonably long
|
|
assert decoded.msg_data.pdu.varbinds != []
|
|
[{decoded_oid, _, _} | _] = decoded.msg_data.pdu.varbinds
|
|
# Allow some tolerance for encoding limits
|
|
assert length(decoded_oid) >= 50
|
|
|
|
{:error, _reason} ->
|
|
# Long OIDs may exceed practical encoding limits - this is acceptable
|
|
:ok
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
# Very long OIDs may exceed encoding limits - this is acceptable behavior
|
|
:ok
|
|
end
|
|
end
|
|
|
|
test "many small varbinds" do
|
|
# Create 1000 small varbinds
|
|
many_varbinds = for i <- 1..1000, do: {[1, 3, 6, 1, 2, 1, 1, i, 0], :null, :null}
|
|
|
|
many_pdu = %{
|
|
type: :get_request,
|
|
request_id: 66_666,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: many_varbinds
|
|
}
|
|
|
|
user = create_test_user(:auth_priv)
|
|
many_msg = create_test_v3_message_with_pdu(66_666, many_pdu, user)
|
|
|
|
start_time = System.monotonic_time(:microsecond)
|
|
assert {:ok, encoded} = V3Encoder.encode_message(many_msg, user)
|
|
encode_time = System.monotonic_time(:microsecond) - start_time
|
|
|
|
start_time = System.monotonic_time(:microsecond)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, user)
|
|
decode_time = System.monotonic_time(:microsecond) - start_time
|
|
|
|
# Should handle large numbers of varbinds efficiently
|
|
# Allow up to 100ms for encoding/decoding 1000 varbinds
|
|
assert encode_time < 100_000
|
|
assert decode_time < 100_000
|
|
assert length(decoded.msg_data.pdu.varbinds) == 1000
|
|
end
|
|
|
|
test "repeated encryption/decryption cycles" do
|
|
user = create_test_user(:auth_priv)
|
|
test_data = "Repeated encryption test data"
|
|
|
|
# Perform 100 encryption/decryption cycles
|
|
for i <- 1..100 do
|
|
assert {:ok, {ciphertext, priv_params}} =
|
|
Priv.encrypt(user.priv_protocol, user.priv_key, user.auth_key, test_data)
|
|
|
|
assert {:ok, decrypted} =
|
|
Priv.decrypt(
|
|
user.priv_protocol,
|
|
user.priv_key,
|
|
user.auth_key,
|
|
ciphertext,
|
|
priv_params
|
|
)
|
|
|
|
assert decrypted == test_data
|
|
|
|
# Each encryption should produce different ciphertext (due to random IV)
|
|
if i > 1 do
|
|
{:ok, {other_ciphertext, _}} =
|
|
Priv.encrypt(user.priv_protocol, user.priv_key, user.auth_key, test_data)
|
|
|
|
assert ciphertext != other_ciphertext
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Protocol compliance edge cases" do
|
|
test "RFC minimum and maximum values" do
|
|
# Test RFC 3412 limits
|
|
rfc_values = %{
|
|
msg_id: [0, 2_147_483_647],
|
|
msg_max_size: [484, 2_147_483_647],
|
|
request_id: [0, 2_147_483_647],
|
|
error_status: [0, 5],
|
|
error_index: [0, 2_147_483_647]
|
|
}
|
|
|
|
for {field, values} <- rfc_values do
|
|
for value <- values do
|
|
case field do
|
|
:msg_id ->
|
|
msg = V3Encoder.create_discovery_message(value)
|
|
assert {:ok, encoded} = V3Encoder.encode_message(msg, nil)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, nil)
|
|
assert decoded.msg_id == value
|
|
|
|
:msg_max_size ->
|
|
msg = %{V3Encoder.create_discovery_message(12_345) | msg_max_size: value}
|
|
assert {:ok, encoded} = V3Encoder.encode_message(msg, nil)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, nil)
|
|
assert decoded.msg_max_size == value
|
|
|
|
_ ->
|
|
# Test in PDU context
|
|
user = create_test_user(:no_auth_no_priv)
|
|
|
|
pdu = %{
|
|
type: :get_request,
|
|
request_id: if(field == :request_id, do: value, else: 77_777),
|
|
error_status: if(field == :error_status, do: value, else: 0),
|
|
error_index: if(field == :error_index, do: value, else: 0),
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
|
|
msg = create_test_v3_message_with_pdu(77_777, pdu, user)
|
|
assert {:ok, encoded} = V3Encoder.encode_message(msg, user)
|
|
assert {:ok, _decoded} = V3Encoder.decode_message(encoded, user)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
test "all PDU types with security" do
|
|
user = create_test_user(:auth_priv)
|
|
|
|
pdu_types = [
|
|
:get_request,
|
|
:get_next_request,
|
|
:get_response,
|
|
:set_request,
|
|
:get_bulk_request
|
|
]
|
|
|
|
for pdu_type <- pdu_types do
|
|
pdu =
|
|
case pdu_type do
|
|
:get_bulk_request ->
|
|
%{
|
|
type: pdu_type,
|
|
request_id: 88_888,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
non_repeaters: 0,
|
|
max_repetitions: 10,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
|
|
_ ->
|
|
%{
|
|
type: pdu_type,
|
|
request_id: 88_888,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
end
|
|
|
|
msg = create_test_v3_message_with_pdu(88_888, pdu, user)
|
|
assert {:ok, encoded} = V3Encoder.encode_message(msg, user)
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, user)
|
|
assert decoded.msg_data.pdu.type == pdu_type
|
|
end
|
|
end
|
|
|
|
test "all combinations of message flags" do
|
|
user = create_test_user(:auth_priv)
|
|
|
|
flag_combinations = [
|
|
%{auth: false, priv: false, reportable: false},
|
|
%{auth: false, priv: false, reportable: true},
|
|
%{auth: true, priv: false, reportable: false},
|
|
%{auth: true, priv: false, reportable: true},
|
|
%{auth: true, priv: true, reportable: false},
|
|
%{auth: true, priv: true, reportable: true}
|
|
]
|
|
|
|
for flags <- flag_combinations do
|
|
msg = %{
|
|
version: 3,
|
|
msg_id: 99_999,
|
|
msg_max_size: 65_507,
|
|
msg_flags: flags,
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: "flag_engine",
|
|
context_name: "",
|
|
pdu: %{
|
|
type: :get_request,
|
|
request_id: 99_999,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
}
|
|
}
|
|
|
|
# Adjust user based on flags
|
|
test_user = user
|
|
|
|
test_user =
|
|
if flags.auth == false do
|
|
%{test_user | auth_protocol: :none, auth_key: <<>>}
|
|
else
|
|
test_user
|
|
end
|
|
|
|
test_user =
|
|
if flags.priv == false do
|
|
%{test_user | priv_protocol: :none, priv_key: <<>>}
|
|
else
|
|
test_user
|
|
end
|
|
|
|
assert {:ok, encoded} = V3Encoder.encode_message(msg, test_user)
|
|
|
|
assert {:ok, decoded} = V3Encoder.decode_message(encoded, test_user)
|
|
assert decoded.msg_flags == flags
|
|
end
|
|
end
|
|
end
|
|
|
|
# Helper functions
|
|
|
|
defp create_test_user(security_level) do
|
|
{auth_protocol, auth_key} =
|
|
case security_level do
|
|
:no_auth_no_priv -> {:none, <<>>}
|
|
_ -> {:sha256, :crypto.strong_rand_bytes(32)}
|
|
end
|
|
|
|
{priv_protocol, priv_key} =
|
|
case security_level do
|
|
:auth_priv -> {:aes128, :crypto.strong_rand_bytes(16)}
|
|
_ -> {:none, <<>>}
|
|
end
|
|
|
|
%{
|
|
security_name: "edge_test_user",
|
|
auth_protocol: auth_protocol,
|
|
priv_protocol: priv_protocol,
|
|
auth_key: auth_key,
|
|
priv_key: priv_key,
|
|
engine_id: "edge_test_engine",
|
|
engine_boots: 1,
|
|
engine_time: System.system_time(:second)
|
|
}
|
|
end
|
|
|
|
defp create_test_v3_message(msg_id, user) do
|
|
pdu = %{
|
|
type: :get_request,
|
|
request_id: msg_id,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}]
|
|
}
|
|
|
|
create_test_v3_message_with_pdu(msg_id, pdu, user)
|
|
end
|
|
|
|
defp create_test_v3_message_with_pdu(msg_id, pdu, user) do
|
|
flags = %{
|
|
auth: user.auth_protocol != :none,
|
|
priv: user.priv_protocol != :none,
|
|
reportable: true
|
|
}
|
|
|
|
%{
|
|
version: 3,
|
|
msg_id: msg_id,
|
|
msg_max_size: 65_507,
|
|
msg_flags: flags,
|
|
msg_security_model: 3,
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: user.engine_id,
|
|
context_name: "",
|
|
pdu: pdu
|
|
}
|
|
}
|
|
end
|
|
end
|