towerops/test/snmpkit/snmp_lib/asn1_test.exs

521 lines
16 KiB
Elixir

defmodule SnmpKit.SnmpLib.ASN1Test do
use ExUnit.Case, async: true
alias SnmpKit.SnmpLib.ASN1
@moduletag :unit
@moduletag :protocol
@moduletag :phase_2
describe "Integer encoding and decoding" do
test "encodes and decodes positive integers" do
{:ok, encoded} = ASN1.encode_integer(42)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
assert decoded == 42
end
test "encodes and decodes zero" do
{:ok, encoded} = ASN1.encode_integer(0)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
assert decoded == 0
end
test "encodes and decodes negative integers" do
{:ok, encoded} = ASN1.encode_integer(-1)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
assert decoded == -1
{:ok, encoded} = ASN1.encode_integer(-128)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
assert decoded == -128
end
test "encodes and decodes large integers" do
large_value = 123_456_789
{:ok, encoded} = ASN1.encode_integer(large_value)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
assert decoded == large_value
end
test "decodes integer with remaining data" do
{:ok, encoded} = ASN1.encode_integer(42)
test_data = encoded <> <<99, 100, 101>>
{:ok, {decoded, remaining}} = ASN1.decode_integer(test_data)
assert decoded == 42
assert remaining == <<99, 100, 101>>
end
test "rejects invalid integer tags" do
assert {:error, :invalid_tag} = ASN1.decode_integer(<<0x04, 0x01, 0x42>>)
assert {:error, :invalid_tag} = ASN1.decode_integer(<<0x05, 0x00>>)
end
end
describe "OCTET STRING encoding and decoding" do
test "encodes and decodes octet strings" do
test_string = "Hello, World!"
{:ok, encoded} = ASN1.encode_octet_string(test_string)
{:ok, {decoded, <<>>}} = ASN1.decode_octet_string(encoded)
assert decoded == test_string
end
test "encodes and decodes empty octet strings" do
{:ok, encoded} = ASN1.encode_octet_string("")
{:ok, {decoded, <<>>}} = ASN1.decode_octet_string(encoded)
assert decoded == ""
end
test "encodes and decodes binary data" do
binary_data = <<1, 2, 3, 255, 0, 42>>
{:ok, encoded} = ASN1.encode_octet_string(binary_data)
{:ok, {decoded, <<>>}} = ASN1.decode_octet_string(encoded)
assert decoded == binary_data
end
test "decodes octet string with remaining data" do
{:ok, encoded} = ASN1.encode_octet_string("test")
test_data = encoded <> <<99, 100>>
{:ok, {decoded, remaining}} = ASN1.decode_octet_string(test_data)
assert decoded == "test"
assert remaining == <<99, 100>>
end
test "rejects invalid octet string tags" do
assert {:error, :invalid_tag} = ASN1.decode_octet_string(<<0x02, 0x01, 0x42>>)
end
end
describe "NULL encoding and decoding" do
test "encodes and decodes null values" do
{:ok, encoded} = ASN1.encode_null()
assert encoded == <<0x05, 0x00>>
{:ok, {decoded, <<>>}} = ASN1.decode_null(encoded)
assert decoded == :null
end
test "decodes null with remaining data" do
test_data = <<0x05, 0x00, 42, 43>>
{:ok, {decoded, remaining}} = ASN1.decode_null(test_data)
assert decoded == :null
assert remaining == <<42, 43>>
end
test "rejects null with invalid length" do
assert {:error, :invalid_null_length} = ASN1.decode_null(<<0x05, 0x01, 0x00>>)
end
test "rejects invalid null tags" do
assert {:error, :invalid_tag} = ASN1.decode_null(<<0x02, 0x00>>)
end
end
describe "OBJECT IDENTIFIER encoding and decoding" do
test "encodes and decodes simple OIDs" do
oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
{:ok, encoded} = ASN1.encode_oid(oid)
{:ok, {decoded, <<>>}} = ASN1.decode_oid(encoded)
assert decoded == oid
end
test "encodes and decodes short OIDs" do
oid = [1, 3]
{:ok, encoded} = ASN1.encode_oid(oid)
{:ok, {decoded, <<>>}} = ASN1.decode_oid(encoded)
assert decoded == oid
end
test "encodes and decodes OIDs with large components" do
oid = [1, 3, 6, 1, 4, 1, 200, 1]
{:ok, encoded} = ASN1.encode_oid(oid)
{:ok, {decoded, <<>>}} = ASN1.decode_oid(encoded)
assert decoded == oid
end
test "decodes OID with remaining data" do
oid = [1, 3, 6, 1]
{:ok, encoded} = ASN1.encode_oid(oid)
test_data = encoded <> <<99, 100>>
{:ok, {decoded, remaining}} = ASN1.decode_oid(test_data)
assert decoded == oid
assert remaining == <<99, 100>>
end
test "rejects invalid OIDs" do
assert {:error, :invalid_oid} = ASN1.encode_oid([])
# Single-component OIDs are valid
assert {:ok, _} = ASN1.encode_oid([1])
assert {:error, :invalid_oid} = ASN1.encode_oid(:not_a_list)
end
test "rejects invalid OID tags" do
assert {:error, :invalid_tag} = ASN1.decode_oid(<<0x02, 0x01, 0x42>>)
end
end
describe "SEQUENCE encoding and decoding" do
test "encodes and decodes sequences" do
# Create sequence content: INTEGER 42 + OCTET STRING "test"
{:ok, int_encoded} = ASN1.encode_integer(42)
{:ok, str_encoded} = ASN1.encode_octet_string("test")
content = int_encoded <> str_encoded
{:ok, sequence_encoded} = ASN1.encode_sequence(content)
{:ok, {decoded_content, <<>>}} = ASN1.decode_sequence(sequence_encoded)
assert decoded_content == content
end
test "encodes and decodes empty sequences" do
{:ok, encoded} = ASN1.encode_sequence(<<>>)
{:ok, {decoded, <<>>}} = ASN1.decode_sequence(encoded)
assert decoded == <<>>
end
test "decodes sequence with remaining data" do
{:ok, encoded} = ASN1.encode_sequence(<<1, 2, 3>>)
test_data = encoded <> <<99, 100>>
{:ok, {decoded, remaining}} = ASN1.decode_sequence(test_data)
assert decoded == <<1, 2, 3>>
assert remaining == <<99, 100>>
end
test "rejects invalid sequence tags" do
assert {:error, :invalid_tag} = ASN1.decode_sequence(<<0x02, 0x01, 0x42>>)
end
end
describe "Custom TLV encoding" do
test "encodes custom TLV structures" do
custom_tag = 0xA0
content = "custom_content"
{:ok, encoded} = ASN1.encode_custom_tlv(custom_tag, content)
# Should start with our custom tag
assert <<^custom_tag, _rest::binary>> = encoded
end
test "encodes TLV with different tags" do
{:ok, encoded1} = ASN1.encode_custom_tlv(0x80, "content1")
{:ok, encoded2} = ASN1.encode_custom_tlv(0x81, "content2")
assert <<0x80, _::binary>> = encoded1
assert <<0x81, _::binary>> = encoded2
end
end
describe "Generic TLV decoding" do
test "decodes any TLV structure" do
{:ok, encoded} = ASN1.encode_integer(42)
{:ok, {tag, content, <<>>}} = ASN1.decode_tlv(encoded)
# INTEGER tag
assert tag == 0x02
assert content == <<42>>
end
test "decodes TLV with remaining data" do
{:ok, encoded} = ASN1.encode_octet_string("test")
test_data = encoded <> <<99, 100>>
{:ok, {tag, content, remaining}} = ASN1.decode_tlv(test_data)
# OCTET STRING tag
assert tag == 0x04
assert content == "test"
assert remaining == <<99, 100>>
end
test "rejects insufficient data" do
assert {:error, :insufficient_data} = ASN1.decode_tlv(<<>>)
end
end
describe "Length encoding and decoding" do
test "encodes and decodes short form lengths" do
for length <- [0, 1, 42, 127] do
encoded = ASN1.encode_length(length)
{:ok, {decoded, <<>>}} = ASN1.decode_length(encoded)
assert decoded == length
end
end
test "encodes and decodes long form lengths" do
test_cases = [128, 200, 300, 1000, 65_535, 100_000]
for length <- test_cases do
encoded = ASN1.encode_length(length)
{:ok, {decoded, <<>>}} = ASN1.decode_length(encoded)
assert decoded == length
end
end
test "decodes length with remaining data" do
encoded = ASN1.encode_length(42)
test_data = encoded <> <<99, 100>>
{:ok, {decoded, remaining}} = ASN1.decode_length(test_data)
assert decoded == 42
assert remaining == <<99, 100>>
end
test "rejects indefinite length" do
assert {:error, :indefinite_length_not_supported} = ASN1.decode_length(<<0x80>>)
end
test "rejects overly long length encoding" do
# 5 octets for length is too much
assert {:error, :length_too_large} = ASN1.decode_length(<<0x85, 1, 2, 3, 4, 5>>)
end
test "rejects insufficient length bytes" do
assert {:error, :insufficient_length_bytes} = ASN1.decode_length(<<0x82, 0x01>>)
end
end
describe "Tag parsing" do
test "parses universal class tags" do
# INTEGER
info = ASN1.parse_tag(0x02)
assert info.class == :universal
assert info.constructed == false
assert info.tag_number == 2
assert info.original_byte == 0x02
end
test "parses constructed tags" do
# SEQUENCE
info = ASN1.parse_tag(0x30)
assert info.class == :universal
assert info.constructed == true
assert info.tag_number == 16
end
test "parses application class tags" do
# Application, primitive, tag 1
info = ASN1.parse_tag(0x41)
assert info.class == :application
assert info.constructed == false
assert info.tag_number == 1
end
test "parses context class tags" do
# Context, primitive, tag 0
info = ASN1.parse_tag(0x80)
assert info.class == :context
assert info.constructed == false
assert info.tag_number == 0
end
test "parses private class tags" do
# Private, primitive, tag 0
info = ASN1.parse_tag(0xC0)
assert info.class == :private
assert info.constructed == false
assert info.tag_number == 0
end
end
describe "BER structure validation" do
test "validates simple valid structures" do
{:ok, encoded} = ASN1.encode_integer(42)
assert :ok = ASN1.validate_ber_structure(encoded)
end
test "validates sequences of structures" do
{:ok, int_encoded} = ASN1.encode_integer(42)
{:ok, str_encoded} = ASN1.encode_octet_string("test")
combined = int_encoded <> str_encoded
assert :ok = ASN1.validate_ber_structure(combined)
end
test "validates nested sequences" do
# Sequence with INTEGER 42
{:ok, inner_seq} = ASN1.encode_sequence(<<0x02, 0x01, 0x42>>)
{:ok, outer_seq} = ASN1.encode_sequence(inner_seq)
assert :ok = ASN1.validate_ber_structure(outer_seq)
end
test "rejects malformed structures" do
# Invalid length - claims 10 bytes but only has 2
malformed = <<0x02, 0x0A, 0x42, 0x43>>
assert {:error, :insufficient_content} = ASN1.validate_ber_structure(malformed)
end
test "validates empty data" do
assert :ok = ASN1.validate_ber_structure(<<>>)
end
end
describe "BER length calculation" do
test "calculates length for simple structures" do
{:ok, encoded} = ASN1.encode_integer(42)
{:ok, total_length} = ASN1.calculate_ber_length(encoded)
assert total_length == byte_size(encoded)
end
test "calculates length for different structure sizes" do
test_cases = [
42,
123_456,
"Hello, World!",
String.duplicate("A", 200)
]
for test_value <- test_cases do
{:ok, encoded} =
case test_value do
value when is_integer(value) -> ASN1.encode_integer(value)
value when is_binary(value) -> ASN1.encode_octet_string(value)
end
{:ok, calculated_length} = ASN1.calculate_ber_length(encoded)
actual_length = byte_size(encoded)
assert calculated_length == actual_length
end
end
test "rejects insufficient data for length calculation" do
assert {:error, :insufficient_data} = ASN1.calculate_ber_length(<<>>)
assert {:error, :insufficient_data} = ASN1.calculate_ber_length(<<0x02>>)
end
end
describe "Round-trip encoding/decoding" do
test "integer round-trips correctly" do
test_values = [0, 1, -1, 42, -42, 127, -128, 255, -256, 32_767, -32_768]
for value <- test_values do
{:ok, encoded} = ASN1.encode_integer(value)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
assert decoded == value, "Failed round-trip for integer #{value}"
end
end
test "octet string round-trips correctly" do
test_values = [
"",
"Hello",
"Hello, World!",
<<0, 1, 2, 255>>,
String.duplicate("A", 100)
]
for value <- test_values do
{:ok, encoded} = ASN1.encode_octet_string(value)
{:ok, {decoded, <<>>}} = ASN1.decode_octet_string(encoded)
assert decoded == value
end
end
test "OID round-trips correctly" do
test_oids = [
[1, 3],
[1, 3, 6, 1, 2, 1],
[1, 3, 6, 1, 2, 1, 1, 1, 0],
[1, 3, 6, 1, 4, 1, 200, 1, 2, 3]
]
for oid <- test_oids do
{:ok, encoded} = ASN1.encode_oid(oid)
{:ok, {decoded, <<>>}} = ASN1.decode_oid(encoded)
assert decoded == oid
end
end
end
describe "Error handling and edge cases" do
test "handles encoding failures gracefully" do
# These should not crash, but return errors
assert {:error, :invalid_oid} = ASN1.encode_oid([])
assert {:error, :invalid_oid} = ASN1.encode_oid(:not_a_list)
end
test "handles decoding failures gracefully" do
# Truncated data
assert {:error, :insufficient_data} = ASN1.decode_integer(<<0x02>>)
assert {:error, :insufficient_content} = ASN1.decode_integer(<<0x02, 0x05, 0x42>>)
end
test "handles empty input data" do
assert {:error, :invalid_tag} = ASN1.decode_integer(<<>>)
assert {:error, :invalid_tag} = ASN1.decode_octet_string(<<>>)
assert {:error, :invalid_tag} = ASN1.decode_oid(<<>>)
assert {:error, :invalid_tag} = ASN1.decode_sequence(<<>>)
end
test "handles concurrent operations" do
# Test thread safety
tasks =
for i <- 1..20 do
Task.async(fn ->
value = rem(i, 1000)
{:ok, encoded} = ASN1.encode_integer(value)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
{i, value, decoded}
end)
end
results = Task.await_many(tasks, 1000)
# Verify all operations completed successfully
assert length(results) == 20
for {i, original, decoded} <- results do
expected = rem(i, 1000)
assert original == expected
assert decoded == expected
end
end
end
describe "Performance and large data handling" do
@tag :performance
test "handles large integers efficiently" do
large_values = [
123_456_789,
-123_456_789,
2_147_483_647,
-2_147_483_648
]
for value <- large_values do
start_time = System.monotonic_time(:microsecond)
{:ok, encoded} = ASN1.encode_integer(value)
{:ok, {decoded, <<>>}} = ASN1.decode_integer(encoded)
end_time = System.monotonic_time(:microsecond)
assert decoded == value
# Should complete in reasonable time (< 1ms)
assert end_time - start_time < 1000
end
end
test "handles large octet strings efficiently" do
large_string = String.duplicate("Hello, World! ", 100)
start_time = System.monotonic_time(:microsecond)
{:ok, encoded} = ASN1.encode_octet_string(large_string)
{:ok, {decoded, <<>>}} = ASN1.decode_octet_string(encoded)
end_time = System.monotonic_time(:microsecond)
assert decoded == large_string
# Should complete in reasonable time (< 200ms, adjusted for CI/busy systems)
assert end_time - start_time < 200_000
end
test "handles complex nested structures" do
# Create a sequence containing multiple nested sequences
# INTEGER 42
{:ok, inner1} = ASN1.encode_sequence(<<0x02, 0x01, 0x42>>)
# OCTET STRING "test"
{:ok, inner2} = ASN1.encode_sequence(<<0x04, 0x04, "test"::binary>>)
{:ok, outer} = ASN1.encode_sequence(inner1 <> inner2)
# Should be able to decode the outer structure
{:ok, {content, <<>>}} = ASN1.decode_sequence(outer)
assert content == inner1 <> inner2
# Should validate correctly
assert :ok = ASN1.validate_ber_structure(outer)
end
end
end