tests: move unittests into a sub directory

We currently mix the unit-tests with the shell script based integration
tests. Let's put them into a dedicated sub directory.

Related: OS#6531
Change-Id: I0978c5353d0d479a050bbb6e7ae5a63db5e08d24
This commit is contained in:
Philipp Maier
2024-08-15 11:49:30 +02:00
committed by dexter
parent 8680698f97
commit e0241037e7
14 changed files with 2 additions and 2 deletions

91
tests/unittests/test_apdu.py Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
import unittest
from pySim.utils import h2b, b2h
from pySim.construct import filter_dict
from pySim.apdu import Apdu
from pySim.apdu.ts_31_102 import UsimAuthenticateEven
class TestApdu(unittest.TestCase):
def test_successful(self):
apdu = Apdu('00a40400023f00', '9000')
self.assertEqual(apdu.successful, True)
apdu = Apdu('00a40400023f00', '6733')
self.assertEqual(apdu.successful, False)
def test_successful_method(self):
"""Test overloading of the success property with a custom method."""
class SwApdu(Apdu):
def _is_success(self):
return False
apdu = SwApdu('00a40400023f00', '9000')
self.assertEqual(apdu.successful, False)
# TODO: Tests for TS 102 221 / 31.102 ApduCommands
class TestUsimAuth(unittest.TestCase):
"""Test decoding of the rather complex USIM AUTHENTICATE command."""
def test_2g(self):
apdu = ('80880080' + '09' + '080001020304050607',
'04a0a1a2a308b0b1b2b3b4b5b6b79000')
res = {
'cmd': {'p1': 0, 'p2': {'scope': 'df_adf_specific', 'authentication_context': 'gsm'},
'body': {'rand': h2b('0001020304050607'), 'autn': None}},
'rsp': {'body': {'sres': h2b('a0a1a2a3'), 'kc': h2b('b0b1b2b3b4b5b6b7')}}
}
u = UsimAuthenticateEven(apdu[0], apdu[1])
d = filter_dict(u.to_dict())
self.assertEqual(d, res)
def test_3g(self):
apdu = ('80880081' + '12' + '080001020304050607081011121314151617',
'DB' + '08' + 'a0a1a2a3a4a5a6a7' +
'10' + 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebf' +
'10' + 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf' + '9000')
res = {
'cmd': {'p1': 0, 'p2': {'scope': 'df_adf_specific', 'authentication_context': 'umts'},
'body': {'rand': h2b('0001020304050607'), 'autn': h2b('1011121314151617')}},
'rsp': {'body': {'tag': 219,
'body': {
'res': h2b('a0a1a2a3a4a5a6a7'),
'ck': h2b('b0b1b2b3b4b5b6b7b8b9babbbcbdbebf'),
'ik': h2b('c0c1c2c3c4c5c6c7c8c9cacbcccdcecf'),
'kc': None
}
}
}
}
u = UsimAuthenticateEven(apdu[0], apdu[1])
d = filter_dict(u.to_dict())
self.assertEqual(d, res)
def test_3g_sync(self):
apdu = ('80880081' + '12' + '080001020304050607081011121314151617',
'DC' + '08' + 'a0a1a2a3a4a5a6a7' + '9000')
res = {
'cmd': {'p1': 0, 'p2': {'scope': 'df_adf_specific', 'authentication_context': 'umts'},
'body': {'rand': h2b('0001020304050607'), 'autn': h2b('1011121314151617')}},
'rsp': {'body': {'tag': 220, 'body': {'auts': h2b('a0a1a2a3a4a5a6a7') }}}
}
u = UsimAuthenticateEven(apdu[0], apdu[1])
d = filter_dict(u.to_dict())
self.assertEqual(d, res)
def test_vgcs(self):
apdu = ('80880082' + '0E' + '04' + '00010203' +
'01' + '10' +
'08' + '2021222324252627',
'DB' + '10' + 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebf' + '9000')
res = {
'cmd': {'p1': 0, 'p2': {'scope': 'df_adf_specific', 'authentication_context': 'vgcs_vbs'},
'body': { 'vk_id': h2b('10'), 'vservice_id': h2b('00010203'), 'vstk_rand': h2b('2021222324252627')}},
'rsp': {'body': {'vstk': h2b('b0b1b2b3b4b5b6b7b8b9babbbcbdbebf')}}
}
u = UsimAuthenticateEven(apdu[0], apdu[1])
d = filter_dict(u.to_dict())
self.assertEqual(d, res)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
import unittest
from pySim.utils import b2h, h2b
from pySim.construct import *
from construct import FlagsEnum
tests = [
( b'\x80', 0x80 ),
( b'\x80\x01', 0x8001 ),
( b'\x80\x00\x01', 0x800001 ),
( b'\x80\x23\x42\x01', 0x80234201 ),
]
class TestGreedyInt(unittest.TestCase):
def test_GreedyInt_decoder(self):
gi = GreedyInteger()
for t in tests:
self.assertEqual(gi.parse(t[0]), t[1])
def test_GreedyInt_encoder(self):
gi = GreedyInteger()
for t in tests:
self.assertEqual(t[0], gi.build(t[1]))
pass
class TestUtils(unittest.TestCase):
def test_filter_dict(self):
inp = {'foo': 0xf00, '_bar' : 0xba5, 'baz': 0xba2 }
out = {'foo': 0xf00, 'baz': 0xba2 }
self.assertEqual(filter_dict(inp), out)
def test_filter_dict_nested(self):
inp = {'foo': 0xf00, 'nest': {'_bar' : 0xba5}, 'baz': 0xba2 }
out = {'foo': 0xf00, 'nest': {}, 'baz': 0xba2 }
self.assertEqual(filter_dict(inp), out)
class TestUcs2Adapter(unittest.TestCase):
# the three examples from TS 102 221 Annex A
EXAMPLE1 = b'\x80\x00\x30\x00\x31\x00\x32\x00\x33'
EXAMPLE2 = b'\x81\x05\x13\x53\x95\xa6\xa6\xff\xff'
EXAMPLE3 = b'\x82\x05\x05\x30\x2d\x82\xd3\x2d\x31'
ad = Ucs2Adapter(GreedyBytes)
def test_example1_decode(self):
dec = self.ad._decode(self.EXAMPLE1, None, None)
self.assertEqual(dec, "0123")
def test_example2_decode(self):
dec = self.ad._decode(self.EXAMPLE2, None, None)
self.assertEqual(dec, "S\u0995\u09a6\u09a6\u09ff")
def test_example3_decode(self):
dec = self.ad._decode(self.EXAMPLE3, None, None)
self.assertEqual(dec, "-\u0532\u0583-1")
testdata = [
# variant 2 with only GSM alphabet characters
( "mahlzeit", '8108006d61686c7a656974' ),
# variant 2 with mixed GSM alphabet + UCS2
( "mahlzeit\u099523", '810b136d61686c7a656974953233' ),
# variant 3 due to codepoint exceeding 8 bit
( "mahl\u8023zeit", '820980236d61686c807a656974' ),
# variant 1 as there is no common codepoint pointer / prefix
( "\u3000\u2000\u1000", '80300020001000' ),
]
def test_data_decode(self):
for string, encoded_hex in self.testdata:
encoded = h2b(encoded_hex)
dec = self.ad._decode(encoded, None, None)
self.assertEqual(dec, string)
def test_data_encode(self):
for string, encoded_hex in self.testdata:
encoded = h2b(encoded_hex)
re_enc = self.ad._encode(string, None, None)
self.assertEqual(encoded, re_enc)
class TestTrailerAdapter(unittest.TestCase):
Privileges = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), security_domain=0x800000,
dap_verification=0x400000,
delegated_management=0x200000, card_lock=0x100000,
card_terminate=0x080000, card_reset=0x040000,
cvm_management=0x020000, mandated_dap_verification=0x010000,
trusted_path=0x8000, authorized_management=0x4000,
token_management=0x2000, global_delete=0x1000,
global_lock=0x0800, global_registry=0x0400,
final_application=0x0200, global_service=0x0100,
receipt_generation=0x80, ciphered_load_file_data_block=0x40,
contactless_activation=0x20, contactless_self_activation=0x10)
examples = ['00', '80', '8040', '400010']
def test_examples(self):
for e in self.examples:
dec = self.Privileges.parse(h2b(e))
reenc = self.Privileges.build(dec)
self.assertEqual(e, b2h(reenc))
if __name__ == "__main__":
unittest.main()

