mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-20 04:18:36 +03:00
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:
91
tests/unittests/test_apdu.py
Executable file
91
tests/unittests/test_apdu.py
Executable 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()
|
||||
103
tests/unittests/test_construct.py
Normal file
103
tests/unittests/test_construct.py
Normal 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
64
tests/unittests/test_esim.py
Executable file
File diff suppressed because one or more lines are too long
98
tests/unittests/test_esim_bsp.py
Executable file
98
tests/unittests/test_esim_bsp.py
Executable file
File diff suppressed because one or more lines are too long
94
tests/unittests/test_esim_saip.py
Executable file
94
tests/unittests/test_esim_saip.py
Executable 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
31
tests/unittests/test_euicc.py
Executable 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
295
tests/unittests/test_files.py
Executable 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()
|
||||
225
tests/unittests/test_globalplatform.py
Normal file
225
tests/unittests/test_globalplatform.py
Normal 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
304
tests/unittests/test_ota.py
Normal 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
105
tests/unittests/test_sms.py
Normal 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
145
tests/unittests/test_tlv.py
Normal 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
124
tests/unittests/test_tlvs.py
Executable 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
257
tests/unittests/test_utils.py
Executable 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()
|
||||
Reference in New Issue
Block a user