# Copyright 2020 Istvan Ruzman # SPDX-License-Identifier: MIT OR Apache-2.0 import struct from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from itertools import product import pytest from pyrad3 import dictionary, utils # @pytest.mark.parametrize("header", [ # b""]) # def test_valid_header(header): # utils.parse_header(header) SECRET = b"secret" def num_tlv(num_type, num, length, expected=None): exp = num if expected is None else expected return (num_type + num.to_bytes(length, "big"), exp) TEST_ATTRIBUTES = [ (b"\x01\x07ABCDE", "ABCDE"), # rfc string (b"\x02\x07ABCDE", b"ABCDE"), # rfc octets (b"\x03\x06\0\0\0\0", 0), # rfc date # TODO: ABINARY (b"\x05\x03\x00", 0), # rfc byte num_tlv(b"\x06\x04", 0, 2), # rfc short num_tlv(b"\x06\x04", 0xFF, 2), # rfc short num_tlv(b"\x06\x04", 0x100, 2), # rfc short num_tlv(b"\x06\x04", 0xFFFF, 2), # rfc short num_tlv(b"\x07\x06", 0, 4), # rfc integer num_tlv(b"\x07\x06", 0xFF, 4), # rfc integer num_tlv(b"\x07\x06", 0x100, 4), # rfc integer num_tlv(b"\x07\x06", 0xFFFF, 4), # rfc integer num_tlv(b"\x07\x06", 0x10000, 4), # rfc integer num_tlv(b"\x07\x06", 0xFFFFFFFF, 4), # rfc integer num_tlv(b"\x08\x06", 0, 4), # rfc signed num_tlv(b"\x08\x06", 0xFF, 4), # rfc signed num_tlv(b"\x08\x06", 0x1000, 4), # rfc signed num_tlv(b"\x08\x06", 0xFFFF, 4), # rfc signed num_tlv(b"\x08\x06", 0x10000, 4), # rfc signed num_tlv(b"\x08\x06", 0xFFFFFFFF, 4, -1), # rfc signed num_tlv(b"\x08\x06", 0x80000000, 4, -2147483648), # rfc signed num_tlv(b"\x08\x06", 0x7FFFFFFF, 4, 2147483647), # rfc signed num_tlv(b"\x09\x0A", 0, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0xFF, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0x100, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0xFFFF, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0x10000, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0xFFFFFFFF, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0x100000000, 8), # rfc integer64 num_tlv(b"\x09\x0A", 0xFFFFFFFFFFFFFFFF, 8), # rfc integer64 (b"\x0a\x06\xc0\xa8\x01\x08", IPv4Address("192.168.1.8")), (b"\x0b\x07\x10\xc0\xa8\x00\x00", IPv4Network("192.168.0.0/16")), ( b"\x0c\x12\x20\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", IPv6Address("2003::1"), ), ( b"\x0d\x14\x00\x40\x20\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", IPv6Network("2003::0/64"), ), (b"\x0c\x04\x20\x03", IPv6Address("2003::0")), (b"\x0d\x06\x00\x40\x20\x03", IPv6Network("2003::0/64")), ] TAGGED_ATTRIBUTES = [ (b"\x65\x08\x01ABCDE", "ABCDE"), # rfc string (b"\x66\x08\x01ABCDE", b"ABCDE"), # rfc octets (b"\x67\x07\x01\0\0\0\0", 0), # rfc date # TODO: ABINARY (b"\x69\x04\x01\x00", 0), # rfc byte num_tlv(b"\x6a\x05\x01", 0, 2), # rfc short num_tlv(b"\x6a\x05\x01", 0xFF, 2), # rfc short num_tlv(b"\x6a\x05\x01", 0x100, 2), # rfc short num_tlv(b"\x6a\x05\x01", 0xFFFF, 2), # rfc short num_tlv(b"\x6b\x06\x01", 0, 3), # rfc integer num_tlv(b"\x6b\x06\x01", 0xFF, 3), # rfc integer num_tlv(b"\x6b\x06\x01", 0x100, 3), # rfc integer num_tlv(b"\x6b\x06\x01", 0xFFFF, 3), # rfc integer num_tlv(b"\x6b\x06\x01", 0x10000, 3), # rfc integer # integer with tag is only 3 bytes # num_tlv(b"\x70\x06\x01", 0xFFFFFFFF, 4), # rfc integer num_tlv(b"\x6c\x07\x01", 0, 4), # rfc signed num_tlv(b"\x6c\x07\x01", 0xFF, 4), # rfc signed num_tlv(b"\x6c\x07\x01", 0x1000, 4), # rfc signed num_tlv(b"\x6c\x07\x01", 0xFFFF, 4), # rfc signed num_tlv(b"\x6c\x07\x01", 0x10000, 4), # rfc signed num_tlv(b"\x6c\x07\x01", 0xFFFFFFFF, 4, -1), # rfc signed num_tlv(b"\x6c\x07\x01", 0x80000000, 4, -2147483648), # rfc signed num_tlv(b"\x6c\x07\x01", 0x7FFFFFFF, 4, 2147483647), # rfc signed num_tlv(b"\x6d\x0B\x01", 0, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0xFF, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0x100, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0xFFFF, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0x10000, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0xFFFFFFFF, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0x100000000, 8), # rfc integer64 num_tlv(b"\x6d\x0B\x01", 0xFFFFFFFFFFFFFFFF, 8), # rfc integer64 (b"\x6e\x07\x01\xc0\xa8\x01\x08", IPv4Address("192.168.1.8")), (b"\x6f\x08\x01\x10\xc0\xa8\x00\x00", IPv4Network("192.168.0.0/16")), ( b"\x70\x13\x01\x20\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", IPv6Address("2003::1"), ), ( b"\x71\x15\x01\x00\x40\x20\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", IPv6Network("2003::0/64"), ), (b"\x70\x05\x01\x20\x03", IPv6Address("2003::0")), (b"\x71\x07\x01\x00\x40\x20\x03", IPv6Network("2003::0/64")), ] VENDOR_FORMAT_COMBINATIONS = [ (1234, 1, 0), (1235, 1, 1), (1236, 1, 2), (1237, 2, 0), (1238, 2, 1), (1239, 2, 2), (1240, 4, 0), (1241, 4, 1), (1242, 4, 2), ] def flattened_product(l1, l2): result = [] for combination in product(l1, l2): left, right = combination result.append(list(left) + list(right)) return result VENDOR_TEST_ATTRIBUTES = flattened_product( VENDOR_FORMAT_COMBINATIONS, TEST_ATTRIBUTES ) VENDOR_TAGGED_ATTRIBUTES = flattened_product( VENDOR_FORMAT_COMBINATIONS, TAGGED_ATTRIBUTES ) @pytest.fixture def radius_dictionary(): return dictionary.Dictionary("tests/dictionaries/dict") @pytest.mark.parametrize( "header", [ b"\1\0" + struct.pack("!H", 5000) + 4996 * b"\0", b"\1\0" + struct.pack("!H", 100), b"\0\0" + struct.pack("!H", 20) + 16 * b"\0", b"", ], ) def test_invalid_header(header): with pytest.raises(utils.PacketError): utils.decode_header(header) @pytest.mark.parametrize("attr_bytes, expected", TEST_ATTRIBUTES) def test_decode_attribute_rfc(radius_dictionary, attr_bytes, expected): raw_packet = bytes(20) + attr_bytes attrs = utils.decode_attributes(radius_dictionary, raw_packet) assert len(attrs) == 1 assert attrs[0].value == expected assert attrs[0].tag == 0 @pytest.mark.parametrize("attr_bytes, expected", TAGGED_ATTRIBUTES) def test_decode_attribute_rfc_tagged(radius_dictionary, attr_bytes, expected): raw_packet = bytes(20) + attr_bytes attrs = utils.decode_attributes(radius_dictionary, raw_packet) assert len(attrs) == 1 assert attrs[0].value == expected assert attrs[0].tag == 1 def generate_vendor_attribute(vendor_id, tlen, llen, attr_bytes): vendor_id = vendor_id.to_bytes(4, "big") vsa_length = (4 + tlen + llen + len(attr_bytes)).to_bytes(1, "big") attr_type = attr_bytes[0].to_bytes(tlen, "big") attr_len = attr_bytes[1] + tlen if llen == 0: attr_len = b"" else: attr_len = attr_len.to_bytes(llen, "big") attr_bytes = attr_bytes[2:] packet = ( bytes(20) + b"\x1a" + vsa_length + vendor_id + attr_type + attr_len + attr_bytes ) return packet @pytest.mark.parametrize( "vendor_id, tlen, llen, attr_bytes, expected", VENDOR_TEST_ATTRIBUTES ) def test_decode_attribute_vsa( radius_dictionary, vendor_id, tlen, llen, attr_bytes, expected ): raw_packet = generate_vendor_attribute(vendor_id, tlen, llen, attr_bytes) attrs = utils.decode_attributes(radius_dictionary, raw_packet) assert len(attrs) == 1 assert attrs[0].value == expected assert attrs[0].tag == 0 @pytest.mark.parametrize( "vendor_id, tlen, llen, attr_bytes, expected", VENDOR_TAGGED_ATTRIBUTES ) def test_decode_attribute_vsa_tagged( radius_dictionary, vendor_id, tlen, llen, attr_bytes, expected ): raw_packet = generate_vendor_attribute(vendor_id, tlen, llen, attr_bytes) attrs = utils.decode_attributes(radius_dictionary, raw_packet) assert len(attrs) == 1 assert attrs[0].value == expected assert attrs[0].tag == 1 @pytest.mark.parametrize( "plaintext, obfuscated, authenticator", [ ( b"short_password", "ed9b49281f9de8edefae1b09b04beb86", "7b19486d8372b8c136ccf2444d0a5b2c", ), ( b"superlongpassword_exeeding_16_bytes", "1f123e277869997fdfb93f6df037024463918d29064c9fcd5831c57dccd9308ac6b835e6d8f70995d1498a6c5a2a5b71", "12441ce350ce269c04f650f7923058e1", ), ], ) def test_password(plaintext, obfuscated, authenticator): obfuscated = bytes.fromhex(obfuscated) authenticator = bytes.fromhex(authenticator) encoded = utils.password_encode(SECRET, authenticator, plaintext) assert len(encoded) == len(obfuscated) assert encoded == obfuscated decoded = utils.password_decode(SECRET, authenticator, encoded) assert len(decoded) == len(plaintext) assert decoded == plaintext assert utils.validate_pap_password( SECRET, authenticator, encoded, plaintext ) @pytest.mark.parametrize( "plaintext, chap, challenge", [ ( b"short_password", bytes.fromhex("2302a92821f675a52df8e5a3b10e49b0ab"), b"1234567890ABCDEF", ), ], ) def test_chap_password(plaintext, chap, challenge): chapid = chap[:1] encoded = utils.create_chap_password(chapid, challenge, plaintext) assert len(encoded) == len(chap) assert encoded == chap assert utils.validate_chap_password(chapid, challenge, chap, plaintext) def test_salt_crypt(): plaintext = (13).to_bytes(4, "big") authenticator = bytes.fromhex("18e3657cf849d5e677d8752486ceaad7") radius_value = bytes.fromhex("8472d1f6511f389ea42d572fed0f52a77159") salt = int.from_bytes(radius_value[:2], "big") encrypted = utils.salt_encrypt(SECRET, authenticator, plaintext, salt) decrypted = utils.salt_decrypt(SECRET, authenticator, encrypted[2:], salt) assert len(radius_value) == len(encrypted) # assert radius_value == encrypted assert len(plaintext) == len(decrypted) assert plaintext == decrypted # @pytest.mark.parametrize( # "plaintext, encrypted", [("some-password", # bytes.fromhex('7a9528106b80e4aa05b143708400d37e'), # bytes.fromhex('62c3dcbdc8d7239fc782a300f8b7707c'))]), # def test_ascend_password(plaintext, encrypted, authenticator): # salt = # enc = utils.salt_encrypt("secret", authenticator, salt, plaintext) # dec = utils.salt_decrypt("secret", authenticator, salt, enc) # assert enc == encrypted # assert plaintext = dec