64
tests/unittests/test_esim.py Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import logging
import copy
from pySim.utils import h2b, b2h
from pySim.esim.saip import *
from pySim.esim.saip.personalization import *
from pprint import pprint as pp
class SaipTest(unittest.TestCase):
with open('smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der', 'rb') as f:
per_input = f.read()
pes = ProfileElementSequence.from_der(per_input)
expected_pet_list = ['header', 'mf', 'pukCodes', 'pinCodes', 'telecom', 'pinCodes', 'genericFileManagement', 'usim', 'opt-usim', 'pinCodes', 'akaParameter', 'gsm-access', 'df-5gs', 'df-saip','csim', 'opt-csim', 'pinCodes', 'cdmaParameter', 'isim', 'opt-isim', 'pinCodes', 'akaParameter', 'genericFileManagement', 'genericFileManagement', 'securityDomain', 'rfm', 'rfm', 'rfm', 'rfm', 'end']
def test_reencode_sequence(self):
"""Test that we can decode and re-encode the entire DER encoded UPP."""
reencoded_der = self.pes.to_der()
self.assertEqual(reencoded_der, self.per_input)
def test_reencode_pe(self):
"""Test that we can decode and re-encode reach individual ProfileElement."""
remainder = self.per_input
while len(remainder):
first_tlv, remainder = bertlv_first_segment(remainder)
pe = ProfileElement.from_der(first_tlv)
with self.subTest(pe.type):
reenc_tlv = pe.to_der()
self.assertEqual(reenc_tlv, first_tlv)
def test_sequence_helpers(self):
"""Verify that the convenience helpers worked as expected."""
self.assertEqual([x.type for x in self.pes.pe_list], self.expected_pet_list)
self.assertEqual(len(self.pes.pes_by_naa), 4)
def test_personalization(self):
"""Test some of the personalization operations."""
pes = copy.deepcopy(self.pes)
params = [Puk1('01234567'), Puk2(98765432), Pin1('1111'), Pin2(2222), Adm1('11111111'),
K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f'))]
for p in params:
p.validate()
p.apply(pes)
# TODO: we don't actually test the results here, but we just verify there is no exception
pes.to_der()
def test_constructor_encode(self):
"""Test that DER-encoding of PE created by "empty" constructor works without raising exception."""
for cls in [ProfileElementMF, ProfileElementPuk, ProfileElementPin, ProfileElementTelecom,
ProfileElementUSIM, ProfileElementISIM, ProfileElementAKA, ProfileElementSD,
ProfileElementSSD, ProfileElementOptUSIM, ProfileElementOptISIM,
ProfileElementHeader, ProfileElementEnd]:
with self.subTest(cls.__name__):
pes = ProfileElementSequence()
inst = cls()
pes.append(inst)
pes.to_der()
# RFM requires some constructor arguments
cls = ProfileElementRFM
with self.subTest(cls.__name__):
pes = ProfileElementSequence()
inst = cls(inst_aid=b'\x01\x02', sd_aid=b'\x03\x04', tar_list=[b'\x01\x02\x03'])
pes.append(inst)
pes.to_der()
class OidTest(unittest.TestCase):
def test_cmp(self):
self.assertTrue(oid.OID('1.0') > oid.OID('0.9'))
self.assertTrue(oid.OID('1.0') == oid.OID('1.0'))
self.assertTrue(oid.OID('1.0.1') > oid.OID('1.0'))
self.assertTrue(oid.OID('1.0.2') > oid.OID('1.0.1'))
if __name__ == "__main__":
unittest.main()

31
tests/unittests/test_euicc.py Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
import unittest
from pySim.euicc import *
class TestEid(unittest.TestCase):
def test_eid_verify(self):
for eid in ['89049032123451234512345678901235', '89086030202200000022000023022943',
'89044045116727494800000004479366', 89044045116727494800000004479366]:
self.assertTrue(verify_eid_checksum(eid))
def test_eid_verify_wrong(self):
self.assertFalse(verify_eid_checksum('89049032123451234512345678901234'))
self.assertFalse(verify_eid_checksum(89049032123451234512345678901234))
def test_eid_encode_with_32_digits(self):
self.assertEqual(compute_eid_checksum('89049032123451234512345678901200'), '89049032123451234512345678901235')
self.assertEqual(compute_eid_checksum('89086030202200000022000023022900'), '89086030202200000022000023022943')
def test_eid_encode_with_30digits(self):
self.assertEqual(compute_eid_checksum('890490321234512345123456789012'), '89049032123451234512345678901235')
def test_eid_encode_with_wrong_csum(self):
# input: EID with wrong checksum
self.assertEqual(compute_eid_checksum('89049032123451234512345678901299'), '89049032123451234512345678901235')
self.assertEqual(compute_eid_checksum(89049032123451234512345678901299), '89049032123451234512345678901235')
if __name__ == "__main__":
unittest.main()

295
tests/unittests/test_files.py Executable file
View File

@@ -0,0 +1,295 @@
#!/usr/bin/env python3
# (C) 2023 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import logging
from pySim.utils import *
from pySim.filesystem import *
import pySim.iso7816_4
import pySim.ts_102_221
import pySim.ts_102_222
import pySim.ts_31_102
import pySim.ts_31_103
import pySim.ts_51_011
import pySim.sysmocom_sja2
import pySim.gsm_r
import pySim.cdma_ruim
def get_qualified_name(c):
"""return the qualified (by module) name of a class."""
return "%s.%s" % (c.__module__, c.__name__)
class LinFixed_Test(unittest.TestCase):
classes = all_subclasses(LinFixedEF)
maxDiff = None
@staticmethod
def _parse_t(t):
"""Parse a test description which can either be a 2-tuple of (enc, dec) or
a 3-tuple of (enc, rec_nr, dec)."""
if len(t) == 2:
encoded = t[0]
rec_num = 1
decoded = t[1]
else:
encoded = t[0]
rec_num = t[1]
decoded = t[2]
return encoded, rec_num, decoded
def test_decode_record(self):
"""Test the decoder for a linear-fixed EF. Requires the given LinFixedEF subclass
to have an '_test_decode' attribute, containing a list of tuples. Each tuple can
either be a
* 2-tuple (hexstring, decoded_dict) or a
* 3-tuple (hexstring, record_nr, decoded_dict)
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
encoded, rec_num, decoded = self._parse_t(t)
with self.subTest(name, test_decode=t):
inst = c()
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded, rec_num)
self.assertEqual(decoded, re_dec)
if hasattr(c, '_test_no_pad') and c._test_no_pad:
continue
with self.subTest(name, test_decode_padded=t):
encoded = encoded + 'ff'
inst = c()
logging.debug("Testing padded decode of %s", name)
re_dec = inst.decode_record_hex(encoded, rec_num)
self.assertEqual(decoded, re_dec)
def test_encode_record(self):
"""Test the encoder for a linear-fixed EF. Requires the given LinFixedEF subclass
to have an '_test_encode' attribute, containing a list of tuples. Each tuple can
either be a
* 2-tuple (hexstring, decoded_dict) or a
* 3-tuple (hexstring, record_nr, decoded_dict)
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_encode'):
for t in c._test_encode:
with self.subTest(name, test_encode=t):
inst = c()
encoded, rec_num, decoded = self._parse_t(t)
logging.debug("Testing encode of %s", name)
re_enc = inst.encode_record_hex(decoded, rec_num)
self.assertEqual(encoded.upper(), re_enc.upper())
def test_de_encode_record(self):
"""Test the decoder and encoder for a linear-fixed EF. Performs first a decoder
test, and then re-encodes the decoded data, comparing the re-encoded data with the
initial input data.
Requires the given LinFixedEF subclass to have a '_test_de_encode' attribute,
containing a list of tuples. Each tuple can
either be a
* 2-tuple (hexstring, decoded_dict) or a
* 3-tuple (hexstring, record_nr, decoded_dict)
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_de_encode'):
for t in c._test_de_encode:
encoded, rec_num, decoded = self._parse_t(t)
with self.subTest(name, test_de_encode=t):
inst = c()
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded, rec_num)
self.assertEqual(decoded, re_dec)
# re-encode the decoded data
logging.debug("Testing re-encode of %s", name)
re_enc = inst.encode_record_hex(re_dec, rec_num)
self.assertEqual(encoded.upper(), re_enc.upper())
if hasattr(c, '_test_no_pad') and c._test_no_pad:
continue
with self.subTest(name, test_decode_padded=t):
encoded = encoded + 'ff'
inst = c()
logging.debug("Testing padded decode of %s", name)
re_dec = inst.decode_record_hex(encoded, rec_num)
self.assertEqual(decoded, re_dec)
class TransRecEF_Test(unittest.TestCase):
classes = all_subclasses(TransRecEF)
maxDiff = None
def test_decode_record(self):
"""Test the decoder for a transparent record-oriented EF. Requires the given TransRecEF subclass
to have an '_test_decode' attribute, containing a list of tuples. Each tuple has to be a
2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
with self.subTest(name, test_decode=t):
inst = c()
encoded = t[0]
decoded = t[1]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded)
self.assertEqual(decoded, re_dec)
# there's no point in testing padded input, as TransRecEF have a fixed record
# size and we cannot ever receive more input data than that size.
def test_encode_record(self):
"""Test the encoder for a transparent record-oriented EF. Requires the given TransRecEF subclass
to have an '_test_encode' attribute, containing a list of tuples. Each tuple has to be a
2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
with self.subTest(name, test_decode=t):
inst = c()
encoded = t[0]
decoded = t[1]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded)
self.assertEqual(decoded, re_dec)
def test_de_encode_record(self):
"""Test the decoder and encoder for a transparent record-oriented EF. Performs first a decoder
test, and then re-encodes the decoded data, comparing the re-encoded data with the
initial input data.
Requires the given TransRecEF subclass to have a '_test_de_encode' attribute,
containing a list of tuples. Each tuple has to be a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_de_encode'):
for t in c._test_de_encode:
with self.subTest(name, test_de_encode=t):
inst = c()
encoded = t[0]
decoded = t[1]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded)
self.assertEqual(decoded, re_dec)
# re-encode the decoded data
logging.debug("Testing re-encode of %s", name)
re_enc = inst.encode_record_hex(re_dec)
self.assertEqual(encoded.upper(), re_enc.upper())
# there's no point in testing padded input, as TransRecEF have a fixed record
# size and we cannot ever receive more input data than that size.
class TransparentEF_Test(unittest.TestCase):
maxDiff = None
@classmethod
def get_classes(cls):
"""get list of TransparentEF sub-classes which are not a TransRecEF subclass."""
classes = all_subclasses(TransparentEF)
trans_rec_classes = all_subclasses(TransRecEF)
return filter(lambda c: c not in trans_rec_classes, classes)
@classmethod
def setUpClass(cls):
"""set-up method called once for this class by unittest framework"""
cls.classes = cls.get_classes()
def test_decode_file(self):
"""Test the decoder for a transparent EF. Requires the given TransparentEF subclass
to have a '_test_decode' attribute, containing a list of tuples. Each tuple
is a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
encoded = t[0]
decoded = t[1]
with self.subTest(name, test_decode=t):
inst = c()
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
if hasattr(c, '_test_no_pad') and c._test_no_pad:
continue
with self.subTest(name, test_decode_padded=t):
encoded = encoded + 'ff'
inst = c()
logging.debug("Testing padded decode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
def test_encode_file(self):
"""Test the encoder for a transparent EF. Requires the given TransparentEF subclass
to have a '_test_encode' attribute, containing a list of tuples. Each tuple
is a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_encode'):
for t in c._test_encode:
with self.subTest(name, test_encode=t):
inst = c()
encoded = t[0]
decoded = t[1]
logging.debug("Testing encode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
def test_de_encode_file(self):
"""Test the decoder and encoder for a transparent EF. Performs first a decoder
test, and then re-encodes the decoded data, comparing the re-encoded data with the
initial input data.
Requires the given TransparentEF subclass to have a '_test_de_encode' attribute,
containing a list of tuples. Each tuple is a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_de_encode'):
for t in c._test_de_encode:
encoded = t[0]
decoded = t[1]
with self.subTest(name, test_de_encode=t):
inst = c()
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
logging.debug("Testing re-encode of %s", name)
re_dec = inst.decode_hex(encoded)
re_enc = inst.encode_hex(re_dec)
self.assertEqual(encoded.upper(), re_enc.upper())
if hasattr(c, '_test_no_pad') and c._test_no_pad:
continue
with self.subTest(name, test_decode_padded=t):
encoded = encoded + 'ff'
inst = c()
logging.debug("Testing padded decode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
if __name__ == '__main__':
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
unittest.main()

View File

@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import logging
from pySim.global_platform import *
from pySim.global_platform.scp import *
from pySim.utils import b2h, h2b
KIC = h2b('100102030405060708090a0b0c0d0e0f') # enc
KID = h2b('101102030405060708090a0b0c0d0e0f') # MAC
KIK = h2b('102102030405060708090a0b0c0d0e0f') # DEK
ck_3des_70 = GpCardKeyset(0x20, KIC, KID, KIK)
class SCP02_Auth_Test(unittest.TestCase):
host_challenge = h2b('40A62C37FA6304F8')
init_update_resp = h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')
def setUp(self):
self.scp02 = SCP02(card_keys=ck_3des_70)
def test_mutual_auth_success(self):
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8')
self.scp02.parse_init_update_resp(self.init_update_resp)
ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
self.assertEqual(b2h(ext_auth_cmd).upper(), '8482010010BA6961667737C5BCEBECE14C7D6A4376')
def test_mutual_auth_fail_card_cryptogram(self):
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8')
wrong_init_update_resp = self.init_update_resp.copy()
wrong_init_update_resp[-1:] = b'\xff'
with self.assertRaises(ValueError):
self.scp02.parse_init_update_resp(wrong_init_update_resp)
class SCP02_Test(unittest.TestCase):
host_challenge = h2b('40A62C37FA6304F8')
init_update_resp = h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')
def setUp(self):
self.scp02 = SCP02(card_keys=ck_3des_70)
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.scp02.parse_init_update_resp(self.init_update_resp)
ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
def test_mac_command(self):
wrapped = self.scp02.wrap_cmd_apdu(h2b('80f28002024f00'))
self.assertEqual(b2h(wrapped).upper(), '84F280020A4F00B21AAFA3EB2D1672')
class SCP03_Test:
"""some kind of 'abstract base class' for a unittest.UnitTest, implementing common functionality for all
of our SCP03 test caseses."""
get_eid_cmd_plain = h2b('80E2910006BF3E035C015A')
get_eid_rsp_plain = h2b('bf3e125a1089882119900000000000000000000005')
# must be overridden by derived classes
init_upd_cmd = b''
init_upd_rsp = b''
ext_auth_cmd = b''
get_eid_cmd = b''
get_eid_rsp = b''
keyset = None
@property
def host_challenge(self) -> bytes:
return self.init_upd_cmd[5:]
@property
def kvn(self) -> int:
return self.init_upd_cmd[2]
@property
def security_level(self) -> int:
return self.ext_auth_cmd[2]
@property
def card_challenge(self) -> bytes:
if len(self.init_upd_rsp) in [10+3+8+8, 10+3+8+8+3]:
return self.init_upd_rsp[10+3:10+3+8]
else:
return self.init_upd_rsp[10+3:10+3+16]
@property
def card_cryptogram(self) -> bytes:
if len(self.init_upd_rsp) in [10+3+8+8, 10+3+8+8+3]:
return self.init_upd_rsp[10+3+8:10+3+8+8]
else:
return self.init_upd_rsp[10+3+16:10+3+16+16]
@classmethod
def setUpClass(cls):
cls.scp = SCP03(card_keys = cls.keyset)
def test_01_initialize_update(self):
# pylint: disable=no-member
self.assertEqual(self.init_upd_cmd, self.scp.gen_init_update_apdu(self.host_challenge))
def test_02_parse_init_upd_resp(self):
self.scp.parse_init_update_resp(self.init_upd_rsp)
def test_03_gen_ext_auth_apdu(self):
# pylint: disable=no-member
self.assertEqual(self.ext_auth_cmd, self.scp.gen_ext_auth_apdu(self.security_level))
def test_04_wrap_cmd_apdu_get_eid(self):
# pylint: disable=no-member
self.assertEqual(self.get_eid_cmd, self.scp.wrap_cmd_apdu(self.get_eid_cmd_plain))
def test_05_unwrap_rsp_apdu_get_eid(self):
# pylint: disable=no-member
self.assertEqual(self.get_eid_rsp_plain, self.scp.unwrap_rsp_apdu(h2b('9000'), self.get_eid_rsp))
# The SCP03 keysets used for various key lenghs
KEYSET_AES128 = GpCardKeyset(0x30, h2b('000102030405060708090a0b0c0d0e0f'), h2b('101112131415161718191a1b1c1d1e1f'), h2b('202122232425262728292a2b2c2d2e2f'))
KEYSET_AES192 = GpCardKeyset(0x31, h2b('000102030405060708090a0b0c0d0e0f0001020304050607'),
h2b('101112131415161718191a1b1c1d1e1f1011121314151617'), h2b('202122232425262728292a2b2c2d2e2f2021222324252627'))
KEYSET_AES256 = GpCardKeyset(0x32, h2b('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f'),
h2b('101112131415161718191a1b1c1d1e1f101112131415161718191a1b1c1d1e1f'),
h2b('202122232425262728292a2b2c2d2e2f202122232425262728292a2b2c2d2e2f'))
class SCP03_Test_AES128_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128
init_upd_cmd = h2b('8050300008b13e5f938fc108c4')
init_upd_rsp = h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002')
ext_auth_cmd = h2b('84821100107d5f5826a993ebc89eea24957fa0b3ce')
get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a28297')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005971be68992dbbdfa')
class SCP03_Test_AES128_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128
init_upd_cmd = h2b('80503000088e1552d0513c60f3')
init_upd_rsp = h2b('0000000000000000000030037030760cd2c47c1dd395065fe5ead8a9d7000001')
ext_auth_cmd = h2b('8482030010fd4721a14d9b07003c451d2f8ae6bb21')
get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
class SCP03_Test_AES128_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128
init_upd_cmd = h2b('8050300008fdf38259a1e0de44')
init_upd_rsp = h2b('000000000000000000003003703b1aca81e821f219081cdc01c26b372d000003')
ext_auth_cmd = h2b('84823300108c36f96bcc00724a4e13ad591d7da3f0')
get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f')
get_eid_rsp = h2b('f3ba2b1013aa6224f5e1c138d71805c569e5439b47576260b75fc021b25097cb2e68f8a0144975b9')
class SCP03_Test_AES192_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192
init_upd_cmd = h2b('80503100087396430b768b085b')
init_upd_rsp = h2b('000000000000000000003103708cfc23522ffdbf1e5df5542cac8fd866000003')
ext_auth_cmd = h2b('84821100102145ed30b146f5db252fb7e624cec244')
get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d143944')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005162fbd33e04940a9')
class SCP03_Test_AES192_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192
init_upd_cmd = h2b('805031000869c65da8202bf19f')
init_upd_rsp = h2b('00000000000000000000310370b570a67be38446717729d6dd3d2ec5b1000001')
ext_auth_cmd = h2b('848203001065df4f1a356a887905466516d9e5b7c1')
get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
class SCP03_Test_AES192_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192
init_upd_cmd = h2b('80503100089b3f2eef0e8c9374')
init_upd_rsp = h2b('00000000000000000000310370f6bb305a15bae1a68f79fb08212fbed7000002')
ext_auth_cmd = h2b('84823300109100bc22d58b45b86a26365ce39ff3cf')
get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de589')
get_eid_rsp = h2b('c05176c1b6f72aae50c32cbee63b0e95998928fd4dfb2be9f27ffde8c8476f5909b4805cc4039599')
class SCP03_Test_AES256_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256
init_upd_cmd = h2b('805032000811666d57866c6f54')
init_upd_rsp = h2b('0000000000000000000032037053ea8847efa7674e41498a4d66cf0dee000003')
ext_auth_cmd = h2b('84821100102f2ad190eff2fafc4908996d1cebd310')
get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d')
get_eid_rsp = h2b('bf3e125a10898821199000000000000000000000058012dd7f01f1c4c1')
class SCP03_Test_AES256_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256
init_upd_cmd = h2b('8050320008c6066990fc426e1d')
init_upd_rsp = h2b('000000000000000000003203708682cd81bbd8919f2de3f2664581f118000001')
ext_auth_cmd = h2b('848203001077c493b632edadaf865a1e64acc07ce9')
get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe44')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256
init_upd_cmd = h2b('805032000897b2055fe58599fd')
init_upd_rsp = h2b('00000000000000000000320370a8439a22cedf045fa9f1903b2834f26e000002')
ext_auth_cmd = h2b('8482330010508a0fd959d2e547c6b33154a6be2057')
get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea')
get_eid_rsp = h2b('ea90dbfa648a67c5eb6abc57f8530b97d0cd5647c5e8732016b55203b078dd2ace7f8bc5d1c1cd99')
# FIXME:
# - for S8 and S16 mode
# FIXME: test auth with random (0x60) vs pseudo-random (0x70) challenge
class SCP03_KCV_Test(unittest.TestCase):
def test_kcv(self):
self.assertEqual(compute_kcv('aes', KEYSET_AES128.enc), h2b('C35280'))
self.assertEqual(compute_kcv('aes', KEYSET_AES128.mac), h2b('013808'))
self.assertEqual(compute_kcv('aes', KEYSET_AES128.dek), h2b('840DE5'))
if __name__ == "__main__":
unittest.main()

304
tests/unittests/test_ota.py Normal file
View File

@@ -0,0 +1,304 @@
#!/usr/bin/env python3
import unittest
from pySim.utils import h2b, b2h
from pySim.sms import SMS_SUBMIT, SMS_DELIVER, AddressField
from pySim.ota import *
# pre-defined SPI values for use in test cases below
SPI_CC_POR_CIPHERED_CC = {
'counter':'no_counter',
'ciphering':True,
'rc_cc_ds': 'cc',
'por_in_submit':False,
'por_shall_be_ciphered':True,
'por_rc_cc_ds': 'cc',
'por': 'por_required'
}
SPI_CC_POR_UNCIPHERED_CC = {
'counter':'no_counter',
'ciphering':True,
'rc_cc_ds': 'cc',
'por_in_submit':False,
'por_shall_be_ciphered':False,
'por_rc_cc_ds': 'cc',
'por': 'por_required'
}
SPI_CC_POR_UNCIPHERED_NOCC = {
'counter':'no_counter',
'ciphering':True,
'rc_cc_ds': 'cc',
'por_in_submit':False,
'por_shall_be_ciphered':False,
'por_rc_cc_ds': 'no_rc_cc_ds',
'por': 'por_required'
}
######################################################################
# old-style code-driven test (lots of code copy+paste)
######################################################################
class Test_SMS_AES128(unittest.TestCase):
tar = h2b('B00011')
"""Test the OtaDialectSms for AES128 algorithms."""
def __init__(self, foo, **kwargs):
super().__init__(foo, **kwargs)
self.od = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2,
algo_auth='aes_cmac', kid_idx=2,
kic=h2b('200102030405060708090a0b0c0d0e0f'),
kid=h2b('201102030405060708090a0b0c0d0e0f'))
self.dialect = OtaDialectSms()
self.spi_base = SPI_CC_POR_CIPHERED_CC
def _check_response(self, r, d):
self.assertEqual(d['number_of_commands'], 1)
self.assertEqual(d['last_status_word'], '6132')
self.assertEqual(d['last_response_data'], u'')
self.assertEqual(r['response_status'], 'por_ok')
def test_resp_aes128_ciphered(self):
spi = self.spi_base
r, d = self.dialect.decode_resp(self.od, spi, '027100002412b00011ebc6b497e2cad7aedf36ace0e3a29b38853f0fe9ccde81913be5702b73abce1f')
self._check_response(r, d)
def test_cmd_aes128_ciphered(self):
spi = self.spi_base
apdu = h2b('00a40004023f00')
r = self.dialect.encode_cmd(self.od, self.tar, spi, apdu)
self.assertEqual(b2h(r), '00281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058')
# also test decoder
dec_tar, dec_spi, dec_apdu = self.dialect.decode_cmd(self.od, r)
self.assertEqual(b2h(apdu), b2h(dec_apdu))
self.assertEqual(b2h(dec_tar), b2h(self.tar))
self.assertEqual(dec_spi, spi)
class Test_SMS_3DES(unittest.TestCase):
tar = h2b('b00000')
apdu = h2b('00a40000023f00')
"""Test the OtaDialectSms for 3DES algorithms."""
def __init__(self, foo, **kwargs):
super().__init__(foo, **kwargs)
# KIC1 + KID1 of 8988211000000467285
KIC1 = h2b('D0FDA31990D8D64178601317191669B4')
KID1 = h2b('D24EB461799C5E035C77451FD9404463')
KIC3 = h2b('C21DD66ACAC13CB3BC8B331B24AFB57B')
KID3 = h2b('12110C78E678C25408233076AA033615')
self.od = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3, kic=KIC3,
algo_auth='triple_des_cbc2', kid_idx=3, kid=KID3)
self.dialect = OtaDialectSms()
self.spi_base = {
'counter':'no_counter',
'ciphering': True,
'rc_cc_ds': 'cc',
'por_in_submit':False,
'por': 'por_required',
'por_shall_be_ciphered': True,
'por_rc_cc_ds': 'cc',
}
def _check_response(self, r, d):
self.assertEqual(d['number_of_commands'], 1)
self.assertEqual(d['last_status_word'], '612f')
self.assertEqual(d['last_response_data'], u'')
self.assertEqual(r['response_status'], 'por_ok')
def test_resp_3des_ciphered(self):
spi = self.spi_base
spi['por_shall_be_ciphered'] = True
spi['por_rc_cc_ds'] = 'cc'
r, d = self.dialect.decode_resp(self.od, spi, '027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c')
self._check_response(r, d)
def test_resp_3des_signed(self):
spi = self.spi_base
spi['por_shall_be_ciphered'] = False
spi['por_rc_cc_ds'] = 'cc'
r, d = self.dialect.decode_resp(self.od, spi, '027100001612b000110000000000000055f47118381175fb01612f')
self._check_response(r, d)
def test_resp_3des_signed_err(self):
"""Expect an OtaCheckError exception if the computed CC != received CC"""
spi = self.spi_base
spi['por_shall_be_ciphered'] = False
spi['por_rc_cc_ds'] = 'cc'
with self.assertRaises(OtaCheckError) as context:
r, d = self.dialect.decode_resp(self.od, spi, '027100001612b000110000000000000055f47118381175fb02612f')
self.assertTrue('!= Computed CC' in str(context.exception))
def test_resp_3des_none(self):
spi = self.spi_base
spi['por_shall_be_ciphered'] = False
spi['por_rc_cc_ds'] = 'no_rc_cc_ds'
r, d = self.dialect.decode_resp(self.od, spi, '027100000e0ab000110000000000000001612f')
self._check_response(r, d)
def test_cmd_3des_ciphered(self):
spi = self.spi_base
spi['ciphering'] = True
spi['rc_cc_ds'] = 'no_rc_cc_ds'
r = self.dialect.encode_cmd(self.od, self.tar, spi, self.apdu)
self.assertEqual(b2h(r), '00180d04193535b00000e3ec80a849b554421276af3883927c20')
# also test decoder
dec_tar, dec_spi, dec_apdu = self.dialect.decode_cmd(self.od, r)
self.assertEqual(b2h(self.apdu), b2h(dec_apdu))
self.assertEqual(b2h(dec_tar), b2h(self.tar))
self.assertEqual(dec_spi, spi)
def test_cmd_3des_signed(self):
spi = self.spi_base
spi['ciphering'] = False
spi['rc_cc_ds'] = 'cc'
r = self.dialect.encode_cmd(self.od, self.tar, spi, self.apdu)
self.assertEqual(b2h(r), '1502193535b00000000000000000072ea17bdb72060e00a40000023f00')
def test_cmd_3des_none(self):
spi = self.spi_base
spi['ciphering'] = False
spi['rc_cc_ds'] = 'no_rc_cc_ds'
r = self.dialect.encode_cmd(self.od, self.tar, spi, self.apdu)
self.assertEqual(b2h(r), '0d00193535b0000000000000000000a40000023f00')
######################################################################
# new-style data-driven tests
######################################################################
# SJA5 SAMPLE cards provisioned by execute_ipr.py
OTA_KEYSET_SJA5_SAMPLES = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3,
algo_auth='triple_des_cbc2', kid_idx=3,
kic=h2b('300102030405060708090a0b0c0d0e0f'),
kid=h2b('301102030405060708090a0b0c0d0e0f'))
OTA_KEYSET_SJA5_AES128 = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2,
algo_auth='aes_cmac', kid_idx=2,
kic=h2b('200102030405060708090a0b0c0d0e0f'),
kid=h2b('201102030405060708090a0b0c0d0e0f'))
class OtaTestCase(unittest.TestCase):
def __init__(self, methodName='runTest', **kwargs):
super().__init__(methodName, **kwargs)
# RAM: B00000
# SIM RFM: B00010
# USIM RFM: B00011
self.tar = h2b('B00011')
class SmsOtaTestCase(OtaTestCase):
# Array describing the input/output data for the tests. We use the
# unittest subTests context manager to iterate over the entries of
# this testdatasets list. This is much more productive than
# manually writing one class per test.
testdatasets = [
{
'name': '3DES-SJA5-CIPHERED-CC',
'ota_keyset': OTA_KEYSET_SJA5_SAMPLES,
'spi': SPI_CC_POR_CIPHERED_CC,
'request': {
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
'encoded_cmd': '00201506193535b00011ae733256918d050b87c94fbfe12e4dc402f262c41cf67f2f',
'encoded_tpdu': '400881214365877ff6227052000000000302700000201506193535b00011ae733256918d050b87c94fbfe12e4dc402f262c41cf67f2f',
},
'response': {
'encoded_resp': '027100001c12b000118bb989492c632529326a2f4681feb37c825bc9021c9f6d0b',
'response_status': 'por_ok',
'number_of_commands': 1,
'last_status_word': '6132',
'last_response_data': '',
}
}, {
'name': '3DES-SJA5-UNCIPHERED-CC',
'ota_keyset': OTA_KEYSET_SJA5_SAMPLES,
'spi': SPI_CC_POR_UNCIPHERED_CC,
'request': {
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
'encoded_cmd': '00201506093535b00011c49ac91ab8159ba5b83a54fb6385e0a5e31694f8b215fafc',
'encoded_tpdu': '400881214365877ff6227052000000000302700000201506093535b00011c49ac91ab8159ba5b83a54fb6385e0a5e31694f8b215fafc',
},
'response': {
'encoded_resp': '027100001612b0001100000000000000b5bcd6353a421fae016132',
'response_status': 'por_ok',
'number_of_commands': 1,
'last_status_word': '6132',
'last_response_data': '',
}
}, {
'name': '3DES-SJA5-UNCIPHERED-NOCC',
'ota_keyset': OTA_KEYSET_SJA5_SAMPLES,
'spi': SPI_CC_POR_UNCIPHERED_NOCC,
'request': {
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
'encoded_cmd': '00201506013535b000113190be334900f52b025f3f7eddfe868e96ebf310023b7769',
'encoded_tpdu': '400881214365877ff6227052000000000302700000201506013535b000113190be334900f52b025f3f7eddfe868e96ebf310023b7769',
},
'response': {
'encoded_resp': '027100000e0ab0001100000000000000016132',
'response_status': 'por_ok',
'number_of_commands': 1,
'last_status_word': '6132',
'last_response_data': '',
}
}, {
'name': 'AES128-SJA5-CIPHERED-CC',
'ota_keyset': OTA_KEYSET_SJA5_AES128,
'spi': SPI_CC_POR_CIPHERED_CC,
'request': {
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
'encoded_cmd': '00281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058',
'encoded_tpdu': '400881214365877ff6227052000000000302700000281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058',
},
'response': {
'encoded_resp': '027100002412b00011ebc6b497e2cad7aedf36ace0e3a29b38853f0fe9ccde81913be5702b73abce1f',
'response_status': 'por_ok',
'number_of_commands': 1,
'last_status_word': '6132',
'last_response_data': '',
}
},
# TODO: AES192
# TODO: AES256
]
def __init__(self, methodName='runTest', **kwargs):
super().__init__(methodName, **kwargs)
self.dialect = OtaDialectSms()
self.da = AddressField('12345678', 'unknown', 'isdn_e164')
def test_encode_cmd(self):
for t in SmsOtaTestCase.testdatasets:
with self.subTest(name=t['name']):
kset = t['ota_keyset']
outp = self.dialect.encode_cmd(kset, self.tar, t['spi'], apdu=t['request']['apdu'])
#print("result: %s" % b2h(outp))
self.assertEqual(b2h(outp), t['request']['encoded_cmd'])
with_udh = b'\x02\x70\x00' + outp
#print("with_udh: %s" % b2h(with_udh))
tpdu = SMS_DELIVER(tp_udhi=True, tp_oa=self.da, tp_pid=0x7F, tp_dcs=0xF6,
tp_scts=h2b('22705200000000'), tp_udl=3, tp_ud=with_udh)
#print("TPDU: %s" % tpdu)
#print("tpdu: %s" % b2h(tpdu.to_bytes()))
self.assertEqual(b2h(tpdu.to_bytes()), t['request']['encoded_tpdu'])
# also test decoder
dec_tar, dec_spi, dec_apdu = self.dialect.decode_cmd(kset, outp)
self.assertEqual(b2h(t['request']['apdu']), b2h(dec_apdu))
self.assertEqual(b2h(dec_tar), b2h(self.tar))
self.assertEqual(dec_spi, t['spi'])
def test_decode_resp(self):
for t in SmsOtaTestCase.testdatasets:
with self.subTest(name=t['name']):
kset = t['ota_keyset']
r, d = self.dialect.decode_resp(kset, t['spi'], t['response']['encoded_resp'])
#print("RESP: %s / %s" % (r, d))
self.assertEqual(r.response_status, t['response']['response_status'])
self.assertEqual(d.number_of_commands, t['response']['number_of_commands'])
self.assertEqual(d.last_status_word, t['response']['last_status_word'])
self.assertEqual(d.last_response_data, t['response']['last_response_data'])
if __name__ == "__main__":
unittest.main()

105
tests/unittests/test_sms.py Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import unittest
from pySim.utils import h2b, b2h
from pySim.sms import *
class Test_SMS_UDH(unittest.TestCase):
def test_single_ie(self):
udh, tail = UserDataHeader.from_bytes('027100')
self.assertEqual(len(udh.ies), 1)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
self.assertEqual(ie.length, 0)
self.assertEqual(ie.value, b'')
self.assertEqual(tail, b'')
def test_single_ie_tail(self):
udh, tail = UserDataHeader.from_bytes('027100abcdef')
self.assertEqual(len(udh.ies), 1)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
self.assertEqual(ie.length, 0)
self.assertEqual(ie.value, b'')
self.assertEqual(tail, b'\xab\xcd\xef')
def test_single_ie_value(self):
udh, tail = UserDataHeader.from_bytes('03710110')
self.assertEqual(len(udh.ies), 1)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
self.assertEqual(ie.length, 1)
self.assertEqual(ie.value, b'\x10')
self.assertEqual(tail, b'')
def test_two_ie_data_tail(self):
udh, tail = UserDataHeader.from_bytes('0571007001ffabcd')
self.assertEqual(len(udh.ies), 2)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
self.assertEqual(ie.length, 0)
self.assertEqual(ie.value, b'')
ie = udh.ies[1]
self.assertEqual(ie.iei, 0x70)
self.assertEqual(ie.length, 1)
self.assertEqual(ie.value, b'\xff')
self.assertEqual(tail, b'\xab\xcd')
def test_to_bytes(self):
indata = h2b('0571007001ff')
udh, tail = UserDataHeader.from_bytes(indata)
encoded = udh.to_bytes()
self.assertEqual(encoded, indata)
class Test_AddressField(unittest.TestCase):
def test_from_bytes(self):
encoded = h2b('0480214399')
af, trailer = AddressField.from_bytes(encoded)
self.assertEqual(trailer, b'\x99')
self.assertEqual(af.ton, 'unknown')
self.assertEqual(af.npi, 'unknown')
self.assertEqual(af.digits, '1234')
def test_from_bytes_odd(self):
af, trailer = AddressField.from_bytes('038021f399')
self.assertEqual(trailer, b'\x99')
self.assertEqual(af.ton, 'unknown')
self.assertEqual(af.npi, 'unknown')
self.assertEqual(af.digits, '123')
def test_to_bytes(self):
encoded = h2b('04802143')
af, trailer = AddressField.from_bytes(encoded)
self.assertEqual(af.to_bytes(), encoded)
def test_to_bytes_odd(self):
af = AddressField('12345', 'international', 'isdn_e164')
encoded = af.to_bytes()
self.assertEqual(encoded, h2b('05912143f5'))
class Test_SUBMIT(unittest.TestCase):
def test_from_bytes(self):
s = SMS_SUBMIT.from_bytes('550d0b911614261771f000f5a78c0b050423f423f40003010201424547494e3a56434152440d0a56455253494f4e3a322e310d0a4e3a4d650d0a54454c3b505245463b43454c4c3b564f4943453a2b36313431363237313137300d0a54454c3b484f4d453b564f4943453a2b36313339353337303437310d0a54454c3b574f524b3b564f4943453a2b36313339363734373031350d0a454e443a')
self.assertEqual(s.tp_mti, 1)
self.assertEqual(s.tp_rd, True)
self.assertEqual(s.tp_vpf, 'relative')
self.assertEqual(s.tp_rp, False)
self.assertEqual(s.tp_udhi, True)
self.assertEqual(s.tp_srr, False)
self.assertEqual(s.tp_pid, 0)
self.assertEqual(s.tp_dcs, 0xf5)
self.assertEqual(s.tp_udl, 140)
class Test_DELIVER(unittest.TestCase):
def test_from_bytes(self):
d = SMS_DELIVER.from_bytes('0408D0E5759A0E7FF6907090307513000824010101BB400101')
self.assertEqual(d.tp_mti, 0)
self.assertEqual(d.tp_mms, True)
self.assertEqual(d.tp_lp, False)
self.assertEqual(d.tp_rp, False)
self.assertEqual(d.tp_udhi, False)
self.assertEqual(d.tp_sri, False)
self.assertEqual(d.tp_pid, 0x7f)
self.assertEqual(d.tp_dcs, 0xf6)
self.assertEqual(d.tp_udl, 8)

145
tests/unittests/test_tlv.py Normal file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
# (C) 2022 by Harald Welte <laforge@osmocom.org>
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from construct import Int8ub, GreedyBytes
from pySim.tlv import *
from pySim.utils import h2b
class TestUtils(unittest.TestCase):
def test_camel_to_snake(self):
cases = [
('CamelCase', 'camel_case'),
('CamelCaseUPPER', 'camel_case_upper'),
('Camel_CASE_underSCORE', 'camel_case_under_score'),
]
for c in cases:
self.assertEqual(camel_to_snake(c[0]), c[1])
def test_flatten_dict_lists(self):
inp = [
{ 'first': 1 },
{ 'second': 2 },
{ 'third': 3 },
]
out = { 'first': 1, 'second':2, 'third': 3}
self.assertEqual(flatten_dict_lists(inp), out)
def test_flatten_dict_lists_nodict(self):
inp = [
{ 'first': 1 },
{ 'second': 2 },
{ 'third': 3 },
4,
]
self.assertEqual(flatten_dict_lists(inp), inp)
def test_flatten_dict_lists_nested(self):
inp = {'top': [
{ 'first': 1 },
{ 'second': 2 },
{ 'third': 3 },
] }
out = {'top': { 'first': 1, 'second':2, 'third': 3 } }
self.assertEqual(flatten_dict_lists(inp), out)
class TestTranscodable(unittest.TestCase):
class XC_constr_class(Transcodable):
_construct = Int8ub
def __init__(self):
super().__init__();
def test_XC_constr_class(self):
"""Transcodable derived class with _construct class variable"""
xc = TestTranscodable.XC_constr_class()
self.assertEqual(xc.from_bytes(b'\x23'), 35)
self.assertEqual(xc.to_bytes(), b'\x23')
class XC_constr_instance(Transcodable):
def __init__(self):
super().__init__();
self._construct = Int8ub
def test_XC_constr_instance(self):
"""Transcodable derived class with _construct instance variable"""
xc = TestTranscodable.XC_constr_instance()
self.assertEqual(xc.from_bytes(b'\x23'), 35)
self.assertEqual(xc.to_bytes(), b'\x23')
class XC_method_instance(Transcodable):
def __init__(self):
super().__init__();
def _from_bytes(self, do):
return ('decoded', do)
def _to_bytes(self):
return self.decoded[1]
def test_XC_method_instance(self):
"""Transcodable derived class with _{from,to}_bytes() methods"""
xc = TestTranscodable.XC_method_instance()
self.assertEqual(xc.to_bytes(), b'')
self.assertEqual(xc.from_bytes(b''), None)
self.assertEqual(xc.from_bytes(b'\x23'), ('decoded', b'\x23'))
self.assertEqual(xc.to_bytes(), b'\x23')
class TestIE(unittest.TestCase):
class MyIE(IE, tag=0x23, desc='My IE description'):
_construct = Int8ub
def to_ie(self):
return self.to_bytes()
def test_IE_empty(self):
ie = TestIE.MyIE()
self.assertEqual(ie.to_dict(), {'my_ie': None})
self.assertEqual(repr(ie), 'MyIE(None)')
self.assertEqual(ie.is_constructed(), False)
def test_IE_from_bytes(self):
ie = TestIE.MyIE()
ie.from_bytes(b'\x42')
self.assertEqual(ie.to_dict(), {'my_ie': 66})
self.assertEqual(repr(ie), 'MyIE(66)')
self.assertEqual(ie.is_constructed(), False)
self.assertEqual(ie.to_bytes(), b'\x42')
self.assertEqual(ie.to_ie(), b'\x42')
class TestCompact(unittest.TestCase):
class IE_3(COMPACT_TLV_IE, tag=0x3):
_construct = GreedyBytes
class IE_7(COMPACT_TLV_IE, tag=0x7):
_construct = GreedyBytes
class IE_5(COMPACT_TLV_IE, tag=0x5):
_construct = GreedyBytes
# pylint: disable=undefined-variable
class IE_Coll(TLV_IE_Collection, nested=[IE_3, IE_7, IE_5]):
_construct = GreedyBytes
def test_ATR(self):
atr = h2b("31E073FE211F5745437531301365")
c = self.IE_Coll()
c.from_tlv(atr)
self.assertEqual(c.children[0].tag, 3)
self.assertEqual(c.children[0].to_bytes(), b'\xe0')
self.assertEqual(c.children[1].tag, 7)
self.assertEqual(c.children[1].to_bytes(), b'\xfe\x21\x1f')
self.assertEqual(c.children[2].tag, 5)
self.assertEqual(c.children[2].to_bytes(), b'\x45\x43\x75\x31\x30\x13\x65')
self.assertEqual(c.to_tlv(), atr)
if __name__ == "__main__":
unittest.main()

124
tests/unittests/test_tlvs.py Executable file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# (C) 2023 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import logging
from pySim.utils import b2h, h2b, all_subclasses
from pySim.tlv import *
import pySim.iso7816_4
import pySim.ts_102_221
import pySim.ts_102_222
import pySim.ts_31_102
import pySim.ts_31_103
import pySim.ts_51_011
import pySim.sysmocom_sja2
import pySim.gsm_r
import pySim.cdma_ruim
import pySim.global_platform
import pySim.global_platform.http
if 'unittest.util' in __import__('sys').modules:
# Show full diff in self.assertEqual.
__import__('sys').modules['unittest.util']._MAX_LENGTH = 999999999
def get_qualified_name(c):
"""return the qualified (by module) name of a class."""
return "%s.%s" % (c.__module__, c.__name__)
class TLV_IE_Test(unittest.TestCase):
maxDiff = None
@classmethod
def get_classes(cls):
"""get list of TLV_IE sub-classes."""
return all_subclasses(TLV_IE)
@classmethod
def setUpClass(cls):
"""set-up method called once for this class by unittest framework"""
cls.classes = cls.get_classes()
def test_decode_tlv(self):
"""Test the decoder for a TLV_IE. Requires the given TLV_IE subclass
to have a '_test_decode' attribute, containing a list of tuples. Each tuple
is a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
with self.subTest(name, test_decode=t):
inst = c()
encoded = t[0]
decoded = { camel_to_snake(c.__name__): t[1] }
context = t[2] if len(t) == 3 else {}
logging.debug("Testing decode of %s", name)
inst.from_tlv(h2b(encoded), context=context)
re_dec = inst.to_dict()
self.assertEqual(decoded, re_dec)
def test_encode_tlv(self):
"""Test the encoder for a TLV_IE. Requires the given TLV_IE subclass
to have a '_test_encode' attribute, containing a list of tuples. Each tuple
is a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_encode'):
for t in c._test_encode:
with self.subTest(name, test_encode=t):
inst = c()
encoded = t[0]
decoded = { camel_to_snake(c.__name__): t[1] }
context = t[2] if len(t) == 3 else {}
logging.debug("Testing encode of %s", name)
inst.from_dict(decoded)
re_enc = b2h(inst.to_tlv(context))
self.assertEqual(encoded.upper(), re_enc.upper())
def test_de_encode_tlv(self):
"""Test the decoder and encoder for a TLV_IE. Performs first a decoder
test, and then re-encodes the decoded data, comparing the re-encoded data with the
initial input data.
Requires the given TLV_IE subclass to have a '_test_de_encode' attribute,
containing a list of tuples. Each tuple is a 2-tuple (hexstring, decoded_dict).
"""
for c in self.classes:
name = get_qualified_name(c)
if hasattr(c, '_test_de_encode'):
for t in c._test_de_encode:
with self.subTest(name, test_de_encode=t):
inst = c()
encoded = t[0]
decoded = { camel_to_snake(c.__name__): t[1] }
context = t[2] if len(t) == 3 else {}
logging.debug("Testing decode of %s", name)
inst.from_tlv(h2b(encoded), context=context)
re_dec = inst.to_dict()
self.assertEqual(decoded, re_dec)
logging.debug("Testing re-encode of %s", name)
re_enc = b2h(inst.to_tlv(context=context))
self.assertEqual(encoded.upper(), re_enc.upper())
if __name__ == '__main__':
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
unittest.main()

257
tests/unittests/test_utils.py Executable file
View File

@@ -0,0 +1,257 @@
#!/usr/bin/env python3
import unittest
from pySim import utils
from pySim.legacy import utils as legacy_utils
from pySim.ts_31_102 import EF_SUCI_Calc_Info
# we don't really want to thest TS 102 221, but the underlying DataObject codebase
from pySim.ts_102_221 import AM_DO_EF, AM_DO_DF, SC_DO
class DoTestCase(unittest.TestCase):
def testSeqOfChoices(self):
"""A sequence of two choices with each a variety of DO/TLVs"""
arr_seq = utils.DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
# input data
dec_in = [{'access_mode': ['update_erase', 'read_search_compare']}, {'control_reference_template':'PIN1'}]
# encode it once
encoded = arr_seq.encode(dec_in)
# decode again
re_decoded = arr_seq.decode(encoded)
self.assertEqual(dec_in, re_decoded[0])
class DecTestCase(unittest.TestCase):
# TS33.501 Annex C.4 test keys
hnet_pubkey_profile_b = "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1" # ID 27 in test file
hnet_pubkey_profile_a = "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650" # ID 30 in test file
# TS31.121 4.9.4 EF_SUCI_Calc_Info test file
testfile_suci_calc_info = "A006020101020000A14B80011B8121" +hnet_pubkey_profile_b +"80011E8120" +hnet_pubkey_profile_a
decoded_testfile_suci = {
'prot_scheme_id_list': [
{'priority': 0, 'identifier': 2, 'key_index': 1},
{'priority': 1, 'identifier': 1, 'key_index': 2},
{'priority': 2, 'identifier': 0, 'key_index': 0}],
'hnet_pubkey_list': [
{'hnet_pubkey_identifier': 27, 'hnet_pubkey': hnet_pubkey_profile_b.lower()}, # because h2b/b2h returns all lower-case
{'hnet_pubkey_identifier': 30, 'hnet_pubkey': hnet_pubkey_profile_a.lower()}]
}
def testSplitHexStringToListOf5ByteEntries(self):
input_str = "ffffff0003ffffff0002ffffff0001"
expected = [
"ffffff0003",
"ffffff0002",
"ffffff0001",
]
self.assertEqual(legacy_utils.hexstr_to_Nbytearr(input_str, 5), expected)
def testDecMCCfromPLMN(self):
self.assertEqual(utils.dec_mcc_from_plmn("92f501"), 295)
def testDecMCCfromPLMN_unused(self):
self.assertEqual(utils.dec_mcc_from_plmn("ff0f00"), 4095)
def testDecMCCfromPLMN_str(self):
self.assertEqual(utils.dec_mcc_from_plmn_str("92f501"), "295")
def testDecMCCfromPLMN_unused_str(self):
self.assertEqual(utils.dec_mcc_from_plmn_str("ff0f00"), "")
def testDecMNCfromPLMN_twoDigitMNC(self):
self.assertEqual(utils.dec_mnc_from_plmn("92f501"), 10)
def testDecMNCfromPLMN_threeDigitMNC(self):
self.assertEqual(utils.dec_mnc_from_plmn("031263"), 361)
def testDecMNCfromPLMN_unused(self):
self.assertEqual(utils.dec_mnc_from_plmn("00f0ff"), 4095)
def testDecMNCfromPLMN_twoDigitMNC_str(self):
self.assertEqual(utils.dec_mnc_from_plmn_str("92f501"), "10")
def testDecMNCfromPLMN_threeDigitMNC_str(self):
self.assertEqual(utils.dec_mnc_from_plmn_str("031263"), "361")
def testDecMNCfromPLMN_unused_str(self):
self.assertEqual(utils.dec_mnc_from_plmn_str("00f0ff"), "")
def test_enc_plmn(self):
with self.subTest("2-digit MCC"):
self.assertEqual(utils.enc_plmn("001", "01F"), "00F110")
self.assertEqual(utils.enc_plmn("001", "01"), "00F110")
self.assertEqual(utils.enc_plmn("295", "10"), "92F501")
with self.subTest("3-digit MCC"):
self.assertEqual(utils.enc_plmn("001", "001"), "001100")
self.assertEqual(utils.enc_plmn("302", "361"), "031263")
def testDecAct_noneSet(self):
self.assertEqual(utils.dec_act("0000"), [])
def testDecAct_onlyUtran(self):
self.assertEqual(utils.dec_act("8000"), ["UTRAN"])
def testDecAct_onlyEUtran(self):
self.assertEqual(utils.dec_act("4000"), ["E-UTRAN NB-S1", "E-UTRAN WB-S1"])
def testDecAct_onlyNgRan(self):
self.assertEqual(utils.dec_act("0800"), ["NG-RAN"])
def testDecAct_onlyGsm(self):
self.assertEqual(utils.dec_act("0084"), ["GSM"])
def testDecAct_onlyGsmCompact(self):
self.assertEqual(utils.dec_act("0040"), ["GSM COMPACT"])
def testDecAct_onlyCdma2000HRPD(self):
self.assertEqual(utils.dec_act("0020"), ["cdma2000 HRPD"])
def testDecAct_onlyCdma20001xRTT(self):
self.assertEqual(utils.dec_act("0010"), ["cdma2000 1xRTT"])
def testDecAct_allSet(self):
self.assertEqual(utils.dec_act("ffff"), ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'GSM COMPACT', 'NG-RAN', 'UTRAN', 'cdma2000 1xRTT', 'cdma2000 HRPD'])
def testDecxPlmn_w_act(self):
expected = {'mcc': '295', 'mnc': '10', 'act': ["UTRAN"]}
self.assertEqual(utils.dec_xplmn_w_act("92f5018000"), expected)
def testFormatxPlmn_w_act(self):
input_str = "92f501800092f5508000ffffff0000ffffff0000ffffff0000ffffff0000ffffff0000ffffff0000ffffff0000ffffff0000"
expected = "\t92f5018000 # MCC: 295 MNC: 10 AcT: UTRAN\n"
expected += "\t92f5508000 # MCC: 295 MNC: 05 AcT: UTRAN\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
self.assertEqual(legacy_utils.format_xplmn_w_act(input_str), expected)
def testDecodeSuciCalcInfo(self):
suci_calc_info = EF_SUCI_Calc_Info()
decoded = suci_calc_info.decode_hex(self.testfile_suci_calc_info)
self.assertDictEqual(self.decoded_testfile_suci, decoded)
def testEncodeSuciCalcInfo(self):
suci_calc_info = EF_SUCI_Calc_Info()
encoded = suci_calc_info.encode_hex(self.decoded_testfile_suci)
self.assertEqual(encoded.lower(), self.testfile_suci_calc_info.lower())
def testEnc_msisdn(self):
msisdn_encoded = utils.enc_msisdn("+4916012345678", npi=0x01, ton=0x03)
self.assertEqual(msisdn_encoded, "0891946110325476f8ffffffffff")
msisdn_encoded = utils.enc_msisdn("123456", npi=0x01, ton=0x03)
self.assertEqual(msisdn_encoded, "04b1214365ffffffffffffffffff")
msisdn_encoded = utils.enc_msisdn("12345678901234567890", npi=0x01, ton=0x03)
self.assertEqual(msisdn_encoded, "0bb121436587092143658709ffff")
msisdn_encoded = utils.enc_msisdn("+12345678901234567890", npi=0x01, ton=0x03)
self.assertEqual(msisdn_encoded, "0b9121436587092143658709ffff")
msisdn_encoded = utils.enc_msisdn("", npi=0x01, ton=0x03)
self.assertEqual(msisdn_encoded, "ffffffffffffffffffffffffffff")
msisdn_encoded = utils.enc_msisdn("+", npi=0x01, ton=0x03)
self.assertEqual(msisdn_encoded, "ffffffffffffffffffffffffffff")
def testDec_msisdn(self):
msisdn_decoded = utils.dec_msisdn("0891946110325476f8ffffffffff")
self.assertEqual(msisdn_decoded, (1, 1, "+4916012345678"))
msisdn_decoded = utils.dec_msisdn("04b1214365ffffffffffffffffff")
self.assertEqual(msisdn_decoded, (1, 3, "123456"))
msisdn_decoded = utils.dec_msisdn("0bb121436587092143658709ffff")
self.assertEqual(msisdn_decoded, (1, 3, "12345678901234567890"))
msisdn_decoded = utils.dec_msisdn("ffffffffffffffffffffffffffff")
self.assertEqual(msisdn_decoded, None)
msisdn_decoded = utils.dec_msisdn("00112233445566778899AABBCCDDEEFF001122330bb121436587092143658709ffff")
self.assertEqual(msisdn_decoded, (1, 3, "12345678901234567890"))
msisdn_decoded = utils.dec_msisdn("ffffffffffffffffffffffffffffffffffffffff0bb121436587092143658709ffff")
self.assertEqual(msisdn_decoded, (1, 3, "12345678901234567890"))
class TestBerTlv(unittest.TestCase):
def test_BerTlvTagDec(self):
res = utils.bertlv_parse_tag(b'\x01')
self.assertEqual(res, ({'tag':1, 'constructed':False, 'class': 0}, b''))
res = utils.bertlv_parse_tag(b'\x21')
self.assertEqual(res, ({'tag':1, 'constructed':True, 'class': 0}, b''))
res = utils.bertlv_parse_tag(b'\x81\x23')
self.assertEqual(res, ({'tag':1, 'constructed':False, 'class': 2}, b'\x23'))
res = utils.bertlv_parse_tag(b'\x1f\x8f\x00\x23')
self.assertEqual(res, ({'tag':0xf<<7, 'constructed':False, 'class': 0}, b'\x23'))
def test_BerTlvLenDec(self):
self.assertEqual(utils.bertlv_encode_len(1), b'\x01')
self.assertEqual(utils.bertlv_encode_len(127), b'\x7f')
self.assertEqual(utils.bertlv_encode_len(128), b'\x81\x80')
self.assertEqual(utils.bertlv_encode_len(0x123456), b'\x83\x12\x34\x56')
def test_BerTlvLenEnc(self):
self.assertEqual(utils.bertlv_parse_len(b'\x01\x23'), (1, b'\x23'))
self.assertEqual(utils.bertlv_parse_len(b'\x7f'), (127, b''))
self.assertEqual(utils.bertlv_parse_len(b'\x81\x80'), (128, b''))
self.assertEqual(utils.bertlv_parse_len(b'\x83\x12\x34\x56\x78'), (0x123456, b'\x78'))
def test_BerTlvParseOne(self):
res = utils.bertlv_parse_one(b'\x81\x01\x01');
self.assertEqual(res, ({'tag':1, 'constructed':False, 'class':2}, 1, b'\x01', b''))
class TestComprTlv(unittest.TestCase):
def test_ComprTlvTagDec(self):
res = utils.comprehensiontlv_parse_tag(b'\x12\x23')
self.assertEqual(res, ({'tag': 0x12, 'comprehension': False}, b'\x23'))
res = utils.comprehensiontlv_parse_tag(b'\x92')
self.assertEqual(res, ({'tag': 0x12, 'comprehension': True}, b''))
res = utils.comprehensiontlv_parse_tag(b'\x7f\x12\x34')
self.assertEqual(res, ({'tag': 0x1234, 'comprehension': False}, b''))
res = utils.comprehensiontlv_parse_tag(b'\x7f\x82\x34\x56')
self.assertEqual(res, ({'tag': 0x234, 'comprehension': True}, b'\x56'))
def test_ComprTlvTagEnc(self):
res = utils.comprehensiontlv_encode_tag(0x12)
self.assertEqual(res, b'\x12')
res = utils.comprehensiontlv_encode_tag({'tag': 0x12})
self.assertEqual(res, b'\x12')
res = utils.comprehensiontlv_encode_tag({'tag': 0x12, 'comprehension':True})
self.assertEqual(res, b'\x92')
res = utils.comprehensiontlv_encode_tag(0x1234)
self.assertEqual(res, b'\x7f\x12\x34')
res = utils.comprehensiontlv_encode_tag({'tag': 0x1234, 'comprehension':True})
self.assertEqual(res, b'\x7f\x92\x34')
class TestDgiTlv(unittest.TestCase):
def test_DgiTlvLenEnc(self):
self.assertEqual(utils.dgi_encode_len(10), b'\x0a')
self.assertEqual(utils.dgi_encode_len(254), b'\xfe')
self.assertEqual(utils.dgi_encode_len(255), b'\xff\x00\xff')
self.assertEqual(utils.dgi_encode_len(65535), b'\xff\xff\xff')
with self.assertRaises(ValueError):
utils.dgi_encode_len(65536)
def testDgiTlvLenDec(self):
self.assertEqual(utils.dgi_parse_len(b'\x0a\x0b'), (10, b'\x0b'))
self.assertEqual(utils.dgi_parse_len(b'\xfe\x0b'), (254, b'\x0b'))
self.assertEqual(utils.dgi_parse_len(b'\xff\x00\xff\x0b'), (255, b'\x0b'))
class TestLuhn(unittest.TestCase):
def test_verify(self):
utils.verify_luhn('8988211000000530082')
def test_encode(self):
self.assertEqual(utils.calculate_luhn('898821100000053008'), 2)
def test_sanitize_iccid(self):
# 19 digits with correct luhn; we expect no change
self.assertEqual(utils.sanitize_iccid('8988211000000530082'), '8988211000000530082')
# 20 digits with correct luhn; we expect no change
self.assertEqual(utils.sanitize_iccid('89882110000005300811'), '89882110000005300811')
# 19 digits without correct luhn; we expect check digit to be added
self.assertEqual(utils.sanitize_iccid('8988211000000530081'), '89882110000005300811')
# 18 digits; we expect luhn check digit to be added
self.assertEqual(utils.sanitize_iccid('898821100000053008'), '8988211000000530082')
if __name__ == "__main__":
unittest.main()