407 Commits

Author SHA1 Message Date
Harald Welte
3c37033eb2 WIP: saip.data_source
Change-Id: Ia6f70ff467ba58024d94742ba5cecd8141b93ad6
2024-03-10 15:26:23 +01:00
Harald Welte
1b2c35149d WIP: global_platform: LOAD and INSTALL [for load] support
Change-Id: I924aaeecbb3a72bdb65eefbff6135e4e9570579e
2024-03-10 15:26:23 +01:00
Harald Welte
e6f3e153b5 setup.py: Expose pySim.esim as package
Change-Id: I524d2b160e743e9a75d08d3bb285ed5781e65c59
2024-03-10 15:26:23 +01:00
Harald Welte
93c402f442 HACK: saip-test.py
Change-Id: I97601e758fd5c5423bb48d3849daf58681a6c5c9
2024-03-10 15:14:28 +01:00
Vadim Yanitskiy
decb468092 tests: assertEquals() is deprecated, use assertEqual()
This fixes deprecation warnings printed by Python 3.11.7.

Change-Id: I1de93b0fee9e8439f7da8a3b9fd2a6974973fb4f
2024-03-02 01:36:19 +07:00
Harald Welte
b18c7d9be0 saip.personalization: Fix encoding of ICCID in ProfileHeader
To make things exciting, they decided that the ICCID in the profile
header is encoded different from the ICCID contained in EF.ICCID...

Change-Id: I5eacdcdc6bd0ada431eb047bfae930d79d6e3af8
2024-02-21 09:23:58 +01:00
Harald Welte
6d63712b51 saip.personalization: automatically compute class 'name' attribute
We can use the metaclass to set a proper non-camel-case name attribute.

Change-Id: If02df436c8f5ce01d21e9ee077ad3736e669d103
2024-02-21 09:23:58 +01:00
Harald Welte
2de552e712 saip.personalization: differentiate input_value from value
When personalizing e.g. the ICCID, the input_value is the raw
incrementing counter.  From that, we calculate the Luhn check digit,
and that "output" value is what we'll put in to the EF.ICCID specific
encoder.

However, we also store that output value in the instance in order
to generate the output CSV file containig the card-specific
personalization data.

Change-Id: Idfcd26c8ca9d73a9c2955f7c97e711dd59a27c4e
2024-02-21 09:23:55 +01:00
Harald Welte
19fa98e7d0 saip.personalization: Add support for SCP80/81/02/03 keys
Those keys are normally per-card unique, and hence the personalization
must be able to modify them in the profile.

Change-Id: Ibe4806366f1cce8edb09d52613b1dd56250fa5ae
2024-02-21 09:22:40 +01:00
Harald Welte
318faef583 saip.personalization: include encode/decode of value; add validation method
Change-Id: Ia9fa39c25817448afb191061acd4be894300eeef
2024-02-21 09:22:40 +01:00
Harald Welte
aa76546d16 osmo-smdpp: Add TS.48 profiles modified for unique ICCIDs
The original TS.48 profiles have shared/overlapping ICCIDs meaning you
can always install one of them on a given eUICC.  Let's add a set of
modified TS.48 profiles so  you can install any number of them in
parallel on a single eUICC, switching between them via your LPA.

Change-Id: Id5019b290db1ee90ae1c72b312f08bf3184908ea
2024-02-21 09:22:40 +01:00
Harald Welte
8449b14d08 osmo-smdpp: Get rid of hard-coded ICCID
Read the ICCID from the header of the UPP when building the
ProfileMetdata.  This allows the download of profiles with arbitrary ICCID.

Change-Id: I1b9e17f757f9935436828e6dc1ab75ff17d1d1a4
2024-02-20 23:55:37 +01:00
Harald Welte
922b8a279c saip: improve docstrings
Change-Id: I0ca82a434e0bde3dc1b304dfc179d568588631c6
2024-02-18 22:30:08 +01:00
Harald Welte
7d88b076ad pylint: esim/saip/validation.py
pySim/esim/saip/validation.py:95:42: C0117: Consider changing "not not ('usim' in m_svcs or 'isim' in m_svcs)" to "'usim' in m_svcs or 'isim' in m_svcs" (unnecessary-negation)
pySim/esim/saip/validation.py:129:0: C0305: Trailing newlines (trailing-newlines)

Change-Id: Idcc9871d6a7068e8aedbd8cd81f4156918af5e50
2024-02-18 22:30:08 +01:00
Harald Welte
5ff0bafcda pylint: esim/saip/__init__.py
pySim/esim/saip/__init__.py:28:0: R0402: Use 'from pySim.esim.saip import templates' instead (consider-using-from-import)
pySim/esim/saip/__init__.py:166:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/esim/saip/__init__.py:206:4: W0612: Unused variable 'tagdict' (unused-variable)
pySim/esim/saip/__init__.py:273:23: C1802: Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty (use-implicit-booleaness-not-len)

Change-Id: I12ef46c847d197fb0c01e624818aeac14eb99e31
2024-02-18 22:30:08 +01:00
Harald Welte
d16a20ccc3 saip: profile processing; merging with templates
Introduce code that makes use of the information from
pySim.esim.saip.templates to build a complete representation of a file
by merging the template with the ProfileElement decribing the file.

This happens within the class pySim.esim.saip.File, whose instances are
created from ProfileElement + Template.

Change-Id: Ib1674920e488ade9597cb039e4e2047dcbc7864e
2024-02-18 22:30:08 +01:00
Harald Welte
54b4f0ccbd asn1/saip: Fix typo in original ASN.1: Compontents -> Components
Change-Id: I6bec5625579873a9ec267d896584608c9d5e3a2f
2024-02-18 22:30:08 +01:00
Harald Welte
efdf423a7f utils: Add function to verify Luhn check digits and to sanitize ICCIDs
Change-Id: I7812420cf97984dd834fca6a38c5e5ae113243cb
2024-02-18 22:30:08 +01:00
Harald Welte
979c837286 Dynamically determine maximum CMD data length depending on SCP
If we're using a Secure Channel Protocol, this will add overhead
in terms of the C-MAC appended to the C-APDU.  This means in turn that
the useable length of the data field shrinks by a certain number of
bytes.

Let's make sure the SCP instances expose an 'overhead' property
of how much overhead they add - and that other commands use this to
determine the maximum command data field length.

Change-Id: I0a081a23efe20c77557600e62b52ba90a401058d
2024-02-15 20:35:29 +01:00
Harald Welte
1432af5150 Add terminal_capability command to send TERMINAL CAPABILITY
TS 102 221 specifies a TERMINAL CAPABILITY command using which the
terminal (Software + hardware talking to the card) can expose their
capabilities.  This is also used in the eUICC universe to let the eUICC
know which features are supported.

Change-Id: Iaeb8b4c34524edbb93217bf401e466399626e9b0
2024-02-12 18:59:54 +01:00
Harald Welte
eac459fe24 ts_31_102: Add support for "USIM supporting non-IMSI SUPI Type"
This type of USIM was introduced in Release 16.4. It is basically
a copy of ADF.USIM without the EF.IMSI file and a dedicated AID.

Change-Id: Ifcde27873a398273a89889bb38537f79859383e9
2024-02-12 18:04:19 +01:00
Harald Welte
95873a964e Introduce code for ES2+ API client functionality
Change-Id: Id652bb4c2df8893a824b8bb44beeafdfbb91de3f
2024-02-09 21:41:51 +01:00
Harald Welte
e1c0b626d8 global_platform: Add --suppress-key-check option to put_key command
In some cases we may not want to auto-generate the Key Check Values.

Change-Id: I244b717b3e3aae6eb3ad512f9e23ff0b65958bb7
2024-02-06 20:36:32 +01:00
Harald Welte
d6ecf272f5 pySim-shell: Fix regression in 'apdu' command on cards without profile
Cards where no profile was detected don't have a logical channel, and
hence must use the raw APDU at all times.

Change-Id: I08e5d190bdb4e62ee808bfd77584cb3e0b85a8ae
Fixes: Change-Id Id0c364f772c31e11e8dfa21624d8685d253220d0
2024-02-05 17:54:51 +01:00
Harald Welte
9d1487af6d global_platform: Fix INSTALL [for personalization]
The APDU hex string needs to use %02x instead of %02u...

Change-Id: Ic3b30ba623ee04f5190c77afd226b52165b3183f
2024-02-05 17:54:30 +01:00
Harald Welte
908634396f pylint: global_platform/__init__.py
pySim/global_platform/__init__.py:468:4: W0221: Number of parameters was 2 in 'CardFile.decode_select_response' and is now 1 in overriding 'ADF_SD.decode_select_response' method (arguments-differ)
pySim/global_platform/__init__.py:473:8: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/global_platform/__init__.py:491:19: W0612: Unused variable 'sw' (unused-variable)
pySim/global_platform/__init__.py:528:22: W0612: Unused variable 'sw' (unused-variable)
pySim/global_platform/__init__.py:559:12: C0200: Consider using enumerate instead of iterating with range and len (consider-using-enumerate)
pySim/global_platform/__init__.py:587:18: W0612: Unused variable 'sw' (unused-variable)
pySim/global_platform/__init__.py:617:20: W0612: Unused variable 'dec' (unused-variable)
pySim/global_platform/__init__.py:645:12: W0612: Unused variable 'data' (unused-variable)
pySim/global_platform/__init__.py:645:18: W0612: Unused variable 'sw' (unused-variable)
pySim/global_platform/__init__.py:746:15: C0121: Comparison 'opts.key_id == None' should be 'opts.key_id is None' (singleton-comparison)
pySim/global_platform/__init__.py:746:39: C0121: Comparison 'opts.key_ver == None' should be 'opts.key_ver is None' (singleton-comparison)
pySim/global_platform/__init__.py:750:15: C0121: Comparison 'opts.key_id != None' should be 'opts.key_id is not None' (singleton-comparison)
pySim/global_platform/__init__.py:752:15: C0121: Comparison 'opts.key_ver != None' should be 'opts.key_ver is not None' (singleton-comparison)
pySim/global_platform/__init__.py:787:16: W0612: Unused variable 'rsp_hex' (unused-variable)
pySim/global_platform/__init__.py:787:25: W0612: Unused variable 'sw' (unused-variable)
pySim/global_platform/__init__.py:836:30: W0612: Unused variable 'sw' (unused-variable)
pySim/global_platform/__init__.py:839:12: W0612: Unused variable 'ext_auth_resp' (unused-variable)
pySim/global_platform/__init__.py:846:33: W0613: Unused argument 'opts' (unused-argument)
pySim/global_platform/__init__.py:878:15: R1716: Simplify chained comparison between the operands (chained-comparison)
pySim/global_platform/__init__.py:886:29: W0613: Unused argument 'kvn' (unused-argument)
pySim/global_platform/__init__.py:893:0: C0413: Import "from Cryptodome.Cipher import DES, DES3, AES" should be placed at the top of the module (wrong-import-position)
pySim/global_platform/__init__.py:23:0: C0411: standard import "from typing import Optional, List, Dict, Tuple" should be placed before "from construct import Optional as COptional" (wrong-import-order)
pySim/global_platform/__init__.py:24:0: C0411: standard import "from copy import deepcopy" should be placed before "from construct import Optional as COptional" (wrong-import-order)
pySim/global_platform/__init__.py:893:0: C0411: third party import "from Cryptodome.Cipher import DES, DES3, AES" should be placed before "from pySim.global_platform.scp import SCP02, SCP03" (wrong-import-order)
pySim/global_platform/__init__.py:893:0: C0412: Imports from package Cryptodome are not grouped (ungrouped-imports)

Change-Id: Iea6afb5e72e035637e761bb25535f48fd4bc99f4
2024-02-05 17:54:30 +01:00
Harald Welte
55be7d48ee pylint: construct.py
pySim/construct.py:47:0: W0311: Bad indentation. Found 16 spaces, expected 12 (bad-indentation)
pySim/construct.py:59:0: W0311: Bad indentation. Found 16 spaces, expected 12 (bad-indentation)
pySim/construct.py:82:0: W0311: Bad indentation. Found 16 spaces, expected 12 (bad-indentation)
pySim/construct.py:1:0: C0114: Missing module docstring (missing-module-docstring)
pySim/construct.py:14:0: W0105: String statement has no effect (pointless-string-statement)
pySim/construct.py:178:29: W0613: Unused argument 'instr' (unused-argument)
pySim/construct.py:199:15: C0121: Comparison 'codepoint_prefix == None' should be 'codepoint_prefix is None' (singleton-comparison)
pySim/construct.py:269:15: C0121: Comparison 'v == False' should be 'v is False' if checking for the singleton value False, or 'not v' if testing for falsiness (singleton-comparison)
pySim/construct.py:271:17: C0121: Comparison 'v == True' should be 'v is True' if checking for the singleton value True, or 'v' if testing for truthiness (singleton-comparison)
pySim/construct.py:385:15: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/construct.py:392:15: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/construct.py:408:11: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/construct.py:421:7: R1701: Consider merging these isinstance calls to isinstance(c, (Container, dict)) (consider-merging-isinstance)
pySim/construct.py:444:11: R1729: Use a generator instead 'all(v == 255 for v in raw_bin_data)' (use-a-generator)
pySim/construct.py:434:81: W0613: Unused argument 'exclude_prefix' (unused-argument)
pySim/construct.py:544:12: W0707: Consider explicitly re-raising using 'raise IntegerError(str(e), path=path) from e' (raise-missing-from)
pySim/construct.py:561:8: R1731: Consider using 'nbytes = max(nbytes, minlen)' instead of unnecessary if block (consider-using-max-builtin)
pySim/construct.py:573:12: W0707: Consider explicitly re-raising using 'raise IntegerError(str(e), path=path) from e' (raise-missing-from)
pySim/construct.py:3:0: C0411: standard import "import typing" should be placed before "from construct.lib.containers import Container, ListContainer" (wrong-import-order)
pySim/construct.py:10:0: C0411: third party import "import gsm0338" should be placed before "from pySim.utils import b2h, h2b, swap_nibbles" (wrong-import-order)
pySim/construct.py:11:0: C0411: standard import "import codecs" should be placed before "from construct.lib.containers import Container, ListContainer" (wrong-import-order)
pySim/construct.py:12:0: C0411: standard import "import ipaddress" should be placed before "from construct.lib.containers import Container, ListContainer" (wrong-import-order)
pySim/construct.py:7:0: W0611: Unused BitwisableString imported from construct.core (unused-import)

Change-Id: Ic8a06d65a7bcff9ef399fe4e7e5d82f271c946bb
2024-02-05 17:54:30 +01:00
Harald Welte
6db681924c pylint: tlv.py
pySim/tlv.py:29:0: W0401: Wildcard import pySim.exceptions (wildcard-import)
pySim/tlv.py:43:4: C0204: Metaclass class method __new__ should have 'mcs' as first argument (bad-mcs-classmethod-argument)
pySim/tlv.py:66:4: C0204: Metaclass class method __new__ should have 'mcs' as first argument (bad-mcs-classmethod-argument)
pySim/tlv.py:89:11: C0121: Comparison 'self.decoded == None' should be 'self.decoded is None' (singleton-comparison)
pySim/tlv.py:170:8: R1703: The if statement can be replaced with 'return bool(test)' (simplifiable-if-statement)
pySim/tlv.py:202:4: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/tlv.py:257:4: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/tlv.py:308:4: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/tlv.py:383:15: C0121: Comparison 'tag == None' should be 'tag is None' (singleton-comparison)
pySim/tlv.py:382:17: W0612: Unused variable 'r' (unused-variable)
pySim/tlv.py:389:16: W0612: Unused variable 'dec' (unused-variable)
pySim/tlv.py:461:22: R1718: Consider using a set comprehension (consider-using-set-comprehension)
pySim/tlv.py:473:0: C0206: Consider iterating with .items() (consider-using-dict-items)
pySim/tlv.py:473:58: C0201: Consider iterating the dictionary directly instead of calling .keys() (consider-iterating-dictionary)
pySim/tlv.py:20:0: W0611: Unused Optional imported from typing (unused-import)
pySim/tlv.py:20:0: W0611: Unused Dict imported from typing (unused-import)
pySim/tlv.py:20:0: W0611: Unused Any imported from typing (unused-import)
pySim/tlv.py:21:0: W0611: Unused bidict imported from bidict (unused-import)
pySim/tlv.py:28:0: W0611: Unused LV imported from pySim.construct (unused-import)
pySim/tlv.py:28:0: W0611: Unused HexAdapter imported from pySim.construct (unused-import)
pySim/tlv.py:28:0: W0611: Unused BcdAdapter imported from pySim.construct (unused-import)
pySim/tlv.py:28:0: W0611: Unused BitsRFU imported from pySim.construct (unused-import)
pySim/tlv.py:28:0: W0611: Unused GsmStringAdapter imported from pySim.construct (unused-import)
pySim/tlv.py:29:0: W0614: Unused import(s) NoCardError, ProtocolError, ReaderError and SwMatchError from wildcard import of pySim.exceptions (unused-wildcard-import)

Change-Id: Ic22d00d3ae73ad81167276d9482b7b86a04476ba
2024-02-05 17:54:30 +01:00
Harald Welte
f2b20bf6ca pylint: utils.py
pySim/utils.py:903:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/utils.py:153:16: R1719: The if expression can be replaced with 'bool(test)' (simplifiable-if-expression)
pySim/utils.py:158:16: R1719: The if expression can be replaced with 'bool(test)' (simplifiable-if-expression)
pySim/utils.py:166:16: R1719: The if expression can be replaced with 'bool(test)' (simplifiable-if-expression)
pySim/utils.py:222:19: R1719: The if expression can be replaced with 'not test' (simplifiable-if-expression)
pySim/utils.py:237:18: R1719: The if expression can be replaced with 'bool(test)' (simplifiable-if-expression)
pySim/utils.py:246:19: R1719: The if expression can be replaced with 'not test' (simplifiable-if-expression)
pySim/utils.py:279:11: W0612: Unused variable 'remainder' (unused-variable)
pySim/utils.py:541:7: R1714: Consider merging these comparisons with 'in' by using 'eutran_bits in (16384, 28672)'. Use a set instead if elements are hashable. (consider-using-in)
pySim/utils.py:550:7: R1714: Consider merging these comparisons with 'in' by using 'gsm_bits in (128, 140)'. Use a set instead if elements are hashable. (consider-using-in)
pySim/utils.py:614:7: C0121: Comparison 'imsi == None' should be 'imsi is None' (singleton-comparison)
pySim/utils.py:627:7: C0121: Comparison 'imsi == None' should be 'imsi is None' (singleton-comparison)
pySim/utils.py:733:7: R1714: Consider merging these comparisons with 'in' by using 'msisdn in ('', '+')'. Use a set instead if elements are hashable. (consider-using-in)
pySim/utils.py:774:8: W0612: Unused variable 'try_encode' (unused-variable)
pySim/utils.py:803:16: W0707: Consider explicitly re-raising using 'except ValueError as exc' and 'raise ValueError('PIN-ADM needs to be hex encoded using this option') from exc' (raise-missing-from)
pySim/utils.py:801:16: W0612: Unused variable 'try_encode' (unused-variable)
pySim/utils.py:821:7: C1802: Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty (use-implicit-booleaness-not-len)
pySim/utils.py:836:4: W0612: Unused variable 'e' (unused-variable)
pySim/utils.py:892:7: C0121: Comparison 'str_list == None' should be 'str_list is None' (singleton-comparison)
pySim/utils.py:991:11: R1701: Consider merging these isinstance calls to isinstance(o, (BytesIO, bytearray, bytes)) (consider-merging-isinstance)

Change-Id: I190ae75964ef6e0ed43fae994693a8bccd21c7f7
2024-02-05 17:54:30 +01:00
Harald Welte
472165f20f pylint: ts_102_222.py
pySim/ts_102_222.py:195:0: W0311: Bad indentation. Found 15 spaces, expected 12 (bad-indentation)
pySim/ts_102_222.py:201:0: W0311: Bad indentation. Found 15 spaces, expected 12 (bad-indentation)
pySim/ts_102_222.py:26:0: W0401: Wildcard import pySim.exceptions (wildcard-import)
pySim/ts_102_222.py:35:4: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/ts_102_222.py:52:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:52:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:73:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:73:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:89:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:89:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:107:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:107:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:152:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:152:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:203:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:203:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:220:9: W0612: Unused variable 'data' (unused-variable)
pySim/ts_102_222.py:220:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_222.py:24:0: C0411: standard import "import argparse" should be placed before "import cmd2" (wrong-import-order)
pySim/ts_102_222.py:23:0: W0611: Unused with_argparser imported from cmd2 (unused-import)
pySim/ts_102_222.py:27:0: W0611: Unused h2b imported from pySim.utils (unused-import)
pySim/ts_102_222.py:27:0: W0611: Unused swap_nibbles imported from pySim.utils (unused-import)
pySim/ts_102_222.py:27:0: W0611: Unused JsonEncoder imported from pySim.utils (unused-import)
pySim/ts_102_222.py:26:0: W0614: Unused import(s) NoCardError, ProtocolError, ReaderError and SwMatchError from wildcard import of pySim.exceptions (unused-wildcard-import)

Change-Id: If251c6cb10e637a13adaaf3ae848501908b9c345
2024-02-05 17:54:30 +01:00
Harald Welte
f2322774c7 pylint: filesystem.py
pySim/filesystem.py:823:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/filesystem.py:849:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/filesystem.py:43:0: W0401: Wildcard import pySim.exceptions (wildcard-import)
pySim/filesystem.py:74:45: C0121: Comparison 'fid == None' should be 'fid is None' (singleton-comparison)
pySim/filesystem.py:94:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/filesystem.py:100:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/filesystem.py:149:8: W0105: String statement has no effect (pointless-string-statement)
pySim/filesystem.py:170:11: C0121: Comparison 'self.parent == None' should be 'self.parent is None' (singleton-comparison)
pySim/filesystem.py:283:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:309:8: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/filesystem.py:314:15: C0117: Consider changing "not 'fid' in kwargs" to "'fid' not in kwargs" (unnecessary-negation)
pySim/filesystem.py:317:24: R1735: Consider using '{}' instead of a call to 'dict'. (use-dict-literal)
pySim/filesystem.py:418:11: C0121: Comparison 'name == None' should be 'name is None' (singleton-comparison)
pySim/filesystem.py:427:11: C0121: Comparison 'sfid == None' should be 'sfid is None' (singleton-comparison)
pySim/filesystem.py:452:28: R1735: Consider using '{}' instead of a call to 'dict'. (use-dict-literal)
pySim/filesystem.py:508:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/filesystem.py:531:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/filesystem.py:576:8: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/filesystem.py:599:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:609:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:620:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:633:28: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:642:41: W0613: Unused argument 'opts' (unused-argument)
pySim/filesystem.py:644:24: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:696:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:723:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:749:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:777:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:797:8: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/filesystem.py:822:23: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:838:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:844:34: W0613: Unused argument 'opts' (unused-argument)
pySim/filesystem.py:848:23: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:866:23: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:878:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:893:28: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:910:24: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:967:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:995:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1023:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1051:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1114:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1141:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1167:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1194:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/filesystem.py:1226:8: W0246: Useless parent or super() delegation in method '__init__' (useless-parent-delegation)
pySim/filesystem.py:1236:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:1239:35: W0613: Unused argument 'opts' (unused-argument)
pySim/filesystem.py:1252:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:1263:19: W0612: Unused variable 'sw' (unused-variable)
pySim/filesystem.py:1315:24: R1735: Consider using '{}' instead of a call to 'dict'. (use-dict-literal)
pySim/filesystem.py:35:0: C0411: standard import "import argparse" should be placed before "import cmd2" (wrong-import-order)
pySim/filesystem.py:37:0: C0411: standard import "from typing import cast, Optional, Iterable, List, Dict, Tuple, Union" should be placed before "import cmd2" (wrong-import-order)
pySim/filesystem.py:27:0: W0611: Unused import code (unused-import)
pySim/filesystem.py:34:0: W0611: Unused with_argparser imported from cmd2 (unused-import)
pySim/filesystem.py:41:0: W0611: Unused i2h imported from pySim.utils (unused-import)
pySim/filesystem.py:41:0: W0611: Unused Hexstr imported from pySim.utils (unused-import)
pySim/filesystem.py:44:0: W0611: Unused js_path_find imported from pySim.jsonpath (unused-import)
pySim/filesystem.py:43:0: W0614: Unused import(s) NoCardError, ProtocolError, ReaderError and SwMatchError from wildcard import of pySim.exceptions (unused-wildcard-import)

Change-Id: I94e1f5791e9fc34a60d0254978a35fd6ab2ff8d7
2024-02-05 17:51:59 +01:00
Harald Welte
8829f8e690 pylint: commands.py
pySim/commands.py:443:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/commands.py:446:0: C0325: Unnecessary parens after 'elif' keyword (superfluous-parens)
pySim/commands.py:669:0: C0325: Unnecessary parens after 'elif' keyword (superfluous-parens)
pySim/commands.py:27:0: W0622: Redefining built-in 'BlockingIOError' (redefined-builtin)
pySim/commands.py:27:0: W0401: Wildcard import construct (wildcard-import)
pySim/commands.py:30:0: W0404: Reimport 'Hexstr' (imported line 29) (reimported)
pySim/commands.py:42:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:48:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:98:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:114:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:131:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:223:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:234:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/commands.py:252:11: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/commands.py:271:11: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/commands.py:274:18: W0612: Unused variable 'sw' (unused-variable)
pySim/commands.py:326:16: W0707: Consider explicitly re-raising using 'raise ValueError('%s, failed to read (offset %d)' % (str_sanitize(str(e)), offset)) from e' (raise-missing-from)
pySim/commands.py:386:16: W0707: Consider explicitly re-raising using 'raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' % (str_sanitize(str(e)), chunk_offset, chunk_len)) from e' (raise-missing-from)
pySim/commands.py:443:12: R1720: Unnecessary "elif" after "raise", remove the leading "el" from "elif" (no-else-raise)
pySim/commands.py:521:14: R1714: Consider merging these comparisons with 'in' by using 'sw in ('62f1', '62f2')'. Use a set instead if elements are hashable. (consider-using-in)
pySim/commands.py:532:11: R1701: Consider merging these isinstance calls to isinstance(data, (bytearray, bytes)) (consider-merging-isinstance)
pySim/commands.py:666:8: R1720: Unnecessary "elif" after "raise", remove the leading "el" from "elif" (no-else-raise)
pySim/commands.py:762:12: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/commands.py:776:12: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)

Change-Id: Idfcd6f799d5de9ecacd2c3d1e0d1f7d932f2b8db
2024-02-05 12:41:38 +01:00
Harald Welte
09f9663005 pylint: pySim/euicc.py
pySim/euicc.py:27:0: W0622: Redefining built-in 'BlockingIOError' (redefined-builtin)
pySim/euicc.py:27:0: W0401: Wildcard import construct (wildcard-import)
pySim/euicc.py:37:7: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/euicc.py:47:9: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/euicc.py:337:12: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/euicc.py:325:63: W0613: Unused argument 'exp_sw' (unused-argument)
pySim/euicc.py:335:15: W0612: Unused variable 'sw' (unused-variable)
pySim/euicc.py:361:13: W0612: Unused variable 'data' (unused-variable)
pySim/euicc.py:361:19: W0612: Unused variable 'sw' (unused-variable)
pySim/euicc.py:363:52: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:380:41: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:386:37: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:392:37: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:398:39: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:415:39: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:478:29: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:480:13: W0612: Unused variable 'data' (unused-variable)
pySim/euicc.py:480:19: W0612: Unused variable 'sw' (unused-variable)
pySim/euicc.py:500:31: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:506:48: W0613: Unused argument 'opts' (unused-argument)
pySim/euicc.py:26:0: C0411: third party import "from construct import Optional as COptional" should be placed before "from pySim.tlv import *" (wrong-import-order)
pySim/euicc.py:27:0: C0411: third party import "from construct import *" should be placed before "from pySim.tlv import *" (wrong-import-order)
pySim/euicc.py:28:0: C0411: standard import "import argparse" should be placed before "from construct import Optional as COptional" (wrong-import-order)
pySim/euicc.py:29:0: C0411: third party import "from cmd2 import cmd2, CommandSet, with_default_category" should be placed before "from pySim.tlv import *" (wrong-import-order)
pySim/euicc.py:30:0: C0412: Imports from package pySim are not grouped (ungrouped-imports)
pySim/euicc.py:31:0: W0611: Unused CardADF imported from pySim.filesystem (unused-import)
pySim/euicc.py:31:0: W0611: Unused CardApplication imported from pySim.filesystem (unused-import)

Change-Id: I6c33e2361a042a16f27e66cb883c392333b8383d
2024-02-05 12:37:54 +01:00
Harald Welte
356a6c0f99 pylint: runtime.py
pySim/runtime.py:272:0: C0325: Unnecessary parens after 'raise' keyword (superfluous-parens)
pySim/runtime.py:276:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/runtime.py:280:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/runtime.py:349:0: C0325: Unnecessary parens after 'raise' keyword (superfluous-parens)
pySim/runtime.py:549:0: C0305: Trailing newlines (trailing-newlines)
pySim/runtime.py:29:4: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/runtime.py:119:16: W0612: Unused variable 'data' (unused-variable)
pySim/runtime.py:153:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/runtime.py:161:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/runtime.py:212:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/runtime.py:273:12: W0707: Consider explicitly re-raising using 'raise RuntimeError('%s: %s - %s' % (swm.sw_actual, k[0], k[1])) from swm' (raise-missing-from)
pySim/runtime.py:267:19: W0612: Unused variable 'sw' (unused-variable)
pySim/runtime.py:343:27: W0612: Unused variable 'sw' (unused-variable)
pySim/runtime.py:397:15: W0612: Unused variable 'sw' (unused-variable)
pySim/runtime.py:527:14: W0612: Unused variable 'sw' (unused-variable)
pySim/runtime.py:528:8: W0612: Unused variable 'tag' (unused-variable)
pySim/runtime.py:528:13: W0612: Unused variable 'length' (unused-variable)
pySim/runtime.py:528:28: W0612: Unused variable 'remainder' (unused-variable)

Change-Id: I2e164dbaa2070116bed3bac63b0fa5b8aa5b1331
2024-02-05 11:27:36 +01:00
Harald Welte
f01c4b2c98 pylint: ara_m.py
pySim/ara_m.py:29:0: W0622: Redefining built-in 'BlockingIOError' (redefined-builtin)
pySim/ara_m.py:29:0: W0401: Wildcard import construct (wildcard-import)
pySim/ara_m.py:68:12: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/ara_m.py:89:12: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/ara_m.py:282:12: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/ara_m.py:280:15: W0612: Unused variable 'sw' (unused-variable)
pySim/ara_m.py:312:34: W0613: Unused argument 'opts' (unused-argument)
pySim/ara_m.py:318:37: W0613: Unused argument 'opts' (unused-argument)
pySim/ara_m.py:356:15: C0121: Comparison 'opts.aid != None' should be 'opts.aid is not None' (singleton-comparison)
pySim/ara_m.py:385:37: W0613: Unused argument 'opts' (unused-argument)
pySim/ara_m.py:309:8: W0238: Unused private member `AddlShellCommands.__init(self)` (unused-private-member)
pySim/ara_m.py:309:8: W0238: Unused private member `ADF_ARAM.AddlShellCommands.__init(self)` (unused-private-member)

Change-Id: I5a739187a8966cdb0ae5c6cbc7bc5d4115433aeb
2024-02-05 11:27:36 +01:00
Harald Welte
a5fafe8b48 pylint: ts_102_221.py
pySim/ts_102_221.py:20:0: W0622: Redefining built-in 'BlockingIOError' (redefined-builtin)
pySim/ts_102_221.py:30:0: R0402: Use 'from pySim import iso7816_4' instead (consider-using-from-import)
pySim/ts_102_221.py:20:0: W0401: Wildcard import construct (wildcard-import)
pySim/ts_102_221.py:235:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/ts_102_221.py:272:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/ts_102_221.py:281:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/ts_102_221.py:484:12: W0715: Exception arguments suggest string formatting might be intended (raising-format-tuple)
pySim/ts_102_221.py:486:12: W0715: Exception arguments suggest string formatting might be intended (raising-format-tuple)
pySim/ts_102_221.py:488:12: W0715: Exception arguments suggest string formatting might be intended (raising-format-tuple)
pySim/ts_102_221.py:523:11: C1802: Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty (use-implicit-booleaness-not-len)
pySim/ts_102_221.py:647:0: W0613: Unused argument 'kwargs' (unused-argument)
pySim/ts_102_221.py:747:19: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_102_221.py:26:0: C0411: third party import "from bidict import bidict" should be placed before "from pySim.construct import *" (wrong-import-order)
pySim/ts_102_221.py:27:0: C0412: Imports from package pySim are not grouped (ungrouped-imports)
pySim/ts_102_221.py:29:0: W0611: Unused match_sim imported from pySim.profile (unused-import)
pySim/ts_102_221.py:34:0: W0611: Unused DF_GSM imported from pySim.ts_51_011 (unused-import)
pySim/ts_102_221.py:34:0: W0611: Unused DF_TELECOM imported from pySim.ts_51_011 (unused-import)

Change-Id: I99d408bdf2551527f097a04240e857728b738621
2024-02-05 09:56:02 +01:00
Harald Welte
a5630dc45c pylint: apdu/ts_102_221.py
pySim/apdu/ts_102_221.py:60:16: R1724: Unnecessary "else" after "continue", remove the "else" and de-indent the code inside it (no-else-continue)
pySim/apdu/ts_102_221.py:107:16: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/apdu/ts_102_221.py:294:8: R1703: The if statement can be replaced with 'return bool(test)' (simplifiable-if-statement)
pySim/apdu/ts_102_221.py:294:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/ts_102_221.py:299:31: W0613: Unused argument 'lchan' (unused-argument)
...
pySim/apdu/ts_102_221.py:389:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/apdu/ts_102_221.py:421:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/apdu/ts_102_221.py:425:12: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/apdu/ts_102_221.py:438:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/ts_102_221.py:26:0: C0411: standard import "from typing import Optional, Dict, Tuple" should be placed before "from construct import GreedyRange, Struct" (wrong-import-order)

Change-Id: Id5caac8da4c965dbaf88d624cdc9dcc8fc168b8c
2024-02-05 09:55:56 +01:00
Harald Welte
4b56c6cd3e pylint: ts_31_102.py
Change-Id: I5b72ad476d338aa4048bb15a74796ef69191f028
2024-02-05 09:55:50 +01:00
Harald Welte
0d9c8f73a8 pylint: sysmocom_sja2.py
pySim/sysmocom_sja2.py:20:0: W0401: Wildcard import pytlv.TLV (wildcard-import)
pySim/sysmocom_sja2.py:27:0: W0401: Wildcard import construct (wildcard-import)
pySim/sysmocom_sja2.py:21:0: C0411: standard import "from struct import pack, unpack" should be placed before "from pytlv.TLV import *" (wrong-import-order)
pySim/sysmocom_sja2.py:27:0: C0411: third party import "from construct import *" should be placed before "from pySim.utils import *" (wrong-import-order)
pySim/sysmocom_sja2.py:21:0: W0611: Unused pack imported from struct (unused-import)
pySim/sysmocom_sja2.py:25:0: W0611: Unused CardProfileUICC imported from pySim.ts_102_221 (unused-import)

Change-Id: I0e5b5c6f3179f9710464af4cba91d682412b8a09
2024-02-05 09:55:43 +01:00
Harald Welte
4f3976d77f pylint: cdma_ruim.py
pySim/cdma_ruim.py:30:0: W0401: Wildcard import construct (wildcard-import)
pySim/cdma_ruim.py:188:4: W0237: Parameter 'data_hex' has been renamed to 'resp_hex' in overriding 'CardProfileRUIM.decode_select_response' method (arguments-renamed)
pySim/cdma_ruim.py:30:0: C0411: third party import "from construct import *" should be placed before "from pySim.utils import *" (wrong-import-order)

Change-Id: I4c384f37a6a317c6eddef8742572fcfa76a5fc20
2024-02-05 09:53:59 +01:00
Harald Welte
4c0b80415e pylint: global_platform/scp.py
pySim/global_platform/scp.py:27:0: W0404: Reimport 'Optional' (imported line 20) (reimported)
pySim/global_platform/scp.py:157:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/global_platform/scp.py:165:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/global_platform/scp.py:182:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/global_platform/scp.py:189:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/global_platform/scp.py:266:4: W0221: Variadics removed in overriding 'SCP02._wrap_cmd_apdu' method (arguments-differ)
pySim/global_platform/scp.py:298:4: W0237: Parameter 'rsp_apdu' has been renamed to 'apdu' in overriding 'SCP02.unwrap_rsp_apdu' method (arguments-renamed)
pySim/global_platform/scp.py:314:7: C0121: Comparison 'l == None' should be 'l is None' (singleton-comparison)
pySim/global_platform/scp.py:436:11: C0121: Comparison 'host_challenge == None' should be 'host_challenge is None' (singleton-comparison)
pySim/global_platform/scp.py:506:4: W0237: Parameter 'rsp_apdu' has been renamed to 'apdu' in overriding 'SCP03.unwrap_rsp_apdu' method (arguments-renamed)
pySim/global_platform/scp.py:27:0: C0411: standard import "from typing import Optional" should be placed before "from Cryptodome.Cipher import DES3, DES" (wrong-import-order)

Change-Id: Idd2b779a6628c88d9a48c94b8581525209824426
2024-02-05 09:53:54 +01:00
Harald Welte
530bf73cbc pylint: esim/saip/oid.py
pySim/esim/saip/oid.py:30:11: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/esim/saip/oid.py:46:11: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)

Change-Id: I65c9cd1bb2b6a1747a7fbb25052adc75605bc870
2024-02-05 09:53:50 +01:00
Harald Welte
5f6b64dc25 pylint: esim/saip/templates.py
pySim/esim/saip/templates.py:106:0: R1707: Disallow trailing comma tuple (trailing-comma-tuple)
pySim/esim/saip/templates.py:56:37: C0121: Comparison 'self.fid != None' should be 'self.fid is not None' (singleton-comparison)
pySim/esim/saip/templates.py:57:28: C0121: Comparison 'self.arr != None' should be 'self.arr is not None' (singleton-comparison)
pySim/esim/saip/templates.py:58:37: C0121: Comparison 'self.sfi != None' should be 'self.sfi is not None' (singleton-comparison)
pySim/esim/saip/templates.py:96:11: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/esim/saip/templates.py:591:0: W1404: Implicit string concatenation found in list (implicit-str-concat)

Change-Id: I181578ba630c8bdb558297e990411b59593652a0
2024-02-05 09:53:45 +01:00
Harald Welte
1258e464a1 pylint: esim/saip/personalization.py
pySim/esim/saip/personalization.py:104:0: W0311: Bad indentation. Found 17 spaces, expected 16 (bad-indentation)
pySim/esim/saip/personalization.py:105:0: W0311: Bad indentation. Found 17 spaces, expected 16 (bad-indentation)
pySim/esim/saip/personalization.py:151:0: C0305: Trailing newlines (trailing-newlines)
pySim/esim/saip/personalization.py:36:4: C0204: Metaclass class method __new__ should have 'mcs' as first argument (bad-mcs-classmethod-argument)
pySim/esim/saip/personalization.py:56:4: W0237: Parameter 'pe_seq' has been renamed to 'pes' in overriding 'Iccid.apply' method (arguments-renamed)
pySim/esim/saip/personalization.py:19:0: W0611: Unused Optional imported from typing (unused-import)

Change-Id: I70b3e266bbafabbfcec3d48027d50b45c2c17809
2024-02-05 09:53:40 +01:00
Harald Welte
2b84644c08 pylint: esim/rsp.py
pySim/esim/rsp.py:101:4: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/esim/rsp.py:27:0: C0411: standard import "from collections.abc import MutableMapping" should be placed before "from cryptography.hazmat.primitives.asymmetric import ec" (wrong-import-order)
pySim/esim/rsp.py:22:0: W0611: Unused import copyreg (unused-import)
pySim/esim/rsp.py:27:0: W0611: Unused MutableMapping imported from collections.abc (unused-import)

Change-Id: Id87dbf82cd41ce6e5276e5bdd7af1877d77e3fab
2024-02-05 09:53:35 +01:00
Harald Welte
f235b799e9 pylint: esim/x509_cert.py
pySim/esim/x509_cert.py:70:4: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/esim/x509_cert.py:91:20: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/esim/x509_cert.py:105:28: W3101: Missing timeout argument for method 'requests.get' can cause your program to hang indefinitely (missing-timeout)
pySim/esim/x509_cert.py:163:0: C0413: Import "from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature" should be placed at the top of the module (wrong-import-position)
pySim/esim/x509_cert.py:20:0: C0411: standard import "from typing import Optional, List" should be placed before "import requests" (wrong-import-order)
pySim/esim/x509_cert.py:163:0: C0411: third party import "from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature" should be placed before "from pySim.utils import b2h" (wrong-import-order)
pySim/esim/x509_cert.py:163:0: C0412: Imports from package cryptography are not grouped (ungrouped-imports)
pySim/esim/x509_cert.py:22:0: W0611: Unused padding imported from cryptography.hazmat.primitives.asymmetric (unused-import)
pySim/esim/x509_cert.py:24:0: W0611: Unused InvalidSignature imported from cryptography.exceptions (unused-import)

Change-Id: Ic435c9a7cfcc18cacec3a3d872925bd737fb5cd9
2024-02-05 09:53:30 +01:00
Harald Welte
e6e74229c9 pylint: pySim/esim/bsp.py
pySim/esim/bsp.py:1:0: C0114: Missing module docstring (missing-module-docstring)
pySim/esim/bsp.py:28:0: C0413: Import "import abc" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:29:0: C0413: Import "from typing import List" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:30:0: C0413: Import "import logging" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:33:0: C0413: Import "from cryptography.hazmat.primitives import hashes" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:34:0: C0413: Import "from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:36:0: C0413: Import "from Cryptodome.Cipher import AES" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:37:0: C0413: Import "from Cryptodome.Hash import CMAC" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:39:0: C0413: Import "from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h" should be placed at the top of the module (wrong-import-position)
pySim/esim/bsp.py:48:55: W0613: Unused argument 'padding' (unused-argument)
pySim/esim/bsp.py:55:45: W0613: Unused argument 'multiple' (unused-argument)
pySim/esim/bsp.py:84:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/esim/bsp.py:89:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/esim/bsp.py:94:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/esim/bsp.py:169:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/esim/bsp.py:292:8: W0612: Unused variable 'tdict' (unused-variable)
pySim/esim/bsp.py:292:15: W0612: Unused variable 'l' (unused-variable)
pySim/esim/bsp.py:292:23: W0612: Unused variable 'remain' (unused-variable)

Change-Id: I64bd634606c375e767676a4b5ba7c2cc042350c2
2024-02-05 09:53:26 +01:00
Harald Welte
9ef65099d2 pylint: apdu/__init__.py
pySim/apdu/__init__.py:41:0: W0105: String statement has no effect (pointless-string-statement)
pySim/apdu/__init__.py:55:4: C0204: Metaclass class method __new__ should have 'mcs' as first argument (bad-mcs-classmethod-argument)
pySim/apdu/__init__.py:187:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/apdu/__init__.py:200:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:208:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:216:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:224:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:239:11: C0117: Consider changing "not 'p1' in self.cmd_dict" to "'p1' not in self.cmd_dict" (unnecessary-negation)
pySim/apdu/__init__.py:295:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:313:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:416:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:429:12: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:455:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/apdu/__init__.py:31:0: C0411: standard import "import typing" should be placed before "from termcolor import colored" (wrong-import-order)
pySim/apdu/__init__.py:32:0: C0411: standard import "from typing import List, Dict, Optional" should be placed before "from termcolor import colored" (wrong-import-order)

Change-Id: I5657912df474f3ed0e277458a8eb33e28aeb2927
2024-02-05 09:53:21 +01:00
Harald Welte
0930bcbbb7 pylint: apdu/ts_31_102.py
pySim/apdu/ts_31_102.py:12:0: W0401: Wildcard import construct (wildcard-import)
pySim/apdu/ts_31_102.py:38:0: W0404: Reimport 'ApduCommand' (imported line 18) (reimported)
pySim/apdu/ts_31_102.py:38:0: W0404: Reimport 'ApduCommandSet' (imported line 18) (reimported)

Change-Id: I191147af95142adcd7d768d7dae6480b0c7513fc
2024-02-05 09:53:16 +01:00
Harald Welte
295d4a4907 pylint: apdu_source/pyshark_rspro
pySim/apdu_source/pyshark_rspro.py:19:0: W0611: Unused import sys (unused-import)
pySim/apdu_source/pyshark_rspro.py:21:0: W0611: Unused pprint imported from pprint as pp (unused-import)
pySim/apdu_source/pyshark_rspro.py:25:0: W0611: Unused b2h imported from pySim.utils (unused-import)

Change-Id: Ibe8482d8adbb82a74f36b0d64bc5dae27da02b73
2024-02-05 09:53:12 +01:00
Harald Welte
9bc016e777 pylint: apdu_source/pyshark_gsmtap
pySim/apdu_source/pyshark_gsmtap.py:90:0: C0305: Trailing newlines (trailing-newlines)
pySim/apdu_source/pyshark_gsmtap.py:68:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/apdu_source/pyshark_gsmtap.py:30:0: C0411: first party import "from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands" should be placed before "from . import ApduSource, PacketType, CardReset" (wrong-import-order)
pySim/apdu_source/pyshark_gsmtap.py:31:0: C0411: first party import "from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands" should be placed before "from . import ApduSource, PacketType, CardReset" (wrong-import-order)
pySim/apdu_source/pyshark_gsmtap.py:32:0: C0411: first party import "from pySim.apdu.global_platform import ApduCommands as GpApduCommands" should be placed before "from . import ApduSource, PacketType, CardReset" (wrong-import-order)
pySim/apdu_source/pyshark_gsmtap.py:19:0: W0611: Unused import sys (unused-import)
pySim/apdu_source/pyshark_gsmtap.py:21:0: W0611: Unused pprint imported from pprint as pp (unused-import)
pySim/apdu_source/pyshark_gsmtap.py:25:0: W0611: Unused b2h imported from pySim.utils (unused-import)
pySim/apdu_source/pyshark_gsmtap.py:26:0: W0611: Unused Tpdu imported from pySim.apdu (unused-import)

Change-Id: I0f2bfed2f671e02fc48bcc2a03c785edc691584f
2024-02-05 09:53:06 +01:00
Harald Welte
528d922510 pylint: apdu_source/gsmtap.py
pySim/apdu_source/gsmtap.py:48:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/apdu_source/gsmtap.py:44:20: W0612: Unused variable 'addr' (unused-variable)
pySim/apdu_source/gsmtap.py:22:0: C0411: first party import "from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands" should be placed before "from . import ApduSource, PacketType, CardReset" (wrong-import-order)
pySim/apdu_source/gsmtap.py:23:0: C0411: first party import "from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands" should be placed before "from . import ApduSource, PacketType, CardReset" (wrong-import-order)
pySim/apdu_source/gsmtap.py:24:0: C0411: first party import "from pySim.apdu.global_platform import ApduCommands as GpApduCommands" should be placed before "from . import ApduSource, PacketType, CardReset" (wrong-import-order)
pySim/apdu_source/gsmtap.py:19:0: W0611: Unused GsmtapMessage imported from pySim.gsmtap (unused-import)

Change-Id: I672e8838ebe11015863fd4fd6047181a3f184658
2024-02-05 09:52:58 +01:00
Harald Welte
c5ff0a6ab5 pylint: apdu_source/__init__.py
pySim/apdu_source/__init__.py:1:0: C0114: Missing module docstring (missing-module-docstring)
pySim/apdu_source/__init__.py:17:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/apdu_source/__init__.py:34:16: W0133: Exception statement has no effect (pointless-exception-statement)

Change-Id: Iec552afd6004b849132132642910a4c7f91e3473
2024-02-05 09:52:52 +01:00
Harald Welte
49d69335b2 pylint: transport/__init__.py
pySim/transport/__init__.py:139:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)

Change-Id: Ibeeb7d6fde40bb37774bbd09ad185203ac7bcb48
2024-02-05 09:52:46 +01:00
Harald Welte
fdaefd9a8a pylint: transport/serial.py
pySim/transport/serial.py:54:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/transport/serial.py:89:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/transport/serial.py:225:0: C0325: Unnecessary parens after 'while' keyword (superfluous-parens)
pySim/transport/serial.py:63:12: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/transport/serial.py:106:8: R1720: Unnecessary "elif" after "raise", remove the leading "el" from "elif" (no-else-raise)
pySim/transport/serial.py:124:12: W0707: Consider explicitly re-raising using 'except Exception as exc' and 'raise ValueError('Invalid reset pin %s' % self._rst_pin) from exc' (raise-missing-from)
pySim/transport/serial.py:204:12: R1723: Unnecessary "elif" after "break", remove the leading "el" from "elif" (no-else-break)
pySim/transport/serial.py:20:0: C0411: standard import "import time" should be placed before "import serial" (wrong-import-order)
pySim/transport/serial.py:21:0: C0411: standard import "import os" should be placed before "import serial" (wrong-import-order)
pySim/transport/serial.py:22:0: C0411: standard import "import argparse" should be placed before "import serial" (wrong-import-order)
pySim/transport/serial.py:23:0: C0411: standard import "from typing import Optional" should be placed before "import serial" (wrong-import-order)

Change-Id: I82ef12492615a18a13cbdecf0371b3a5d02bbd5c
2024-02-05 09:52:40 +01:00
Harald Welte
7781c70c09 pylint: transport/pcsc.py
pySim/transport/pcsc.py:26:0: W0404: Reimport 'CardConnectionException' (imported line 26) (reimported)
pySim/transport/pcsc.py:60:4: R1711: Useless return at end of function or method (useless-return)
pySim/transport/pcsc.py:74:12: W0707: Consider explicitly re-raising using 'except CardRequestTimeoutException as exc' and 'raise NoCardError() from exc' (raise-missing-from)
pySim/transport/pcsc.py:86:12: W0707: Consider explicitly re-raising using 'except CardConnectionException as exc' and 'raise ProtocolError() from exc' (raise-missing-from)
pySim/transport/pcsc.py:88:12: W0707: Consider explicitly re-raising using 'except NoCardException as exc' and 'raise NoCardError() from exc' (raise-missing-from)
pySim/transport/pcsc.py:22:0: W0611: Unused Union imported from typing (unused-import)

Change-Id: I0ef440d8825300d6efb8959a67da095ab5623f9c
2024-02-05 09:52:29 +01:00
Harald Welte
181becb676 pylint: transport/modem_atcmd.py
pySim/transport/modem_atcmd.py:70:0: C0325: Unnecessary parens after 'assert' keyword (superfluous-parens)
pySim/transport/modem_atcmd.py:28:0: W0401: Wildcard import pySim.exceptions (wildcard-import)
pySim/transport/modem_atcmd.py:60:22: C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
pySim/transport/modem_atcmd.py:72:12: W0707: Consider explicitly re-raising using 'except Exception as exc' and 'raise ReaderError('Failed to send AT command: %s' % cmd) from exc' (raise-missing-from)
pySim/transport/modem_atcmd.py:120:12: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/transport/modem_atcmd.py:138:8: W1201: Use lazy % formatting in logging functions (logging-not-lazy)
pySim/transport/modem_atcmd.py:170:12: W0707: Consider explicitly re-raising using 'except Exception as exc' and 'raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc' (raise-missing-from)
pySim/transport/modem_atcmd.py:168:13: W0612: Unused variable 'rsp_pdu_len' (unused-variable)
pySim/transport/modem_atcmd.py:21:0: C0411: standard import "import time" should be placed before "import serial" (wrong-import-order)
pySim/transport/modem_atcmd.py:22:0: C0411: standard import "import re" should be placed before "import serial" (wrong-import-order)
pySim/transport/modem_atcmd.py:23:0: C0411: standard import "import argparse" should be placed before "import serial" (wrong-import-order)
pySim/transport/modem_atcmd.py:24:0: C0411: standard import "from typing import Optional" should be placed before "import serial" (wrong-import-order)
pySim/transport/modem_atcmd.py:28:0: W0614: Unused import(s) NoCardError and SwMatchError from wildcard import of pySim.exceptions (unused-wildcard-import)

Change-Id: I2c8994eabd973b65132af1030429b1021d0c20df
2024-02-05 09:52:24 +01:00
Harald Welte
c4d80870e8 pylint: transport/calypso.py
pySim/transport/calypso.py:27:0: W0401: Wildcard import pySim.exceptions (wildcard-import)
pySim/transport/calypso.py:61:23: W0622: Redefining built-in 'type' (redefined-builtin)
pySim/transport/calypso.py:62:8: R1725: Consider using Python 3 style super() without arguments (super-with-arguments)
pySim/transport/calypso.py:73:8: R1725: Consider using Python 3 style super() without arguments (super-with-arguments)
pySim/transport/calypso.py:27:0: W0614: Unused import(s) NoCardError and SwMatchError from wildcard import of pySim.exceptions (unused-wildcard-import)

Change-Id: I6b10d5f3370c00b07288300b537c6f0e17c84a87
2024-02-05 09:52:17 +01:00
Harald Welte
f5a8e70f44 pylint: gsm_r.py
pySim/gsm_r.py:97:0: W0311: Bad indentation. Found 11 spaces, expected 12 (bad-indentation)
pySim/gsm_r.py:32:0: C0411: standard import "from struct import pack, unpack" should be placed before "from pySim.utils import *" (wrong-import-order)
pySim/gsm_r.py:33:0: C0411: third party import "from construct import Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum" should be placed before "from pySim.utils import *" (wrong-import-order)
pySim/gsm_r.py:34:0: C0411: third party import "from construct import Optional as COptional" should be placed before "from pySim.utils import *" (wrong-import-order)
pySim/gsm_r.py:35:0: C0412: Imports from package pySim are not grouped (ungrouped-imports)
pySim/gsm_r.py:30:0: W0611: Unused import enum (unused-import)
pySim/gsm_r.py:32:0: W0611: Unused pack imported from struct (unused-import)
pySim/gsm_r.py:32:0: W0611: Unused unpack imported from struct (unused-import)
pySim/gsm_r.py:39:0: W0611: Unused import pySim.ts_51_011 (unused-import)

Change-Id: I2a3bba5994d0d4d90fcd3f51bee962fec3a8b0dc
2024-02-05 09:52:12 +01:00
Harald Welte
fd9188d306 pylint: cat.py
pySim/cat.py:586:4: W0237: Parameter 'do' has been renamed to 'x' in overriding 'PlmnWactList._from_bytes' method (arguments-renamed)
pySim/cat.py:981:8: W0120: Else clause on loop without a break statement, remove the else and de-indent all the code inside it (useless-else-on-loop)
pySim/cat.py:1000:4: W0221: Number of parameters was 3 in 'TLV_IE_Collection.from_bytes' and is now 2 in overriding 'ProactiveCommand.from_bytes' method (arguments-differ)
pySim/cat.py:1010:12: W0612: Unused variable 'dec' (unused-variable)
pySim/cat.py:1010:17: W0612: Unused variable 'remainder' (unused-variable)
pySim/cat.py:1022:4: W0221: Number of parameters was 2 in 'TLV_IE_Collection.to_bytes' and is now 1 in overriding 'ProactiveCommand.to_bytes' method (arguments-differ)
pySim/cat.py:22:0: C0411: standard import "from typing import List" should be placed before "from bidict import bidict" (wrong-import-order)
pySim/cat.py:26:0: C0411: third party import "from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger" should be placed before "from pySim.utils import b2h, h2b, dec_xplmn_w_act" (wrong-import-order)
pySim/cat.py:27:0: C0411: third party import "from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil" should be placed before "from pySim.utils import b2h, h2b, dec_xplmn_w_act" (wrong-import-order)
pySim/cat.py:28:0: C0411: third party import "from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum" should be placed before "from pySim.utils import b2h, h2b, dec_xplmn_w_act" (wrong-import-order)
pySim/cat.py:23:0: W0611: Unused h2b imported from pySim.utils (unused-import)
pySim/cat.py:26:0: W0611: Unused Bit imported from construct (unused-import)
pySim/cat.py:26:0: W0611: Unused Flag imported from construct (unused-import)
pySim/cat.py:27:0: W0611: Unused Tell imported from construct (unused-import)
pySim/cat.py:27:0: W0611: Unused Padding imported from construct (unused-import)
pySim/cat.py:27:0: W0611: Unused RepeatUntil imported from construct (unused-import)

Change-Id: I0c6327a7a8045736e8678b7286a7ed685c96fb71
2024-02-05 09:52:07 +01:00
Harald Welte
6088b554ef pylint: app.py
pySim/app.py:113:0: C0305: Trailing newlines (trailing-newlines)
pySim/app.py:23:0: W0611: Unused NoCardError imported from pySim.exceptions (unused-import)

Change-Id: I3cac6892f4d3da66f116cecd49f751da227528a4
2024-02-05 09:51:59 +01:00
Harald Welte
eb18ed08b0 pylint: ts_31_102_telecom.py
pySim/ts_31_102_telecom.py:45:0: C0325: Unnecessary parens after '=' keyword (superfluous-parens)
pySim/ts_31_102_telecom.py:33:0: W0401: Wildcard import construct (wildcard-import)
pySim/ts_31_102_telecom.py:76:15: C0121: Comparison 'in_json[srv]['activated'] == True' should be 'in_json[srv]['activated'] is True' if checking for the singleton value True, or 'in_json[srv]['activated']' if testing for truthiness (singleton-comparison)
pySim/ts_31_102_telecom.py:85:23: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_31_102_telecom.py:124:22: W0612: Unused variable 'sw' (unused-variable)
pySim/ts_31_102_telecom.py:32:0: C0411: third party import "from construct import Optional as COptional" should be placed before "from pySim.tlv import *" (wrong-import-order)
pySim/ts_31_102_telecom.py:33:0: C0411: third party import "from construct import *" should be placed before "from pySim.tlv import *" (wrong-import-order)

Change-Id: I4ee0d0e1b5b418b8527b4674141cbaef896a64a2
2024-02-05 09:51:00 +01:00
Harald Welte
33cd964c1a pylint: profile.py
pySim/profile.py:169:0: C0325: Unnecessary parens after 'assert' keyword (superfluous-parens)
pySim/profile.py:189:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/profile.py:197:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/profile.py:27:0: C0411: standard import "import abc" should be placed before "from pySim.commands import SimCardCommands" (wrong-import-order)
pySim/profile.py:28:0: C0411: standard import "import operator" should be placed before "from pySim.commands import SimCardCommands" (wrong-import-order)
pySim/profile.py:29:0: C0411: standard import "from typing import List" should be placed before "from pySim.commands import SimCardCommands" (wrong-import-order)

Change-Id: Ifd55e8ab5ab04da06c8d11e50bc15740580b2900
2024-02-05 09:50:54 +01:00
Harald Welte
e8439d9639 pylint: sms.py
pySim/sms.py:23:0: W0404: Reimport 'Flag' (imported line 23) (reimported)
pySim/sms.py:54:4: C0103: Method name "fromBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:60:4: C0103: Method name "toBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:120:4: C0103: Method name "fromBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:132:4: C0103: Method name "fromSmpp" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:137:4: C0103: Method name "toSmpp" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:141:4: C0103: Method name "toBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:188:4: C0103: Method name "fromBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:192:8: W0612: Unused variable 'flags' (unused-variable)
pySim/sms.py:209:4: C0103: Method name "toBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:228:4: C0103: Method name "fromSmpp" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:236:4: C0103: Method name "fromSmppSubmit" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:279:4: C0103: Method name "fromBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:306:12: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/sms.py:311:12: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/sms.py:319:4: C0103: Method name "toBytes" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:339:4: C0103: Method name "fromSmpp" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:347:4: C0103: Method name "fromSmppSubmit" doesn't conform to snake_case naming style (invalid-name)
pySim/sms.py:373:4: C0103: Method name "toSmpp" doesn't conform to snake_case naming style (invalid-nam

Change-Id: I8082a01443ef568eebda696239572f0af7b56f1b
2024-02-05 09:50:47 +01:00
Harald Welte
8e7d28cad7 pylint: ota.py
pySim/ota.py:21:0: W0401: Wildcard import construct (wildcard-import)
pySim/ota.py:129:8: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
pySim/ota.py:150:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/ota.py:192:8: W0612: Unused variable 'padded_data' (unused-variable)
pySim/ota.py:202:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/ota.py:207:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/ota.py:210:4: C0103: Method name "fromKeyset" doesn't conform to snake_case naming style (invalid-name)
pySim/ota.py:239:8: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/ota.py:242:4: C0103: Method name "fromKeyset" doesn't conform to snake_case naming style (invalid-name)
pySim/ota.py:328:4: W0221: Number of parameters was 4 in 'OtaDialect.encode_cmd' and is now 5 in overriding 'OtaDialectSms.encode_cmd' method (arguments-differ)
pySim/ota.py:392:4: W0221: Number of parameters was 3 in 'OtaDialect.decode_resp' and is now 4 in overriding 'OtaDialectSms.decode_resp' method (arguments-differ)

Change-Id: Icb8d690e541dbaf1406085a8446a0c67641fefff
2024-02-05 09:44:53 +01:00
Harald Welte
cb4c0cf1e8 pylint: exceptions.py
pySim/exceptions.py:27:4: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/exceptions.py:32:4: W0107: Unnecessary pass statement (unnecessary-pass)
pySim/exceptions.py:37:4: W0107: Unnecessary pass statement (unnecessary-pass)

Change-Id: Ibd0725ceb5fdcdc0995c39a4449ada2fd6088552
2024-02-05 09:44:53 +01:00
Harald Welte
c5c9728127 pylint: cards.py
pySim/cards.py:30:0: W0401: Wildcard import pySim.utils (wildcard-import)
pySim/cards.py:41:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/cards.py:55:4: R1711: Useless return at end of function or method (useless-return)
pySim/cards.py:78:8: R1725: Consider using Python 3 style super() without arguments (super-with-arguments)
pySim/cards.py:91:8: R1725: Consider using Python 3 style super() without arguments (super-with-arguments)
pySim/cards.py:159:12: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
pySim/cards.py:28:0: C0411: standard import "import abc" should be placed before "from pySim.ts_102_221 import EF_DIR" (wrong-import-order)
pySim/cards.py:25:0: W0611: Unused Dict imported from typing (unused-import)
pySim/cards.py:28:0: W0611: Unused import abc (unused-import)

Change-Id: I708da28caffb417ed2f8413f9611526b18b29cd4
2024-02-05 09:44:53 +01:00
Harald Welte
f57912ea15 pylint: gsmtap.py
pySim/gsmtap.py:28:0: W0401: Wildcard import construct (wildcard-import)
pySim/gsmtap.py:26:0: W0611: Unused List imported from typing (unused-import)
pySim/gsmtap.py:26:0: W0611: Unused Dict imported from typing (unused-import)
pySim/gsmtap.py:26:0: W0611: Unused Optional imported from typing (unused-import)

Change-Id: I53739874edef0a9ae25a8599e7cc7eee9dedb703
2024-02-05 09:44:53 +01:00
Harald Welte
0f2ac70397 pylint: card_key_provider.py, card_handler.py, iso7816_4.py, jsonpath.py
pySim/card_key_provider.py:57:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)
pySim/card_key_provider.py:61:0: C0325: Unnecessary parens after 'if' keyword (superfluous-parens)

pySim/card_handler.py:100:0: C0325: Unnecessary parens after '=' keyword (superfluous-parens)
pySim/card_handler.py:100:24: C0121: Comparison 'self.cmds.get('verbose') == True' should be 'self.cmds.get('verbose') is True' if checking for the singleton value True, or 'bool(self.cmds.get('verbose'))' if testing for truthiness (singleton-comparison)
pySim/card_handler.py:29:0: C0411: standard import "import subprocess" should be placed before "from pySim.transport import LinkBase" (wrong-import-order)
pySim/card_handler.py:30:0: C0411: standard import "import sys" should be placed before "from pySim.transport import LinkBase" (wrong-import-order)
pySim/card_handler.py:31:0: C0411: third party import "import yaml" should be placed before "from pySim.transport import LinkBase" (wrong-import-order)

pySim/iso7816_4.py:20:0: W0401: Wildcard import construct (wildcard-import)

pySim/jsonpath.py:1:0: C0114: Missing module docstring (missing-module-docstring)
pySim/jsonpath.py:6:0: W0105: String statement has no effect (pointless-string-statement)
pySim/jsonpath.py:2:0: W0611: Unused import json (unused-import)
pySim/jsonpath.py:3:0: W0611: Unused import pprint (unused-import)

Change-Id: I780595d69000f727ad0fbaff4b89918b91b3122e
2024-02-05 09:44:18 +01:00
Harald Welte
62bd7d3df2 global_platform: Add DEK (key) encryption support
Change-Id: I940cc2e16a1d3e3cdef4ebcf3f15fc2c8de21284
2024-02-05 01:45:02 +01:00
Harald Welte
2bb2ff4aeb global_platform: INSTALL [for install] support
Change-Id: I4c1da90f1aa8ad9609602272f374078d1e1faa11
2024-02-05 01:41:49 +01:00
Harald Welte
7156a40187 construct: Add StripTrailerAdapter
In smart cards, we every so often encounter data types that contain a
bit-mask whose length depends on whether or not there are any of the
least-significant bits are set.  So far we worked around this with
some kind of Struct('byte1', 'byte2'/COptional, 'byte3'/COptional)
approach.

Let's do thisin a generic way using the new StripTrailerAdapter.

Change-Id: I659aa7247c57c680895b0bf8412f9e477fc3587d
2024-02-05 01:39:39 +01:00
Harald Welte
cd8e16fdfe global_platform: KCV support for PUT KEY
GlobalPlatform requires the use of the KCV for DES + AES keys. Let's
implement that.

(11.8.2.3.3: "For all key types described in section B.6, the Key Check
Value shall be present.")

Change-Id: Ief168a66dee58b56f4126db12829b3a98906c8db
2024-02-04 21:27:00 +01:00
Harald Welte
e55fcf66bf Be more conservative in our imports
Try to avoid '*' from anything into various modules, polluting the
namespace.

Change-Id: Iba749d18e1863ded88ba2d2183e2e8d718b2d612
2024-02-04 21:27:00 +01:00
Harald Welte
bc8e2e1664 contrib/jenkins.sh: include tests/*.py in pylint
Change-Id: I9c8113acf5341b198d91040710b6b10cb2b6ef38
2024-02-04 21:27:00 +01:00
Harald Welte
57f73f8de7 make our tests pass pylint
Change-Id: If3a9f178c3f915123178efe00269fce74f6e585d
2024-02-04 21:27:00 +01:00
Harald Welte
af8826a02b Implement Global Platform SCP03
This adds an implementation of the GlobalPlatform SCP03 protocol. It has
been tested in S8 mode for C-MAC, C-ENC, R-MAC and R-ENC with AES using
128, 192 and 256 bit key lengh.  Test vectors generated while talking to
a sysmoEUICC1-C2T are included as unit tests.

Change-Id: Ibc35af5474923aed2e3bcb29c8d713b4127a160d
2024-02-04 17:56:59 +01:00
Harald Welte
13a1723c2e rename global_platform.scp02 to global_platform.scp
This is in preparation of extending it to cover SCP03 in a follow-up
patch.

Change-Id: Idc0afac6e95f89ddaf277a89f9c95607e70a471c
2024-02-04 17:56:59 +01:00
Harald Welte
afd89ca36d Contstrain argparse integers to permitted range
In many casese we used type=int permitting any integer value, positive
or negative without a constratint in size.  However, in reality often
we're constrained to unsigned 8 or 16 bit ranges.  Let's use the
auto_uint{8,16} functions to enforce this within argparse before
we even try to encode something that won't work.

Change-Id: I35c81230bc18e2174ec1930aa81463f03bcd69c8
2024-02-04 17:56:59 +01:00
Harald Welte
a30ee17246 global_platform: Fix --key-id argument
The key-id is actually a 7-bit integer and on the wire the 8th bit
has a special meaning which can be derived automatically.

Let's unburden the user from explicitly encoding that 8th bit and
instead set it automatically.

Change-Id: I8da37aa8fd064e6d35ed29a70f5d7a0e9060be3a
2024-02-04 17:56:59 +01:00
Harald Welte
bdf8419966 global_platform: add delete_key and delete_card_content
This GlobalPlatform command is used to delete applications/load-files
or keys.

Change-Id: Ib5d18e983d0e918633d7c090c54fb9a3384a22e5
2024-02-04 17:56:59 +01:00
Harald Welte
a7eaefc8d9 global_platform: add set_status command
Using this command, one can change the life cycle status of on-card
applications, specifically one can LOCK (disable) them and re-enable
them as needed.

Change-Id: Ie14297a119d01cad1284f315a2508aa92cb4633b
2024-02-04 17:56:59 +01:00
Harald Welte
4d5fd25f31 global_platform: Add install_for_personalization command
This allows us to perform STORE DATA on applications like ARA-M/ARA-D
after establishing SCP02 to the related security domain.

Change-Id: I2ce766b97bba42c64c4d4492b505be66c24f471e
2024-02-04 17:56:59 +01:00
Harald Welte
321973ad20 pySim-shell: Make 'apdu' command use logical (and secure) channel
The 'apdu' command so far bypassed the logical channel and also
the recently-introduced support for secure channels.  Let's change
that, at least by default.  If somebody wants a raw APDU without
secure / logical channel processing, they may use the --raw option.

Change-Id: Id0c364f772c31e11e8dfa21624d8685d253220d0
2024-02-04 17:43:11 +01:00
Harald Welte
41a7379a4f Introduce GlobalPlatform SCP02 implementation
This implementation of GlobalPlatform SCP02 currently only supports
C-MAC and C-ENC, but no R-MAC or R-ENC yet.

The patch also introduces the notion of having a SCP instance associated
with a SimCardCommands instance.  It also adds the establish_scp0w and
release_scp shell commands to all GlobalPlatform Security Domains.

Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27
2024-02-04 17:42:30 +01:00
Harald Welte
762a72b308 global_platform 'put_key': constrain ranges of KVN + KID in argparse
The earlier we catch errors in user input, the better.

Change-Id: Icee656f1373a993b6883ffaab441fe178c0fe8cb
2024-02-03 13:32:41 +01:00
Harald Welte
a2f1654051 move global_platform.py to global_platform/__init__.py
This will allow us to have multiple different modules for different
aspects of global_platform.

Change-Id: Ieca0b20c26a2e41eb11455941164474b76eb3c7a
2024-02-01 12:06:07 +01:00
Harald Welte
eecef54eee commands.py: Wrap the transport send_apdu* methods
Let's not have higher level code directly call the transports send_apdu*
methods.  We do this as a precursor to introducing secure channel
support, where the secure channel driver would add MAC and/or encrypt
APDUs before they are sent to the transport.

Change-Id: I1b870140959aa8241cda2246e74576390123cb2d
2024-02-01 12:06:07 +01:00
Harald Welte
5918345c78 global_platform: implement GET STATUS command
The GlobalPlatform GET STATUS command is used to display information
about ISD / Applications / ExecutabLoad Files / Modules on the card.

Change-Id: Ic92f96c1c6a569aebc93a906c62a43b86fe3b811
2024-01-31 22:24:42 +01:00
Harald Welte
93bdf00967 pySim.esim: Add class for parsing/encoding eSIM activation codes
Change-Id: I2256722c04b56e8d9c16a65e3cd94f6a46f4ed85
2024-01-30 21:33:41 +01:00
Harald Welte
d7715043a3 osmo-smdpp: Add more GSMA TS.48 test profiles
We now have all v1, v2, v3, v4 and v5 profiles from
https://github.com/GSMATerminals/Generic-eUICC-Test-Profile-for-Device-Testing-Public

Change-Id: Ia204c055b9d04552ae664130ff8ae4fc4163b88c
2024-01-30 21:33:41 +01:00
Harald Welte
8a39d00cc3 osmo-smdpp: Support multiple different profiles
Let's simply use the matchingId for filesystem lookup of the UPP file.

This way we can have any number of profiles by simply creating the
respeective files.

Change-Id: I0bc3a14b9fdfcc6322917dd0c69d8295de486950
2024-01-30 21:33:41 +01:00
Harald Welte
3f3fd1a841 add SAIP template handling + v3.1 definitions
This adds classes for describing profile templates as well
as derived classes defining the profile templates of the
"Profile Interoperability Technical Specification", specifically
it's "ANNEX A (Normative): File Structure Templates Definition"

We need a machine-readable definition of those templates, so
we can fully interpret an unprotected profile package (UPP),
as the UPP usually only contains the increment/difference to
a given teplate.

Change-Id: I79bc0a480450ca2de4b687ba6f11d0a4ea4f14c8
2024-01-29 09:23:36 +01:00
Harald Welte
263e3094ba requirements.txt: Switch to osmocom fork of asn1tools
This is sadly required as the Interoperable Profile format must process
elements of an ASN.1 sequence in order, which doesn't work if the parser
puts the elements in a python dict.

The osmocom fork of asn1tools hence uses OrderedDict to work around
this problem.

Change-Id: Id28fcf060f491bb3d76aa6d8026aa76058edb675
2024-01-29 09:21:57 +01:00
Harald Welte
e815e79db9 esim.saip: More type annotations
Change-Id: Ib549817ee137bab610aea9c89a5ab86c2a7592ea
2024-01-29 09:21:53 +01:00
Harald Welte
9f55da998f esim.saip: Move OID to separate sub-module
This helps us to prevent circular imports in follow-up code.

Change-Id: I94f85f2257d4702376f4ba5eb995a544a2e53fd3
2024-01-29 08:06:12 +01:00
Harald Welte
488427993d saip.personalization: Fix ICCID fillFileContent replacement
Change-Id: Ic267fdde3b648b376ea6814783df1e90ea9bb9ad
2024-01-28 22:03:11 +01:00
Harald Welte
0bce94996f saip.personalization: Also drop any fillFileOffset
When replacing a file's contents, we must not just remove any
fillFileContent tuples, but also the fillFileOffset.

Change-Id: I3e4d97ae9de8a78f7bc0165ece5954481568b800
2024-01-28 22:03:11 +01:00
Harald Welte
3d6df6ce13 [cosmetic] ara_m: Give a spec reference for the PERM-AR-DO
PERM-AR-DO actually originates in a different spec than all other parts
of the ara_m.py, so let's explicitly mention that.

Change-Id: I6e0014c323f605860d0f70cd0c04d7e461e8a9de
2024-01-27 20:50:12 +00:00
Harald Welte
7f2263b4a0 runtime: Reset selected_file_fcp[_hex] if SELECT returns no data
In case SELECT doesn't return any response data, we must reset
the lchan.selected_file_fcp* members to None to prevent pySim-shell
preventing stale data from the previously selected file.

Change-Id: Ia04b8634e328e604e8df7e8d59b7fd532242d2ca
2024-01-27 21:47:13 +01:00
Harald Welte
9b1a9d9b2e ara_m: Use GlobalPlatform SELECT decoding
As the ARA-M applet is a GlobalPlatform applet, its SELECT response
decoding should be used, not the ETSI EUICC TS 102 221 fall-back.

Change-Id: I1a30b88a385f6de663aa837483dd32c0d104856f
2024-01-27 21:47:13 +01:00
Harald Welte
5e0439f881 ara_m: Permit encoding of empty AID (--aid '') in ARA-M rules
Encoding an empty AID-REF-DO (4F) is neccessary to achieve the meaning
described in "Secure Element Access Control - Public Release v1.0"
Table 6-1: "Empty: Indicates that the rules to be stored or retrieved
are associated with all SE applications not covered by a specific rule".

Change-Id: Iac6c3d78bc9ce36bac47589e5f7a0cc78e2efc38
2024-01-27 21:47:13 +01:00
Harald Welte
9fd4bbe42e osmo-smdpp: Constrain selection of CI certificate
We can only choose a CI certificate which is supported both by the eUICC
as well as which has signed our own SM-DP+ certificates.

Change-Id: I0b9130f06d501ca7d484063d56d606cfdd2544f4
2024-01-25 19:16:57 +01:00
Harald Welte
18d0a7de96 global_platform: Add shell command for PUT KEY
This command is used for installation of GlobalPlatform keys.  We only
implement the command without secure messaging at this point, as it is
used during card personalization.  Authentication will later be handled
by generic implementations of SCP02 and/or SCP03.

Change-Id: Icffe9e7743266d7262fbf440dd361b21eed7c5cf
2024-01-25 19:16:57 +01:00
Harald Welte
280a9a3408 docs: Add missing global_platform store_data command docs
In If30c5d31b4e7dd60d3a5cfb1d1cbdcf61741a50e we introduced a store_data
comamnd, but forgot to add it to the pySim-shell manual.

Change-Id: I6039818c2c0c5373b4a4ef1e33e152de7fbbd01a
2024-01-25 19:16:57 +01:00
Harald Welte
e6124b0aba add contrib/eidtool.py: Tool for checking + computing EID checksum
Change-Id: If6c560a2de51718948fb99b816e080f2aff4d0ed
2024-01-25 19:16:57 +01:00
Harald Welte
6dadb6c215 docs: Update osmo-smdpp with pointer to sysmoEUICC1-C2T and SGP.26
Change-Id: Id031ca48549a3c2ac21c93a169262570843d8e2d
2024-01-25 19:16:57 +01:00
Harald Welte
af87cd544f osmo-smdpp: Implement eUICC + EUM certificate signature chain validation
Change-Id: I961827c50ed5e34c6507bfdf853952ece5b0d121
2024-01-22 19:08:09 +01:00
Harald Welte
45b7dc9466 Move X.509 related code from osmo-smdpp to pySim.esim.x509_cert
Change-Id: I230ba0537b702b0bf6e9da9a430908ed2a21ca61
2024-01-22 17:57:55 +01:00
Harald Welte
c83a963877 New pySim.esim.x509_cert module for X.509 certificate handling
Change-Id: Ia8cc2dac02fcd96624dc6d9348f103373eeeb614
2024-01-22 17:57:55 +01:00
Harald Welte
667d589f20 pySim.utils: Support datetime.datetime in JsonEncoder
Change-Id: I6223475cec8eb45c6fc4278109ad9dd1cb557800
2024-01-18 16:58:48 +01:00
Harald Welte
ebb6f7f938 osmo-smdpp: Actually dump Rx/Tx JSON in JSON format and not as python dict
Change-Id: Ieea3fd2d0f0239acfa6a5c4cfdbfd558d1a3e0ea
2024-01-18 16:58:48 +01:00
Harald Welte
0311c92e96 Fix encoding of decoded/json data in update_{record_binary}_decoded
The patch introducing the is_hexstr type into the argparser was
accidentially also introduce in two locations where we actually don't
expect a hex-string.

This is a partial revert of I6426ea864bec82be60554dd125961a48d7751904

Change-Id: I3c3d2a2753aa7a2566a3b1add7ba70c86499d293
Closes: #6331
2024-01-18 16:58:48 +01:00
Harald Welte
66b337079a pySim-shell: Permit 'reset' command also in unqeuipped stage
If we are not 'equipped' as we could not detect any known applications
on the card, we used to only permit the 'apdu' command. However, we
should also permit the 'reset' command, as it also is something that's
possible with ever card, even of unknown types.

Change-Id: I23199da727973d7095ac18031f49e1e8423aa287
2024-01-16 18:48:52 +00:00
Harald Welte
4f3d11b378 euicc: Implement EID checksum verification + computation
Change-Id: I2cb342783137ee7e4b1be3b14e9c3747316f1995
2024-01-16 19:04:19 +01:00
Harald Welte
cd18ed0a82 ts_102_221: Better explain 'selected file invalidated'
Some specs call it 'invalidated', others call it 'deactivated'.  If the
user is unfamiliar with this, the error message about "invalidated"
might not be obvious enough; let's also mention 'deactivated' in the
message and explicitly mention that it needs to be activated before use.

Change-Id: I91488b0e7dc25a8970022b09e575485a4165eefa
2024-01-16 19:04:10 +01:00
Harald Welte
ecfb09037e global_platform: More definitions to support key loading
With the definitions from this commit, we can build key loading
TLVs, which is used to load ECC keys into eUICCs.

Change-Id: I853c94d37939ef3dd795f893232b0276a5a4af81
2024-01-14 18:21:57 +01:00
Harald Welte
1f7a9bd5b4 TLV: Add DGI encoding of "GP Scripting Language Annex B"
The DGI encoding is specified in Annex B of the
"GlobalPlatform Systems Scripting Language Specification v1.1.0"

which is an "archived" specification that is no longer published
by GlobalPlatform, despite it being referenced from the GlobalPlatform
Card Specification v2.3, which is the basis of the GSMA eSIM
specifications.

For some reason it was the belief of the specification authors that
yet another format of TLV encoding is needed, in addition to the BER-TLV
and COMPREHENSION-TLV used by the very same specifications.

The encoding of the tag is not really specified anywhere, but I've only
seen 16-bit examples.  The encoding of the length is specified and
implemented here accordingly.

Change-Id: Ie29ab7eb39f3165f3d695fcc1f02051338095697
2024-01-14 17:42:01 +01:00
Harald Welte
d5be46ae7e global_platform: Implement generic store_data command
Change-Id: If30c5d31b4e7dd60d3a5cfb1d1cbdcf61741a50e
2024-01-14 17:32:53 +01:00
Harald Welte
7ba09f9392 euicc: Migrate ECASD + ISD-R over to global_platform.CardApplicationSD
Actually, the GSMA eUICC is a kind of derivative of a GlobalPlatform
card, and the ECASD and ISD-R are security domains.  As such, we
should make them derived classes of global_platform.CardApplicationSD
which means they inherit some of the shared shell_commands etc.

Change-Id: I660e874d9bcbb8c28a64e4ef82dc53bee97aacfc
2024-01-12 10:02:54 +01:00
Harald Welte
91842b471d Constrain user input to hex-string in argparse
We do have an is_hexstr function which we should use anywhere
where we expect the user to input a string of hex digits.  This way
we validate the input before running in some random exception.

Change-Id: I6426ea864bec82be60554dd125961a48d7751904
2024-01-12 10:02:54 +01:00
Harald Welte
d1cc8d0c1d euicc: Fix decoding of SubjectKeyIdentifier.
There's actually no additional TLV structure inside the Tag 0x04.

Change-Id: Ic922355308747a888083c5b26765d272b6b20bd0
2024-01-09 23:35:10 +01:00
Harald Welte
f2bcb44ccc pySim.saip.*: Support for parsing / operating on eSIM profiles
This commit introduces the capability to parse and encode
SimAlliance/TCA "Interoperable Profiles" and apply personalization
operations on them.

Change-Id: I71c252a214a634e1bd6f73472107efe2688ee6d2
2024-01-09 21:37:12 +00:00
Harald Welte
5bbb144a31 Initial proof-of-concept SM-DP+ for GSMA consumer eSIM RSP
This commit introduces

* the osmo-smdpp.py program implementing the main procedures and the
  HTTP/REST based ES9+
* python modules for ES8+ and non-volatile RSP Session State storage
* the ASN.1 source files required to parse/encode RSP
* 3GPP test certificates from SGP.26
* an unsigned profile package (UPP) of a SAIP v2.3 TS48 test profile

As I couldn't get the 'Klein' tls support to work, the SM-DP+ code
currently does not support HTTPS/TLS but plan HTTP, so you either have
to modify your LPA to use HTTP instead of HTTPS, or put a TLS proxy in
front.

I have successfully installed an eSIM profile on a test eUICC that
contains certificate/key data within the test CI defined in GSMA SGP.26

Change-Id: I6232847432dc6920cd2bd08c84d7099c29ca1c11
2024-01-09 21:37:12 +00:00
Harald Welte
e76fae9c4c pySim-shell: Update manual with examples for using with eUICC ISD-R
Change-Id: I4a0acdad5c7478ee76f92c7610c0e2a5331dea46
2024-01-08 20:56:32 +00:00
Harald Welte
c499dc79a8 euicc: Fix eUICC list_notifications command
Prior to this patch, the command would always raise exceptions.

Change-Id: I75a7840c3f4b68bfc164a43908b100dd6e41e575
2024-01-08 12:10:22 +00:00
Harald Welte
0002789a88 euicc: Fix delete_profile command
Contrary to {enable,disable}_profile, the delete_profile does not use
the ProfileIdentifier TLV, but directly the Iccid / IsdpAid.

Change-Id: I43e298524048703264e16cbdd0b76d82ba976985
2024-01-08 12:10:17 +00:00
Harald Welte
cfa62cb95b Allow logger to do lazy evaluation of format strings
Change-Id: I39d26cdd5b85a61a06fd8c7a9d0a046e398819bd
2024-01-08 12:10:11 +00:00
Harald Welte
d657708df2 add contrib/unber.py utility
This tool is a replacement for asn1c 'unber' program with a much more
useful/readable output:
* contains hexadecimal raw tag values
* contains hexdump of value, rather than HTML entities in pseudo-XML

Change-Id: I22c1a461ccba04c2c8caaab7ca29ea6ae76e2ea3
2024-01-08 11:18:49 +00:00
Harald Welte
242197b53d Add pySim.esim.bsp module implementing BSP (BPP Protection Protocol)
This is the protocol used for the ES8+ interface between SM-DP+ and the
eUICC in the GSMA eSIM system.

Change-Id: Ic461936f2e68e1e6f7faab33d06acf3063e261e7
2024-01-07 10:22:04 +01:00
Harald Welte
5b623a1247 ts_102_310: Add file definitions resembling ETSI TS 102 310 (EAP)
The definitions are not used yet, as one would have to add that
dynamically based on which EF.DIR entries contain the 0x73 discretionary
template.  As I don't have any cards implementing this so far, I'll skip
that part.

Change-Id: I532ff2c94021ab1b4520fe2b6988c8960319d208
2024-01-04 21:50:38 +01:00
Harald Welte
62e570b620 ts_31_103: Add TLV + construct for EF_NAFKCA
Change-Id: I124064994eb695790e9a3aff40be8139b3a2f2cf
2024-01-04 21:27:39 +01:00
Harald Welte
4fe7de8568 ts_31_103: Add construct for EF.GBABP and EF.GBANL
Change-Id: Ife06f54c2443f3e048bd36f706f309843703403a
2024-01-04 21:27:39 +01:00
Harald Welte
b0c9ccba66 construct: avoid StreamError exceptions due to files containing all-ff
In smart cards, files/records containing all-ff means they are simply
not used/initialized.  Let's avoid raising exceptions when interpreting
0xff as length value and reading less bytes as value.

Change-Id: I09c3cb82063fc094eb047749996a6eceff757ea2
2024-01-04 21:20:19 +01:00
Harald Welte
e13403b206 ts_31_102: Start to use construct for EF.SUCI_Calc_Info
We cannot fully switch to construct for all of it easily due to
the priority value and the ordering/sorting by priority implemented
in the hand-coded version.  But we can at least migrate the
encode/decode of the hnet_pubkey_list via construct.

Change-Id: I4ad5ea57bab37c2dc218e7752d538aa4cdc36ee3
2024-01-04 20:13:06 +01:00
Harald Welte
9a48aea263 fileystem/tlv: remove unused imports
Change-Id: I519c7792c7fbe18be63ddc77d211f0d034afcd1f
2024-01-02 21:13:30 +01:00
Harald Welte
19d2b93d7e move SUCI sub-classes to EF_SUCI_CalcInfo
Change-Id: Iea6b176327881ff9414f4fe624e94811f9782927
2023-12-29 18:51:25 +01:00
Harald Welte
9d607978fa global_platform: Add support for more GET DATA TLVs
Example:

pySIM-shell (00:MF/ADF.ISD)> get_data extended_card_resources_info
{
    "extended_card_resources_info": [
        {
            "number_of_installed_app": 8
        },
        {
            "free_non_volatile_memory": 354504
        },
        {
            "free_volatile_memory": 10760
        }
    ]
}

Change-Id: I129e43c377b62dae1b9a88a0a2dc9663ac2a97da
2023-12-29 18:51:25 +01:00
Harald Welte
1c0a249131 commands: Ignore exceptions during READ while UPDATE
If we are reading a file to check if we can skip the write to conserve
writes, don't treat exceptions as fatal.  The file may well have the
access mode in a way that permits us to UPDATE but not to READ.  Simply
fall-back to unconditional UPDATE in this case.

Change-Id: I7bffdaa7596e63c8f0ab04a3cb3ebe12f137d3a8
2023-12-29 18:51:25 +01:00
Harald Welte
db1684df04 sysmocom_sja2: Implement EF_CHV files using construct
this has the advantage of getting the encoder for free (so far we only
had the decoder).  While at it, also add some tests data for the unit
tests.

Change-Id: Ifb8caf5cd96706d7fb6b452d6552b115c0828797
2023-12-29 18:51:25 +01:00
Harald Welte
ce01f48b00 test_files: Test decoder also with ff-padded input
It's customary in the SIM card universe to right-pad data with ff bytes.
So far we only test decoders without such padding, which is unrealistic.
Let's also tests the decoders with extra 'ff' padding present.

For some files this doesn't make sense, so we add a _test_no_pad class
attribute that can be spcified to prevent this new "test with ff-padding"
from being executed for the test data of the class.

Change-Id: I7f5cbb4a6f91040fe9adef9da0a1f30f9f156dae
2023-12-29 18:51:25 +01:00
Harald Welte
bcd261583c tests_files.py: Reduce code duplication
Change-Id: Ib84a0ae35262a19fce3e688afe8e1678a4c59eba
2023-12-29 18:51:25 +01:00
Harald Welte
69bdcf5022 Fix TLV_IE_Collection.from_tlv in certain situations
The existing code used to produce an empty output in situations where a
TLV_IE_Collection would be parsed from a single TLV only with some
additional trailing padding:

>>> from pySim.utils import h2b
>>> from pySim.ts_31_102 import EF_CSGT
>>> t = EF_CSGT.Csgt_TLV_Collection()
>>> t.from_tlv(h2b('8906810300666f6fff'))
[TextCsgType(foo)]
>>> t.to_dict()
[]

This was caused by an early return (actually returning the decoded
result) but *without updating self.children*.

Change-Id: I1c84ccf698c6ff7e7f14242f9aaf7d15ac2239f4
2023-12-29 18:51:25 +01:00
Harald Welte
a77f7e1eb9 ts_31_102: Implement decoders/encoders for EFs below DF.HNB
These files are mostly related to CSG (Closed Subscriber Group)
in the context of HomeNodeB (HNB), aka femtocells.

Change-Id: Ie57963381e928e2c1da408ad46549a780056242a
2023-12-29 18:51:25 +01:00
Harald Welte
6e6caa8b4a support UCS-2 characters in EF.MMSUP, EF.ADN, EF.SPN, EF.PNN, EF.ECC
Now that we have support for the UCS-2 encoding as per TS 102 221 Annex A,
we can start to make use of it from various file constructs.

As some specs say "Either 7-bit GSM or UCS-2" we also introduce
a related automatic GsmOrUcs2Adapter and GsmOrUcs2String class.

Change-Id: I4eb8aea0a13260a143e2c60fca73c3c4312fd3b2
2023-12-29 18:51:25 +01:00
Harald Welte
f6fceb8684 Implement convoluted encoding of UCS-2 as per TS 102 221 Annex A
TS 102 221 Annex A defines three variants of encoding UCS-2 characters
into byte streams in files on UICC cards: One rather simplistic one, and
two variants for optimizing memory utilization on the card.

Let's impelement a construct "Ucs2Adapter" class for this.

Change-Id: Ic8bc8f71079faec1bf0e538dc0dfa21403869c6d
2023-12-29 18:51:21 +01:00
Harald Welte
842fbdb15d add PlmnAdapter for decoding PLMN bcd-strings like 262f01 to 262-01
The human representation of a PLMN is usually MCC-MNC like 262-01
or 262-001.  Let's add a PlmnAdapter for use within construct, so we
can properly decode that.

Change-Id: I96f276e6dcdb54a5a3d2bcde5ee6dbaf981ed789
2023-12-28 08:08:54 +01:00
Harald Welte
dffe7af578 Fix enumeration of GlobbalPlatformISDR during card_init()
We used __subclasses__(), but this only returns the immediate
subclasses and not all further/nested subclasses.  Instead, we must
use the pySim.utils.all_subclasses() function to really get all of them.

The hack to use the method signature of the constructor to determine if
it's an intermediate class didn't work, as even GlobbalPlatformISDR
has a optional argument for non-default AIDs.  So let's introduce an
explicit class attribute for that purpose.

Change-Id: I7fb1637f8f7a149b536c4d77dac92736c526aa6c
2023-12-27 22:17:38 +01:00
Harald Welte
722c11a7e9 global_platform: Add support for key types of v2.3.1 (including AES)
Change-Id: Iae30f18435c2b0a349bfd9240b9c7cca06674534
2023-12-27 15:16:03 +00:00
Harald Welte
45626271cf global_platform: Add TLV test data for Key Information Data
Change-Id: Ib7b73cb28abea98986a66264a0779263873d7fb2
2023-12-27 15:15:58 +00:00
Harald Welte
2538dd7621 global_platform: Correctly decode Key Information Data
The list contains tuples of (key_type, key_length). Let's fix that.

Change-Id: Icf367827d62ed67afa27ee3d0ba9d5cd5bc65c99
2023-12-27 15:15:54 +00:00
Harald Welte
ee6a951774 Add TLV decoder test data
This adds some first test data for the new unitdata driven test cases
for the TLV encoder/decoder.

It also fixes a bug in the ts_102_221.FileDescriptor decoder for BER-TLV
structured files which was found and fixed while introducing the test
data.

Related: OS#6317
Change-Id: Ief156b7e466a772c78fb632b2fa00cba2eb1eba5
2023-12-27 15:15:24 +00:00
Harald Welte
2a36c1b921 data-driven TLV unit data test support
While we do have the _test_de_encode data driven tests for file
definitions, we don't yet have something similar for derived classes of
BER_TLV_IE. This means that TLVs used outside of the filesystem context
(for example, decoding the SELECT/STATUS response, but also eUICC and
other stuff) do not yet have test coverage.

This commit just adds the related test code, but no test data yet.

Related: OS#6317
Change-Id: Ied85f292bb57fde11dc188be84e3384dc3ff1601
2023-12-27 15:15:17 +00:00
Harald Welte
a9b21bdb1f tlv: Fix from_dict() symmetry
the to_dict() method generates a {class_name: value} dictionary,
for both the nested and non-nested case.  However, before this patch,
the from_dict() method expects a plain list of child IE dicts
in the nested case.  This is illogical.

Let's make sure from_dict always expectes a {class_name: value} dict
for both nested and non-nested situations.

Change-Id: I07e4feb3800b420d8be7aae8911f828f1da9dab8
2023-12-27 15:14:48 +00:00
Harald Welte
a5eb924f9e filesystem: use pySim.utils.build_construct()
We recently introduced a pySim.utils.build_construct() wrapper around
the raw call of the construct.build() method.  So far, this wrapper
was only used from pySim.tlv, but let's also use it from
pySim.filesystem.

Basically, whenever we use parse_construct(), we should use
build_construct() as the inverse operation.

Change-Id: Ibfd61cd87edc72882aa66d6ff17861a3e918affb
2023-12-23 17:49:01 +01:00
Harald Welte
a4b9bdf238 pySim-trace_test.sh: Force termcolor to suppress color generation
on some systems, the output would otherwise contain colored status
words, which in turn mean the test otuput no longer matches the expected
output.

Change-Id: Icb700f6e85a285748e00367a398975aa5e75dec5
2023-12-23 10:38:21 +01:00
Harald Welte
caef0df663 construct/tlv: Pass optional 'context' into construct decoder/encoder
The context is some opaque dictionary that can be used by the
constructs; let's allow the caller of parse_construct,  from_bytes,
from_tlv to specify it.

Also, when decoding a TLV_IE_Collection, pass the decode results of
existing siblings via the construct.

Change-Id: I021016aaa09cddf9d36521c1a54b468ec49ff54d
2023-12-23 09:15:47 +00:00
Harald Welte
188869568a docs/shell: extend the introduction part; link to video presentation
Change-Id: I77c30921f2b8c002c9dda244656c348c96b41f06
2023-12-23 09:14:59 +00:00
Harald Welte
324175f8bd additional encode/decode test data for various files
Change-Id: Ib563a2204922d2013b5f7c5abde0773051e17938
2023-12-23 08:20:42 +01:00
Harald Welte
5376251993 31.102 + 51.011: Fix encode/decode of EF.CFIS
The EF.CFIS definition is not identical to EF.ADN, so we cannot recycle
the EF.ADN class to decode EF.CFIS.

Change-Id: Idcab35cbe28332e3c8612bcb90226335b48ea973
2023-12-23 08:20:42 +01:00
Harald Welte
542dbf6771 fix encode/decode of xPLMNwAcT
There are some pretty intricate rules about how GSM and E-UTRAN are
encoded, let's make sure we fully  support both as per 3GPP TS 31.102
Release 17.  As part of this, switch to a sorted list of access technologies,
in order to have a defined order.  This makes comparing in unit tests
much easier.  However, it also means that we need to sort the set
when printing the list of AcT in pySim-read to generate deterministic
output.

Change-Id: I398ac2a2527bd11e9c652e49fa46d6ca8d334b88
2023-12-23 08:20:42 +01:00
Harald Welte
e45168ef29 test/test_files: set maxDiff attribute
Without this the diff between expected and actual output is truncated
and one instead reads the following output:

	Diff is 844 characters long. Set self.maxDiff to None to see it.

We actually want to see the full diff to see what's not matching.

Change-Id: I6e89705061454191b6db1255de7fe549ad720800
2023-12-22 09:13:10 +01:00
Harald Welte
2822dca9ec tests: use case-insensitive compare of hex strings
Change-Id: I080f6e173fec40c27dd3ebbf252eaddf5a0e15ba
2023-12-22 09:13:10 +01:00
Harald Welte
0ecbf63a02 transport: Extend the documentation for each transport driver
This driver description we add to the code is automatically added to the
respective user manual sections.

Change-Id: I8807bfb11f43b167f1321d556e09ec5234fff629
2023-12-21 12:33:12 +01:00
Harald Welte
baec4e9c81 transport: Move printing of reader number/name to generic code
Let's avoid copy+pasting print statements everywhere.  The instances
do already have a __str__ method for the purpose of printing their name in a
generic way.

Change-Id: I663a9ea69bf7e7aaa6502896b6a71ef692f8d844
2023-12-21 12:33:12 +01:00
Harald Welte
ad002797e2 transport/pcsc: Allow opening PC/SC readers by a regex of their name
Opening PC/SC readers by index/number is very error-prone as the order
is never deterministic in any system with multiple (hot-plugged, USB)
readers.  Instead, let's offer the alternative of specifying a regular
expression to match the reader name (similar to remsim-bankd).

Change-Id: I983f19c6741904c1adf27749c9801b44a03a5d78
2023-12-21 12:33:12 +01:00
Harald Welte
0f177c1d29 transport: Pass argparse.Namespace directly into transport classes
It's odd that the individual transport driver specifies their argparse
options but then the core transport part evaluates them individually.
This means we cannot add new options within a transport.

Let's pass the Namespace instance into the constructor of the
specific transport to improve this.

Change-Id: Ib977007dd605ec9a9c09a3d143d2c2308991a12c
2023-12-21 11:31:57 +00:00
Harald Welte
c108595041 move {enc,dec}_addr_tlv functions from pySim.util to pySim.legacy.util
In the previous commit we've stopped using those functions from modern
pySim-shell code.  Hence, the only remaining user is the legacy tools,
so we can move the code to the legacy module.

Change-Id: I6f18ccb36fc33bc204c01f9ece135676510e67ec
2023-12-17 10:46:31 +00:00
Harald Welte
301d6ed14a isim: Replace legacy imperative address TLV encoder/decoder with construct
We've recently introduced IPv{4,6}Adapter construct classes and can
switch to this instead of using the old imperative encoder/decoder
functions {enc,dec}_addr_tlv().

Aside from code cleanup, this also means we now support the IPv6 address
type in EF.PCSCF.

Change-Id: I4d01ccfe473a8a80fbee33fdcbd8a19b39da85ac
2023-12-17 10:46:31 +00:00
Harald Welte
b3c46135bb bertlv_parse_len: Fix input data is smaller than num length octets
This can happen if there's a file with invalid encoding on the card,
such as a tag followed by all-ff.  Let's gracefully ignore it and
return zero bytes as response.

Change-Id: Ic44557368a6034dbf4bb021ab23a57927c22def0
2023-12-17 10:46:31 +00:00
Harald Welte
6e9ae8a584 usim: Properly decode/encode IPv4 + IPv6 addresses
use normal textual representation for IPv4 and IPv6 addresses

Change-Id: I2c6c377f4502af37639e555826c85d5dcf602f9b
2023-12-17 10:46:31 +00:00
Harald Welte
478b5fe8e3 usim: ePDGId + ePDGSelection: Fix encoder/decoder + add test cases
Change-Id: Idca19b6fdabae6cc708e92c7714fa0903ea5a1ee
2023-12-17 10:46:31 +00:00
Harald Welte
cdfe1c24af usim: Add EF.ePDGSelection + EF.ePDGSelectionEm support
Change-Id: I760a394ae1eac5f1175dc9b86c11b4a60671582e
2023-12-17 10:46:31 +00:00
Harald Welte
5277b5cf2c USIM: add support for EG.ePDGIdEm (Emergency ePDG)
Change-Id: I71cb7a4b9323f57b96db2d9f12f1567eda63f742
2023-12-17 10:46:31 +00:00
Philipp Maier
a5707c7dfb filesystem: fix typo
Change-Id: I721875d302ab69340d56b33102297b56c070465f
2023-12-13 12:47:36 +01:00
Philipp Maier
82cc7cc11a runtime: refactor file selection methods select and select_file
The implementation of the methods select and select_file of class
RuntimeLchan is a bit complex. We access the card directly in several
places which makes it difficult to track the state changes. We should
clean this up so that we call self.rs.card.select_adf_by_aid/
self.scc.select_file from a single place only.

This means that the method select uses the method select_file. This
results in a much cleaner implementation. We also should take care
that the important states that we track (selected_file, selected_adf,
etc.) are updated by a single private method. Since the update always
must happen after a select _select_post is a good place to do this.

Related: OS#5418
Change-Id: I9ae213f3b078983f3e6d4c11db38fdbe504c84f2
2023-12-13 12:47:36 +01:00
Philipp Maier
14bf003dad filesystem: use sort path when selecting an application
The method build_select_path_to uses the internal file system tree model
to find the path to a given file. This works the same for applications
(ADF) as it works for normal files (EF/DF). However, an application can
be selected anytime from any location in the filesystem tree. There is
no need to select a specific path leading to that application first.
This means that if there is an ADF somewhere in the resulting
inter_path, we may clip everything before that ADF.

Related: OS#5418
Change-Id: I838a99bb47afc73b4274baecb04fff31abf7b2e2
2023-12-13 12:45:46 +01:00
Philipp Maier
174fd32f17 runtime: explain how file probing works
We use a trick to probe a file (that does not exist in the local file
model yet). Let's explain further how that works, in particular why we
do not have to upate any state if probing fails.

Change-Id: I2a8af73654251d105af8de1c17da53dfa10dc669
Related: OS#5418
2023-12-13 09:02:30 +00:00
Harald Welte
b582c3c7ea euicc: Fix TLV IE definitions for SetNickname{Req,Resp}
The metaclass uese the 'nested' attribute, while the existing code
accidentially used the 'children' attribute.  The latter is used
by instances for actual child classes, while the Class/nested
attribute is for the list of classes whose instancse could be potential
children.

Change-Id: I968bd84d074dcdcec37d99be5d3d4edac9c35a0c
2023-12-07 23:29:11 +01:00
Harald Welte
c20d442695 euicc: Fix encoding of Lc value in STORE DATA
The length value "of course" is a hex value, don't use %02u but %02x

This fixes any eUICC command with a Lc > 10 bytes.

Change-Id: I1e1efbfb9916fc43699602cc889cf4b3d42736f2
2023-12-07 22:46:40 +01:00
Harald Welte
2b6deddcdc euicc: the ICCID TLV object uses bcd-swapped-nibble encoding
Change-Id: I050f9e0fb128f3e1d472e2330b136a753794a5a1
2023-12-07 14:21:43 +01:00
Philipp Maier
5482737f31 pySim-shell: don't get trapped in applications without file system
When we traverse the file system, we may also end up selecting
applications (ADF), which do not support an USIM/ISIM like file system.
This will leave us without the ability to select the MF (or any other
file) again. The only way out is to select the ISIM or USIM application
again to get the access to the file system again.

Change-Id: Ia2fdd65f430c07acb1afdaf265d24c6928b654e0
Related: OS#5418
2023-12-07 13:21:07 +00:00
Harald Welte
008cdf4664 euicc: Fix encoding of {enable,disable,delete}_profile
The encoding was missing a "CHOICE" container and missed the
fact that the refreshFlag presence is mandatory for enable+disable.

Change-Id: I12e2b16b2c1b4b01dfad0d1fb485399827f25ddc
2023-12-07 13:19:52 +00:00
Harald Welte
0f7d48ed69 tlv: Fix encoding of zero-valued TLVs
If a TLV was elementary (no nested IEs), and it had only a single
integer content whose value is 0, we erroneously encoded that as
zero-length TLV (len=0, no value part):

>>> rf = pySim.euicc.RefreshFlag(decoded=0);
>>> rf.to_bytes()
b''
>>> rf.to_tlv()
b'\x81\x00'

After this change it is correct:

>>> rf = pySim.euicc.RefreshFlag(decoded=0);
>>> rf.to_bytes()
b'\x00'
>>> rf.to_tlv()
b'\x81\x01\x00'

Change-Id: I5f4c0555cff7df9ccfc4a56da12766d1bf89122f
2023-12-07 13:19:52 +00:00
Philipp Maier
c038cccdd8 runtime: cosmetic: prnounce file reference data
One of the most important properties of the RuntimeLchan are the
selected_file/adf properties. Let's reformat the code so that those
properties are more pronounced.

Change-Id: I4aa028f66879b7d6c2a1cd102cda8d8ca5ff48b1
Related: OS#5418
2023-12-07 12:29:17 +01:00
Philipp Maier
e30456b07a runtime: explain why we may access the card object directly
When we are in the constructor of RuntimeState, we may/must access the
card object directly. Let's explain why, since it may not be immediately
obvious.

Change-Id: I01f74d5f021d46679d1c9fa83fb8753382b0f88f
Related: OS#5418
2023-12-07 12:28:57 +01:00
Philipp Maier
b8b61bf8af runtime: do not use the _scc object of the card object to select MF
The constructor of the RuntimeState object selects the MF befor it does
some other steps. However it does this through the _scc object of the
card object. This method is before we had lchan abstraction, so we
should now use the lchan object like in all other places.

Related: OS#5418
Change-Id: I9a751c0228c77077e3fabb50a9a68e4489e7151c
2023-12-07 12:28:39 +01:00
Harald Welte
880db37356 flatten_dict_lists(): Don't flatten lists with duplicate keys
If we have a list of dicts, and we flatten that into a dict: Only
do that if there are no dicts with duplocate key values in the list,
as otherwise we will loose information during the transformation.

Change-Id: I7f6d03bf323a153f3172853a3ef171cbec8aece7
Closes: OS#6288
2023-12-06 09:02:38 +01:00
Harald Welte
9c38711773 ara_m: Fix encoding of DeviceInterfaceVersionDO
Ever since commit 30de9fd8ab in July
we are (properly) using snake_case names in the from_dict (to become
bijective with to_dict).   This code was not updated by accident,
creating an exception when using the `aram_get_config`

Change-Id: If216b56b38ab17d13896074aa726278b9ba16923
Related: OS#6119
2023-12-06 01:07:35 +00:00
Philipp Maier
a1850aeccc filesystem: add flag to tell whether an ADF supports an FS or not
An ADF may or may not support a file system. For example ADF.ARA-M does
not have any filesystem support, which means the SELECT we may use from
this ADF is limited and an can only select a different application. To
know about this in advance let's add a flag that we set when we
instantiate an ADF.

Change-Id: Ifd0f7c34164685ea18d8a746394e55416fa0aa66
Related: OS#5418
2023-12-05 17:37:36 +00:00
Harald Welte
4e02436dba perform multiple GET RESPONSE cycles if more data is available
So far we implemented only one round of "Send the APDU, get SW=61xx,
call GET RESPONSE".  This permitted us to receive only data up to 256
bytes.

Let's extend that to doing multiple rounds, concatenating the result.
This allows us to obtain arbitrary-length data from the card.

See Annex C.1 of ETSI TS 102 221 for examples showing multiple 61xx
iterations.

Change-Id: Ib17da655aa0b0eb203c29dc92690c81bd1300778
Closes: OS#6287
2023-12-04 21:38:50 +01:00
Philipp Maier
1c207a2499 pySim-shell: Do not use self.lchan.scc when sending raw APDUs.
When sending raw APDUs, we access the scc (SimCardCommands) object via
the scc member in the lchan object. Unfortunately self.lchan will not be
populated when the rs (RuntimeState) object is missing. This is in
particular the case when no profile could be detected for the card,
which is a common situation when we boostrap an unprovisioned card.

So let's access the scc object through the card object. This is also
more logical since when we send raw APDUs we work below the level of
logical channels.

Change-Id: I6bbaebe7d7a2013f0ce558ca2da7d58f5e6d991a
Related: OS#6278
2023-11-29 15:24:10 +01:00
Philipp Maier
eb3b0dd379 pySim-shell: refuse to execute a startup script on initialization errors
When there is an error on initialization (e.g. card not present), we
should not continue to execute a startup script that was passed with the
pySim-shell commandline. Instead we should print a message that the
startup script was ignored due to errors.

Related: OS#6271
Change-Id: I61329988e0e9021b5b0ef8e0819fb8e23cabf38b
2023-11-24 12:41:18 +01:00
Philipp Maier
f1e1e729c4 app: do not catch exceptions in init_card
The function init_card catches all exceptions and then returns None
objects for card or rs in case of an error. This does not fit in the
style we pursue in pySim. This is in particular true for library
functions. We want those functions to raise exceptions when something is
wrong, so that we can catch the exception at top level. Let's fix this
for init_card now.

Related: OS#6271
Change-Id: I581125d8273ef024f6dbf3a5db6116be15c5c95d
2023-11-24 12:41:18 +01:00
iw0
40ef226030 ts_31_102: correct name of EF_ePDGId
In 31.102 v17.10, file 6ff3 is called "EF_ePDGId". Adjust the spelling to match.

Change-Id: I2c27a7f325f75274e2110eb312b623cf9e7dab47
2023-11-14 13:18:36 +00:00
Philipp Maier
578cf12e73 runtime: fix tracking of selected_adf
The class property selected_adf is not updated in all locations where an
ADF is selected, this means that we may loose track of the currently
selected ADF in some locations

Change-Id: I4cc0c58ff887422b4f3954d35c8380ddc00baa1d
Related: OS#5418
2023-11-09 14:43:08 +00:00
Harald Welte
8fab463e67 pySim-shell: Move init_card() function to new pySim.app module
The point of this is to move generic code out of pySim-shell.py,
paving the way for more/other executables using the full power of
our class model without having to reinvent the wheel.

Change-Id: Icf557ed3064ef613ed693ce28bd3514a97a938bd
2023-11-09 12:36:47 +00:00
Harald Welte
2d44f03af2 transport: Log it explicitly if user doesn't specify a reader
Change-Id: I37e9d62fabf237ece7e49d8f2253c606999d3d02
2023-11-04 15:48:55 +00:00
Harald Welte
45477a767b Use construct 'Flag' instead of 'Bit' for type descriptions
It's better for the human reader (and more obvious that it's a boolean
value) if we decode single Bits as True/False instead of 1/0.

Change-Id: Ib025f9c4551af7cf57090a0678ab0f66a6684fa4
2023-11-04 15:48:44 +00:00
Harald Welte
7be68b2980 sysmocom_sja2: Add some de/encode test vectors
This increases test coverage and also shows where we so far only
have decoders but no encoders yet

Change-Id: I7932bab7c81a2314c1b9477f50b82a46f24d074e
2023-11-03 00:43:17 +01:00
Harald Welte
1c849f8bc2 pySim-shell: Reject any non-decimal PIN values
Don't even send any non-decimal PIN values to the card, but reject
them when parsing the command arguments.

Change-Id: Icec1698851471af7f76f20201dcdcfcd48ddf365
2023-11-03 00:43:17 +01:00
Harald Welte
977c5925a1 pySim-shell: permit string with spaces for 'echo' command
before this patch:

pySIM-shell (00:MF)> echo foo bar baz
usage: echo [-h] string
echo: error: unrecognized arguments: bar baz

after this patch:

pySIM-shell (00:MF)> echo foo bar baz
foo bar baz

Change-Id: I1369bc3aa975865e3a8a574c132e469813a9f6b9
2023-11-03 00:43:17 +01:00
Harald Welte
4e59d89a5d pySim-shell: Validate that argument to 'apdu' command is proper hexstr
Let's not even send anything to the card if it's not an even number
of hexadecimal digits

Change-Id: I58465244101cc1a976e5a17af2aceea1cf9f9b54
2023-11-03 00:43:17 +01:00
Harald Welte
f9ea63ea51 pySim-shell: Improved argument validation for verify_adm argument
Let's make sure we don't even bother to ask the card to verify
anything as ADM1 pin which is not either a sequence of decimal digits
or an even number of hex digits (even number of bytes).

Change-Id: I4a193a3cf63462fad73d145ab1481070ddf767ca
2023-11-03 00:43:17 +01:00
Harald Welte
469db9393f pySim-shell: Use argparser for verify_adm to support --help
Let's add a proper argparser instance for the 'verify_adm' command,
avoiding situations where the user types 'verif_adm --help' and then
--help is interpreted as the PIN value, removing one more attempt from
the failed ADM1 counter.

Let's use that opportunity to improve the documentation of the command.

Change-Id: I3321fae66a11efd00c53b66c7890fce84796e658
2023-11-02 21:46:38 +00:00
Harald Welte
0ba3fd996a pySim-shell: Add copyright statement and link to online manual to banner
This way the users are reminded where they can go to read the manual.

Change-Id: Ie86822e73bccb3c585cecc818d4462d4ca6e43c2
2023-11-02 21:46:13 +00:00
Harald Welte
3d16fdd8da docs: shell: Various documentation updates/extensions
* examples for export, verify_adm, reset, apdu
* explain CSV option for verify_adm
* fix 'tree' example (--help shouldn't be there)

Change-Id: I6ed8d8c5cf268ad3534e988eff9501f388b8d80f
2023-11-02 21:46:08 +00:00
Harald Welte
aa07ebcdac docs: shell: update output in examples
pySim-shell output has changed over time, so some examples were
showing outdated content.  Let's update those.

Change-Id: I4058719c32b61689522e90eba37253e8accb8ba5
2023-11-02 21:46:01 +00:00
Harald Welte
6663218ab8 docs: Fix docstring syntax to avoid warnings
pySim/tlv.py:docstring of pySim.tlv.IE.from_bytes:1: ERROR: Unknown target name: "part".
pySim/tlv.py:docstring of pySim.tlv.IE.to_bytes:1: ERROR: Unknown target name: "part".

Change-Id: I170176910c4519005b9276dbe5854aaaecb58efb
2023-11-02 21:45:54 +00:00
Harald Welte
0c25e922be docs: shell: Re-order the command sections/classes
the generic pysim command should precede those from specs like ISO7816

Change-Id: I11e66757f10cc28fda547244ae09d51dacd70824
2023-11-02 21:45:48 +00:00
Harald Welte
350cfd822b docs: shell: link to cmd2 documentation
Change-Id: I532cb33781f95fe847db7fae7a5264b5d9c416de
2023-11-02 21:44:46 +00:00
Harald Welte
0f2faa59fb docs: shell: By now we have encoders/decoders for most files
Change-Id: Ia771f9969ae7eb0094d1768af3f7f54cc9d0d581
2023-11-01 17:26:35 +01:00
Harald Welte
47bb33f937 docs: shell: Clarify various different card support
Change-Id: Ibf8e3538aa3c954df72c11ec0a2f885031b54b0e
2023-11-01 17:26:35 +01:00
Philipp Maier
a24755e066 filesystem: fix method build_select_path_to
The method build_select_path_to chops off the first element of the
current path. This is done to prevent re-selection of the first file in
the current path.

Unfortunately chopping off the first element in the current path does
not work properly in a situation when the current path points to the MF.
This would chop off the first and last element in the list and the for
loop below would run 0 times.

To fix this, let's keep the first element and chop it off from the
resulting path.

Related: OS#5418
Change-Id: Ia521a7ac4c25fd3a2bc8edffdc45ec89ba4b16eb
2023-10-31 17:25:55 +01:00
Philipp Maier
1da8636c0f runtime: cosmetic: fix formatting of comment
Change-Id: I4e949a08c1bfab413b82e958a64404390e58148f
2023-10-31 17:25:51 +01:00
Philipp Maier
4af63dc760 transport: print reader device/number on init
When we initialize the reader, we currently tell only which type of
interface we are using, but we do not print the reader number or the
device path.

Let's extend the messages so that the path is printed. To prevent
problems with integration-tests, let's also add an environment variable
that we can use to detect when pySim runs inside a integration-test.

Related: OS#6210
Change-Id: Ibe296d51885b1ef5f9c9ecaf1d28da52014dcc4b
2023-10-26 15:17:07 +00:00
Harald Welte
cbc0bdfaa9 euicc: add some first IoT eUICC commands (GSMA SGP.32)
this is far from being complete, just some basic first commands
to get the certificates and eIM configuration.

Change-Id: Ie05108e635ed9c6de10f0ba431cb1b13893f6be8
2023-10-26 15:16:30 +00:00
Harald Welte
884eb551af euicc: Add get_profiles_info command
Example output:

pySIM-shell (02:MF/ADF.ISD-R)> get_profiles_info
{
    "profile_info_seq": {
        "profile_info": {
            "iccid": "98940462222222222222",
            "isdp_aid": "a0000005591010ffffffff8900001200",
            "profile_state": "enabled",
            "service_provider_name": "foobar",
            "profile_name": "foobar",
            "profile_class": "provisioning"
        }
    }
}

Change-Id: I52d136f99dc0eb29905e7ca0cd0865486d3cf65b
2023-10-26 15:16:30 +00:00
Harald Welte
268a2025db Initial support for eUICC
This just adds basic support for the ISD-R application and its
associated STORE DATA command which is used for the ES10x interfaces
between off-card entities and the on-card ISD-R.

Change-Id: Ieab37b083e25d3f36c20f6e9ed3e4bdfdd14a42a
Closes: OS#5637
2023-10-26 15:16:30 +00:00
Philipp Maier
8c82378bfd transport: move argument parser setup into concrete classes
The argument parser is set up globally for all LinkBase objects in
__init__.py. Since we tend to have only platform independed code in
__init__.py, we should move the argument parser setup into the
specific LinkBase classes.

Related: OS#6210
Change-Id: I22c32aa81ca0588e3314c3ff4546f6e5092c11df
2023-10-24 19:28:34 +00:00
Philipp Maier
3077343739 transport: move init message into concrete classes
In in the module __init__.py we print an init message (which type of
LinkBase class is providing the SimLink). However in __init__.py we tend
to have only platform independed code but the message string can already
be categorized as platform depened. Let's put the init message into the
constructor of the concrete classes of LinkBase.

Related: OS#6210
Change-Id: I0a6dd7deb79a5f3e42b29094a1cf2535075fa430
2023-10-24 19:28:34 +00:00
Harald Welte
10669f2ddf utils: Fix bertlv_encode_tag() for multi-byte tags
We used to support only single-byte tags in bertlv_encode_tag,
let's fix that.  The easy option is to simply call bertlv_parse_tag,
as that already supported multi-byte tags.

Change-Id: If0bd9137883c4c8b01c4dfcbb53cabeee5c1ce2b
2023-10-24 15:10:01 +02:00
Harald Welte
237ddb5bb3 pySim-shell: Include current logical channel in prompt
Now that pySim-shell can switch between logical channels, let's state
the currently used logical channel in the prompt.

Change-Id: I45781a6fba205eeb4ac7f58d5cb642b7131bdd88
Related: OS#6230
2023-10-24 15:10:01 +02:00
Harald Welte
20650997e8 pySim-shell: Add 'switch_channel' command
We've already had the 'open_channel' and 'close_channel' commands,
which were sent to (and acknowledged by) the card.  However,
those commands didn't affect the pySim-shell state, i.e. all
communication would still happen through the default channel '0'.

With this patch we introduce a 'switch_channel' command, using which
the user can determine which of the (previously opened) logical channels
shall be used by pySim-shell.

Change-Id: Ia76eb45c4925882ae6866e50b64d9610bd4d546d
Closes: OS#6230
2023-10-24 15:10:01 +02:00
Harald Welte
6dd6f3e12c prevent SimCardCommands.select_adf_by_aid bypassing lchan
Now that pySim-shell is aware of logical channels and issues almost
all of its APDUs on the currently selected channel, we must also make
sure that ADF selection by AID (implemented by the CardBase class)
issues the SELECT on the respective logical channel.

Before this patch, SELECT ADF by AID would always be issued on the
primary logical channel (0), irrespective of the currently active
RuntimeLchan.

Change-Id: Idf05c297e6a2e24ca539408b8912e348c0782bb4
Related: OS#6230
2023-10-24 15:10:01 +02:00
Harald Welte
46255121e0 pySim-shell: Create + use per-RuntimeLchan SimCardCommands
This new approach will "fork" separate SimCardCommands instances
for each RuntimeLchan.  Higher-layer code should now always use the
RuntimeLchan.scc rather than the RuntimeState.card._scc in order to
make sure commands use the correct logical channel.

Change-Id: I13e2e871f2afc2460d9fd1cd566de42267c7d389
Related: OS#6230
2023-10-24 15:10:01 +02:00
Harald Welte
3dfab9dede commands.py: Add support for multiple logical channels.
Historically we always only had one instance of SimCardCommands, but
with this patch we can now have multiple instances, one for each lchan.

The SimCardCommands class is aware of the logical channel it runs on
and will patch the CLA byte accordingly.

Change-Id: Ibe5650dedc0f7681acf82018a86f83377ba81d30
Related: OS#6230
2023-10-24 15:10:01 +02:00
Harald Welte
91eeecfbf3 docs: Fix command reference for 'apdu' command
This fixes the below error during build of the documentation:

pysim/docs/shell.rst:349: ERROR: "<class 'pySim-shell.PySimCommands'>" has no attribute "apdu_cmd_parser"

Change-Id: If89b66a45ea18b5a3fc56bf77b05e679463da5a8
2023-10-23 22:30:31 +02:00
Harald Welte
49acc06327 RuntimeState: Add type annotation for 'card' argument
Change-Id: I3c5138a918f7e45aabe3972883714d05ee704877
2023-10-21 21:47:04 +02:00
Harald Welte
bdf595756e pySim-shell: Create/delete RuntimeLchan objects on open/close of channel
We already have the open channel and close_channel commands in
pySim-shell. They are sent to the card and acknowledged, respectively.

We also already do have code that can track multiple different logical
channels (the rs.lchan array).  However, this is currently only used by
pySim-trace, and not by pySim-shell.  Let's change that.

Change-Id: Idacee2dc57e8afe85c79bc85b259064e7f5b83a2
Related: OS#6230
2023-10-21 21:47:04 +02:00
Harald Welte
7997252267 cards.py: Fix type annotation
The CardBaes 'scc' member refers to a SimCardCommands instance,
not to a LinkBase.

Change-Id: If4c0dfbd8c9a03d1a0bc4129bb3c5d5fa492d4cb
2023-10-21 21:47:04 +02:00
Philipp Maier
7c0cd0a93b pySim-shell: do not fail when EF.ICCID does not exist
An eUICC that has no active eSIM profile does not have an ICCID. (The
reason for this is that EF.ICCID is part of the eSIM profile).
Unfortunately pySim-shell insists on reading the ICCID from EF.ICCID on
startup in order to use it as a lookup key for verify_adm later.

To solve the problem, let's add a try/except block around the section
where EF.ICCID is read. In case of failure we set the ICCID to None,

Related: OS#5636
Change-Id: I8d18c5073946c5a6bb1f93be0ce692a599f46f8c
2023-10-20 20:51:24 +00:00
Harald Welte
509ecf84fa Use keyword argument for file description argument
While our base classes (TransparentEF / LinFixedEF) always have the
dsecription as 4th argument after "fid, sfid, name", most of the derived
file-specific classes do not share that same argument order.

As seen in the bug fixed by previous Change-Id I7f32c9fd01094620b68b0e54536ecc6cdbe67903
this can have serious consequences.  Let's avoid using unnamed
(positional) arguments for the description text altogether.

Change-Id: Icfb3fd1bae038c54fa14a91aa9f75219d839968c
2023-10-18 23:32:57 +02:00
Harald Welte
28accc88c3 ts_31_102: Fix initialization of file size
We were using positional arguments when instantiating instances
of classes like EF_5GS3GPPLOCI with non-default names/fids/...

However, we got the argument order wrong and were passing the
description string in the position of the file size, which causes
exceptions like the following from pySim-trace:

Traceback (most recent call last):
  File "/home/laforge/projects/git/pysim/./pySim-trace.py", line 198, in <module>
    tracer.main()
  File "/home/laforge/projects/git/pysim/./pySim-trace.py", line 125, in main
    inst.process(self.rs)
  File "/home/laforge/projects/git/pysim/pySim/apdu/__init__.py", line 259, in process
    self.processed = method(self.lchan)
  File "/home/laforge/projects/git/pysim/pySim/apdu/ts_102_221.py", line 152, in process_on_lchan
    if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]:
TypeError: '<' not supported between instances of 'int' and 'str'

Let's use named initializers for any arguments after the usual "fid, sfid, name"
initial arguments.

Change-Id: I7f32c9fd01094620b68b0e54536ecc6cdbe67903
2023-10-18 23:21:46 +02:00
Philipp Maier
af4e5bb18c transport: do not catch exceptions in init_reader
We currently catch any exceptions that may occur when the card reader is
initialized. Then we print the exception string or the exception type
when no string is available. However, a failure during the reader
initialization is usually a severe problem, so a traceback would provde
a lot of helpful information to debug the issue. So lets not catch any
exceptions at this level so that we get the full backtrace.

Related: OS#6210
Change-Id: I4c4807576fe63cf71a7d33b243a3f8fea0b7ff23
2023-10-16 14:36:02 +02:00
Philipp Maier
58e89eb15a transport: add return type annotation to method __str__
The abstract class LinkBase has no return type annotation on its
__str__ method.

Related: OS#6210
Change-Id: I26d3d2714708dbe957704b60d17ba2afa325b2c4
2023-10-10 12:06:57 +02:00
Philipp Maier
6bfa8a8533 pySim-shell: print device info in case an exception occurs
When an exception occurs while initializing or handling the card we
print a traceback, but we do not print any info that allows us to
identify the device that was involved when the exception occurred. Let's
include the device path or number in the error message before we print
the traceback.

In order to make it easier to print the device information, let's add a
__str__() method to all of our devices. This method shall return the
device number or path.

Related: OS#6210
Change-Id: I200463e692245da40ea6d5b609bfc0ca02d15bdb
2023-10-10 11:51:08 +02:00
Philipp Maier
8e03f2f2ed pySim-shell: do not pass failed card object to PysimApp
When the try block in which we also call init_card() fails, there may be
no card object, so we must not pass the card object to PysimApp in the
except block. This is also no problem, PysimApp will run without the
card object until the user executes do_equip for a second attempt.

Related: OS#6210
Change-Id: I28195f442ce007f05f7610c882bbc4a6520a8ce6
2023-10-10 11:26:56 +02:00
Philipp Maier
91c971bf82 pySim-prog, pySim-shell do not use global variables
When __main__ runs different variables get assigned. In particular opts,
scc, sl and ch. Those variables are available in any scope and
technically it is possible to access them. However, lets not do this
since it leads to confusion. Also, pylint will complain about those code
locations.

In pySim-shell.py
- Let's use the proper locations (sl and ch are stored in PysimApp.
- Scc can be assigned in init_card.
- In method walk, the use of the variable opts to call ection_df is wrong,
  lets use **kwargs (see also usage of action_ef).
- The constructor of Cmd2ApduTracer has a parameter cmd2_app, but usese
  the global variable app. Let's use cmd2_app instead.

In pySim-prog.py
- Do not use opts.num in find_row_in_csv_file, use num instead.
- Pass scc to process_card as parameter so that it won't access scc
  in the global scope.

Change-Id: I7f09e9a6a6bfc658de75e86f7383ce73726f6666
Related: OS#6210
2023-10-09 12:37:47 +02:00
Philipp Maier
37e57e0c45 filesystem: add attribute "leftpad" to class LinFixedEF
In some cases, the specs do not specify an absolute record length.
Instead there may be only a minimum record length specified. The card
vendor may then chose to use larger record length at will. This usually
is no problem since the data is usually written from the left and the
remaining bytes are padded at the end (right side) of the data. However
in some rare cases (EF.MSISDN, see also 3GPP TS 51.011, section 10.5.5)
the data must be written right-aligned towards the physical record
length. This means that the data is padded from the left in this case.

To fix this: Let's add a "leftpad" flag to LinFixedEF, which we set to
true in those corner cases. The code that updates the record in
commands.py must then check this flag and padd the data accordingly.

Change-Id: I241d9fd656f9064a3ebb4e8e01a52b6b030f9923
Related: OS#5714
2023-09-07 14:19:26 +02:00
Philipp Maier
0ac4d3c7dc commands: make method verify_binary and verify_record private
The methods verify_binary and verify_record are only used internally
in class SimCardCommands, they can be both private methods. Also lets
move them above the method that uses them.

Related: OS#5714
Change-Id: I57c9af3d6ff45caa4378c400643b4ae1fa42ecac
2023-09-07 13:23:08 +02:00
Philipp Maier
4840d4dc8f pySim-shell: fix commandline option -a (verify_adm)
The commandline option -a, which does an ADM verification on startup,
does no longer work since the verify_adm method is no longer available
in the card base classes (cards.py). Let's use the verify_chv method
from SimCardCommands instead.

Related: RT#68294
Change-Id: Ic1e54d0e9e722d64b3fbeb044134044d47946f7c
2023-09-06 14:57:55 +02:00
Philipp Maier
3a37ad015c sim-reset-server: fix error printing sw_match_error
In the last line of the if,elif,else branch, when we print the ApiError
object, we pass the variable sw to str() instead passing it to
ApiError() like we do it in the lines above. This is not correct and
causes strange exceptions.

Related: OS#67094
Change-Id: I5a1d19abeb00c2c9dc26517abc44a5c916f2d658
2023-09-06 12:59:24 +02:00
Philipp Maier
7d13845285 sim-rest-server: fix REST method info
The REST megthd info uses deprecated methods to read the ICCID and the
IMSI from the card. However, we can replace those methods by selecting
the files we are interested in manually and then reading them.

Related: RT#67094
Change-Id: Ib0178823abb18187404249cfed71cfb3123d1d74
2023-08-25 09:52:48 +02:00
Philipp Maier
91b379a039 sim-rest-server: use UiccCardBase instead of UsimCard
The class UsimCard is deprecated and only still used in very old
legacy applications. let's use the more modern UiccCardBase class
instead.

Related: RT#67094
Change-Id: I3676f033833665751c0d953176eafe175b20c14a
2023-08-21 18:36:10 +00:00
Philipp Maier
71a3fb8b3a sim-rest-server: do not select ADF.USIM in connect_to_card
When the function connect_to_card is done, it selects ADF.USIM. This
might be contraproductive in case someone needs to access files on MF
level in one of the REST methods. Instead fo ADF.USIM, let's use MF as a
common ground to start from.

At the moment the only existing REST (info, auth) immediately select
ADF.USIM after calling connect_to_card already, so there are no further
modifications necessary.

Related: RT#67094
Change-Id: I16e7f3c991c83f81989ecc4e4764bb6cc799c01d
2023-08-21 18:36:10 +00:00
Philipp Maier
a42ee6f99d cards: get rid of method read_iccid
The method read_iccid in class CardBase should be put back to
legacy/cards.py. The reason for this is that it falls in the same
category like read_imsi, read_ki, etc. We should not use those old
methods in future programs since we have a more modern infrastructure
(lchan) now.

Also pySim-shell.py is the only caller of this method now. It is not
used in any other place.

Related: RT#67094
Change-Id: Ied3ae6fd107992abcc1b5ea3edb0eb4bdcd2f892
2023-08-21 18:36:10 +00:00
Florian Klink
09ff0e2b43 README.md: sort dependencies, document smpp.pdu
This dependency is currently only mentioned in requirements.txt, it
makes sense to also document it here.

Change-Id: I89760dd4008829c91fafbd442483d076c92a7ed4
2023-08-13 12:16:16 +02:00
Florian Klink
83222abf2e setup.py: fix package name
The package providing the serial python module seems to be called
pyserial, which also matches what's written in requirements.txt.

Change-Id: I71ef6a19a487101e552219f10f2fa6215b966abd
2023-08-13 12:10:16 +02:00
Philipp Maier
e6cba76a36 pySim-shell: check presence of runtime state before accessing it
When the command equip (do_equip) is executed, it accesses
self.rs.profile to see if there are any commands that need to be
unregistered before moving on with the card initialization.

However, it may be the case that no runtime state exists at this point.
This is in particular the case when the card is completely empty and
hence no profile is picked and no runtime state exists.

Change-Id: I0a8be66a69b630f1f2898b62dc752a8eb5275301
2023-08-11 11:28:31 +02:00
Philipp Maier
63e8a18883 pySim-prog_test: fix typo
Related: OS#6094
Change-Id: I6432ee3ee948fea697067fb3857cb9b83b1f8422
2023-08-01 16:10:14 +02:00
Philipp Maier
a380e4efbe pySim-trace_test: verify output of pySim-trace.py
At the moment we only verify that no exceptions occurred but the output
is not yet verfied.

Related: OS#6094
Change-Id: I3aaa779b5bd8f30936c284a80dbdcb2b0e06985c
2023-08-01 16:10:14 +02:00
Philipp Maier
7124ad1031 pySim-trace_test: fix shebang line
Related: OS#6094
Change-Id: Ib2d3a4659f5db9772ddcd9a4ae73c04fec1070fc
2023-08-01 16:01:47 +02:00
Philipp Maier
d62182ca43 runtime: make sure applications are always listed in the same order
When we print the profile applications. which are not registered in
EF.DIR, we use python sets to subtract the applications which were part
of EF.DIR and hence already listed. Since we use sets the order may be
arbitrary. This is so far not a problem, since the output is meant to be
read by humans, but as soon as we try to use the output for unit-test
verifications we need a consistent order (sorted)

Related: OS#6094
Change-Id: Ie75613910aaba14c27420c52b6596ab080588273
2023-08-01 15:47:27 +02:00
Philipp Maier
600e284a7b README.md: Add note about pySim-trace.py dependencies
Related: OS#6094
Change-Id: I2e03f9c827bd6ee73891bba34bd2f2efe3ded7e6
2023-07-29 08:56:07 +00:00
Philipp Maier
1cdcbe4f57 pysim-test: rename pysim-test.sh to pySim-prog_test.sh
We now have pySim-shell and pySim-trace. Let's give pysim-test.sh a more
distinctive name so that it is clear to which program it refers.

Related: OS#6094
Change-Id: I438f63f9580ebd3c7cc78cc5dab13c9937ac6e3a
2023-07-29 08:56:07 +00:00
Philipp Maier
ec9cdb73e7 tests: add test script for pySim-trace
pySim-trace has no test coverage yet. Let's use a script to run a
GSAMTAP pcacp through it and check that no exceptions are raised.

Related: OS#6094
Change-Id: Icfabfa7c59968021eef0399991bd05b92467d8d2
2023-07-29 08:56:07 +00:00
Alexander Couzens
c8facea845 Fix the remaining functions using the broken Card.update_ust() call
Card.update_ust() got replaced by the file operation ust_update().
In addition to Change-Id I7a6a77b872a6f5d8c478ca75dcff8ea067b8203e

Fixes: f8d2e2ba08 ("split pySim/legacy/{cards,utils} from pySim/{cards,utils}")
Change-Id: Ie6405cae37493a2101e5089a8d11766fbfed4518
2023-07-29 06:23:15 +00:00
Alexander Couzens
2dd59edd74 ARA-M: fix encoding of the PkgRefDO when using aram_store_ref_ar_do
The command wasn't used the correct dict to allow the encoders to work.

Related: OS#6121
Change-Id: Ic2bc179b413a6b139e07e3e55b93ff921cb020a9
2023-07-29 06:23:15 +00:00
Alexander Couzens
760e421be5 utils.py: remove superfluous import from itself
b2h() is already available.

Change-Id: Ied513a08cc8b5091dd467106250f1e6b5067c3a8
2023-07-29 06:21:54 +00:00
Alexander Couzens
6c5c3f8b2b Reimplement ust_service_activate and ust_service_deactivate for USIM/EF.UST
Fixes: f8d2e2ba08 ("split pySim/legacy/{cards,utils} from pySim/{cards,utils}")
Change-Id: I7a6a77b872a6f5d8c478ca75dcff8ea067b8203e
2023-07-28 15:34:40 +00:00
Philipp Maier
8dc2ca2d37 pySim-trace: catch StopIteration exception on trace file end
When the trace file end is reaced, pyShark raises a StopIteration
exception. Let's catch this exception and exit gracefully.

Related: OS#6094
Change-Id: I6ab5689b909333531d08bf46e5dfea59b161a79e
2023-07-28 10:36:52 +02:00
Philipp Maier
162ba3af3e pySim-trace: mark card reset in the trace
The trace log currently does not contain any information about card
resets. This makes the trace difficult to follow. Let's use the
CardReset object to display the ATR in the trace.

Related: OS#6094
Change-Id: Ia550a8bd2f45d2ad622cb2ac2a2905397db76bce
2023-07-28 10:14:19 +02:00
Philipp Maier
1f46f07e3c utils: tolerate uninitialized fields in dec_addr_tlv
TLV fields holding an address may still be uninitialized and hence
filled with 0xff bytes. Lets interpret those fields in the same way as
we interpret empty fields.

Related: OS#6094
Change-Id: Idc0a92ea88756266381c8da2ad62de061a8ea7a1
2023-07-28 10:14:19 +02:00
Philipp Maier
784b947b11 pySim-trace: remove stray debug print
Related: OS#6094
Change-Id: I5f030a8552a84f721bd12ab4751933fc6eeae256
2023-07-28 10:14:19 +02:00
Philipp Maier
407c95520f pySim-trace: add commandline option --show-raw-apdu
The trace log currently only shows the parsed APDU. However, depending
on the problem to investigate it may be required to see the raw APDU
string as well. Let's add an option for this.

Related: OS#6094
Change-Id: I1a3bc54c459e45ed3154479759ceecdc26db9d37
2023-07-28 10:07:35 +02:00
Philipp Maier
791f80a44f construct: add adapter Utf8Adapter to safely interpret utf8 text
Uninitialized Files, File records or fields in a File record or File
usually contain a string of 0xff bytes. This becomes a problem when the
content is normally encoded/decoded as utf8 since by the construct
parser. The parser will throw an expection when it tries to decode the
0xff string as utf8. This is especially a serious problem in pySim-trace
where an execption stops the parser.

Let's fix this by interpreting a string of 0xff as an empty string.

Related: OS#6094
Change-Id: Id114096ccb8b7ff8fcc91e1ef3002526afa09cb7
2023-07-26 17:13:54 +02:00
farhadh
fec721fcb1 Fixed mnc encoding
According to 3GPP TS 24.008 section 10.5.5.36 PLMN identity of the CN operator

Change-Id: I400435abfa8b67da886fc39c801e1abba39725bf
2023-07-21 11:09:49 +00:00
Philipp Maier
92b9356ed2 runtime: fix lchan deletion in method reset
When we perform a reset while multiple channels are open (this is in
particular the case when parsing real world traces with pySim-trace). To
delete those channels during the reset we iterate over the dictionary
using the keys and delete the channels one by one. However, this must
not be done using the keys as index directly. Python will then throw an
exception: "RuntimeError: dictionary changed size during iteration".

Instead using the keys directly we should cast them into a list and then
using that list for the iteration.

Related: OS#6094
Change-Id: I430ef216cf847ffbde2809f492ee9ed9030343b6
2023-07-21 11:52:10 +02:00
Philipp Maier
7d86fe1d8a apdu/ts_102_221: extract channel number from dict before calling del_lchan
When the method del_lchan is called, closed_channel_nr still contains a dict
that contains the channel number under the key 'logical_channel_number'.
This will lead to an exception. We must extact the channel number from
the dict before we can use it with del_lchan. (See also
created_channel_nr)

Related: OS#6094
Change-Id: I399856bc227f17b66cdb4158a69a35d50ba222a7
2023-07-20 15:50:16 +00:00
Philipp Maier
cfb665bb3f pySim-shell: fix verify_adm command
The comman verify_adm does no longer work since the verify_adm method is
no longer available in the card base classes (cards.py). Let's use the
verify_chv method from SimCardCommands instead.

Change-Id: Ic87e1bff221b10d33d36da32b589e2737f6ca9cd
2023-07-20 17:36:08 +02:00
Philipp Maier
3175d61eb2 cards: fix swapped PIN mapping number
The constant for _adm_chv_num is swapped. It should be 0x0A, rather than
0xA0

Change-Id: I5680d2deee855ef316a98058e8c8ff8cf4edbbb2
2023-07-20 17:36:04 +02:00
Harald Welte
38306dfc04 pySim-shell: Add a mode where a pySim-shell cmd can be passed by shell
This adds a new operation mode for pySim-shell, where a single command
can be passed to pySim-shell, which then is executed before pySim-shell
terminates.

Example: ./pySim-shell.py -p0 export --json

Change-Id: I0ed379b23a4b1126006fd8f9e7ba2ba07fb01ada
Closes: OS#6088
2023-07-12 22:05:14 +02:00
Harald Welte
531894d386 move Runtime{State,Lchan} from pySim.filesystem to new pySim.runtime
Those two are really separate concepts, so let's keep them in separate
source code files.

Change-Id: I9ec54304dd8f4a4cba9487054a8eb8d265c2d340
2023-07-12 22:05:14 +02:00
Harald Welte
b77063b9b7 pySim/filesystem.py: remove unused class FileData
Change-Id: I62eb446e4995a532227a45c8cc521f5f80535d93
2023-07-12 22:05:14 +02:00
Harald Welte
6ad9a247ef pySim-shell: Iterate over CardApplication sub-classes
Rather than having to know and explicitly list every CardApplication,
let's iterate over the __subclasses__ of the CardApplication base class.

Change-Id: Ia6918e49d73d80acfaf09506e604d4929d37f1b6
2023-07-12 22:05:14 +02:00
Harald Welte
2d5959bf47 ts_102_221: Remove CardProfileUICCSIM
This profile has always been a hack/work-around for the situation that
a classic GSM SIM is not a UICC, and we didn't yet have the concept of
CardProfileAddons yet, so there was no way to probe and add something
to an UICC which was not an application with its own AID/ADF.

Since now we have CardProfileAddons (including one for GSM SIM),
and pySim-trace (the other user of CardProfileUICCSIM) has also switched
over to using CardProfileUICC + addons, we can remove this work-around.

Change-Id: I45cec68d72f2003123da4c3f86ed6a5a90988bd8
2023-07-12 22:05:14 +02:00
Harald Welte
323a35043f Introduce concept of CardProfileAddon
We have a strict "one CardProfile per card" rule.  For a modern UICC
without legacy SIM support, that works great, as all applications
have AID and ADF and can hence be enumerated/detected that way.

However, in reality there are mostly UICC that have legacy SIM, GSM-R
or even CDMA support, all of which are not proper UICC applications
for historical reasons.

So instead of having hard-coded hacks in various places, let's introduce
the new concept of a CardProfileAddon.  Every profile can have any
number of those.  When building up the RuntimeState, we iterate over the
CardProfile addons, and probe which of those are actually on the card.
For those discovered, we add their files to the filesystem hierarchy.

Change-Id: I5866590b6d48f85eb889c9b1b8ab27936d2378b9
2023-07-12 22:05:14 +02:00
Harald Welte
f9e2df1296 cdma_ruim: Fix unit tests and actually enable them
As pySim.cdma_ruim was not imported by test_files.py, the unit tests
were apparently never executed and hence didn't pass.  Let's fix both
of those problems.

Change-Id: Icdf4621eb68d05a4948ae9efeb81a007d48e1bb7
2023-07-12 22:05:14 +02:00
Harald Welte
659d7c11ca cards: all UICC should use sel_ctrl="0400" and SIM "0000"
Hence move this from the derived classes into the respective base
classes SimCardBase and UiccCardBase

Change-Id: Iad197c2b560c5ea05c54a122144361de5742aafd
2023-07-12 22:05:14 +02:00
Harald Welte
775ab01a2b cards: cosmetic rename, argument name should be scc, not ssc
ssc = SimCardCommands

Change-Id: I9d690a0a5b9b49ea342728a29b7d4ed10ac31e4e
2023-07-12 22:05:14 +02:00
Harald Welte
172c28eba8 cards: All derived of SimCardBase use CLA=A0; all UiccCardBase use CLA=00
Change-Id: Id61b549f68410631529349ee62b08a102f609405
2023-07-12 22:05:14 +02:00
Harald Welte
b314b9be34 ts_31_102, ts_31_103: Move legacy-only code to pySim.legacy
Change-Id: Ifebfbbc00ef0d01cafd6f058a32d243d3696e97e
2023-07-12 22:05:14 +02:00
Harald Welte
57ad38e661 create pySim.legacy.ts_51_011.py and move legacy code there
Those old flat dicts indicating FID to string-name mapping have long
been obsoleted by the pySim.filsystem based classes.

Change-Id: I20ceea3fdb02ee70d8c8889c078b2e5a0f17c83b
2023-07-12 22:05:14 +02:00
Harald Welte
a3961298ef pySim/cards: Add type annotations
Change-Id: Id5752a64b59097584301c860ebf74d858ed3d240
2023-07-12 22:05:14 +02:00
Harald Welte
f8d2e2ba08 split pySim/legacy/{cards,utils} from pySim/{cards,utils}
There are some functions / classes which are only needed by the legacy
tools pySim-{read,prog}, bypassing our modern per-file transcoder
classes.  Let's move this code to the pySim/legacy sub-directory,
rendering pySim.legacy.* module names.

The long-term goal is to get rid of those and have all code use the
modern pySim/filesystem classes for reading/decoding/encoding/writing
any kind of data on cards.

Change-Id: Ia8cf831929730c48f90679a83d69049475cc5077
2023-07-12 22:03:59 +02:00
Harald Welte
263fb0871c pySim/cards: Split legacy classes away from core SIM + UICC
This introduces an internal split between
* the code that is shared between pySim-shell and legacy tools, which is
  now in the new class hierarchy {Card,SimCard,UiccCard}Base
* the code that is only used by legacy tools,
  which is using the old class names inherited from the *Base above

All users still go through the legacy {Sim,Usim,Isim}Card classes, they
will be adjusted in subsequent patches.

Change-Id: Id36140675def5fc44eedce81fc7b09e0adc527e1
2023-07-12 21:35:17 +02:00
Harald Welte
02a7f7441f filesystem: Support selecting MF from MF
This was currently not handled in build_select_path_to(), resulting in
weird exceptions like 'Cannot determine path from MF(3f00) to MF(3f00)'

Change-Id: I41b9f047ee5dc6b91b487f370f011af994aaca04
2023-07-11 17:50:48 +02:00
Harald Welte
284efda086 pySim-prog: Also accept 18-digit ICCIDs
There are cards with 18-digit ICCIDs, so let's be a bit more tolerant.

Change-Id: I5395daeb2e96987335f6f9bf540c28d516001394
2023-07-11 11:09:00 +02:00
Harald Welte
fdcf3c5702 GlobalPlatform ADF.SD: Add command line reference + error message
The get_data shell command didn't have any interactive help / syntax,
and no meaningful error message in case an unknown data object name
was specified by the user.  Let's fix that.

Change-Id: I09faaf5d45118635cf832c8c513033aede1427e5
2023-07-11 08:54:04 +02:00
Harald Welte
a1561fe9ae ts_102_222: Remove unneeded imports
Change-Id: I0fc54a042f03ecf707fde81a859c7dd65a7009cc
2023-07-11 08:42:12 +02:00
Harald Welte
f9f8d7a294 pySim/transport: Use newly-defined ResTuple type
Let's use the newly-added ResTuple type annotation rather than
open-coding it everywhere.

Change-Id: I122589e8aec4bf66dc2e86d7602ebecb771dcb93
2023-07-11 08:42:12 +02:00
Harald Welte
fdb187d7ff pySim/commands.py: Better type annotations
Change-Id: I68081b5472188f80a964ca48d5ec1f03adc70c4a
2023-07-11 08:42:12 +02:00
Harald Welte
ab6897c4cd pySim/transport: More type annotations
Change-Id: I62e081271e3a579851a588a4ed7282017e56f852
2023-07-11 08:42:12 +02:00
Harald Welte
f5e26ae954 pySim/utils: define 'Hexstr' using NewType
This means Hexstr is no longer an alias for 'str', but a distinct
new type, a sub-class of 'str'.

Change-Id: Ifb787670ed0e149ae6fcd0e6c0626ddc68880068
2023-07-11 08:42:12 +02:00
Harald Welte
2352f2dcdd pySim/tlv.py: Fix TLV_IE_Collection from_dict with nested collections
This is all quite complicated.  In general, the TLV_IE.to_dict() method
obviously is expected to return a dict (with key equal to the snake-case
name of the class, value to the decode IE value).  This single-entry
dict can then be passed back to the from_dict() method to build the
binary representation.

However, with a TLV_IE_Collection, any TLV_IE can occur any number of
times, so we need an array to represent it (dict would need unique key,
which doesn't exist in multiple instances of same TLV IE).  Hence, the
TLV_IE_Collection.to_dict() method actually returns a list of dicts,
rather than a dict itself.  Each dict in the list represents one TLV_IE.

When encoding such a TLV_IE_Collection back from the list-of-dicts, we
so far didn't handle this special case and tried to de-serialize with
a class-name-keyed dict, which doesn't work.

This patch fixes a regression in the aram_store_ref_ar_do pySim-shell
command which got introduced in Change-Id I3dd5204510e5c32ef1c4a999258d87cb3f1df8c8

While we're fixing it, add some additional comments to why things are
how they are.

Change-Id: Ibdd30cf1652c864f167b1b655b49a87941e15fd5
2023-07-11 08:42:12 +02:00
Harald Welte
ba955b650e pySim/tlv.py: Don't create an exception from within raise
An invalid variable used in a raise ValueError() would cause a further
exception, depriving the user of a meaningful error message.

Change-Id: I6eb31b91bd69c311f07ff259a424edc58b57529a
2023-07-11 08:42:12 +02:00
Harald Welte
30de9fd8ab TLV_IE_Collection: use snake-style names during from_dict()
The TLV_IE_Collection, just like the individual TLV classes, do
use their snake-style names when converting from binary to dict
using the to_dict() method.  It is inconsistent (and a bug) to
expect the CamelCase names during encoding (from_dict).  After all,
we want the output of to_dict() to be used as input to from_dict().

Change-Id: Iabd1ad98c3878659d123eef919c22ca824886f8a
2023-07-11 08:42:12 +02:00
iw0
f818acd5eb pySim-shell: Unregister profile commands during equip
This avoids error messages about re-registering 'AddlShellCommands' commandsets during 'equip()' in the bulk_script command.

Change-Id: I893bb5ae95f5c6e4c2be2d133754e427bc92a33d
2023-07-09 08:12:28 +00:00
Harald Welte
f4a01472bf pySim-shell: Support USIM specific methods/commands on unknown UICC
So far, if no known programmable card (like sysmoISIM) has been found,
we were using the SimCard base class.  However, once we detect an UICC,
we should have switched to the UsimCard class, as otherwise the various
methods called by USIM/ISIM specific commands don't exist and we get
weird 'SimCard' object has no attribute 'update_ust' execptions.

The entire auto-detection and the legacy SimCard / UsimCard classes
are showing the legacy of the code base and should probably be
re-architected.  However, let's fix the apparent bug for now.

Change-Id: I5a863198084250458693f060ca10b268a58550a1
Closes: OS#6055
2023-07-04 21:17:19 +02:00
Harald Welte
fa9f348180 ts_31_103: enable encode tests for files containing single TLV IE
Now that we have fixed OS#6073 in the previous commit, we can enable
the so-far disabled encoder tests for EF.{DOMAIN,IMPU,IMPI} and
remove associated FIXMEs.

Change-Id: I79bfc5b77122907d6cc2f75605f9331b5e650286
2023-06-27 09:29:37 +02:00
Harald Welte
579ac3ec0e tlv: Fix IE.from_dict() method
The existing IE.from_dict() method *supposedly* accepts a dict as
input value, but it actually expects the raw decoded value, unless it is
a nested IE.  This is inconsistent in various ways, and results in a bug
visible at a higher layer, such as files like EF.{DOMAIN,IMPI,IMPU},
which are transparent files containing a single BER-TLV IE.

Decoding such files worked, but re-encoding them did not, due to the
fact that we'd pass a dict to the from_dict method, which then gets
assigned to self.decoded and further passed along to any later actual
encoder function like to_bytes or to_tlv.  In that instance, the dict
might be handed to a self._construct which has no idea how to process
the dict, as it expects the raw decoded value.

Change-Id: I3dd5204510e5c32ef1c4a999258d87cb3f1df8c8
Closes: OS#6073
Related: OS#6072
2023-06-27 09:29:37 +02:00
Harald Welte
0ec01504ab cosmetic: Implement cmd2.Settable backwards-compat via wrapper class
Let's avoid too many open-coded if-clauses and simply wrap it in
a compatibility class.

Change-Id: Id234f3fa56fe7eff8e1153d71b9be8a2e88dd112
2023-06-27 09:29:25 +02:00
Harald Welte
985ff31efa work-around what appears to be a pylint bug
smpp.pdu.pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS very much exists,
and I can prove that manually in the python shell.  So let's assume this
is a pylint bug and work around it

pySim/sms.py:72:21: E1101: Instance of 'DataCodingScheme' has no 'GSM_MESSAGE_CLASS' member (no-member)

Change-Id: Iab34bae06940fecf681af9f45b8657e9be8cbc7b
2023-06-27 09:26:28 +02:00
Harald Welte
e126872a29 Fix run-editor bug with cmd2 >= 2.0.0 compatibility
In cmd2, the upstream authors decided to rename a method in 2.0.0
without providing a backwards compatibility wrapper.  Let's add that
locally.

Change-Id: Iaa17b93db13ba330551799cce5f0388c78217224
Closes: OS#6071
2023-06-25 08:22:56 +02:00
Harald Welte
721ba9b31f tests: Add new, data-driven OTA tests
Rather than writing one test class with associated method for each
OTA algorithm / test, let's do this in a data-driven way, where new
test cases just have to provide test data, while the code iterates over
it.

Change-Id: I8789a21fa5a4793bdabd468adc9fee3b6e633c25
2023-06-18 10:50:50 +02:00
Harald Welte
0b32725f80 Add support for encoding/decoding SMS in TPDU and SMPP format
This is important when talking OTA with a SIM.

Change-Id: I0d95e62c1e7183a7851d1fe38df0f5133830cb1f
2023-06-18 10:46:23 +02:00
Harald Welte
7e55569f3a docs: Add section on pySim-trace to user manual
Change-Id: I5edb222818f00e36ed5b067e0f8d5786f39ae887
2023-06-13 15:10:25 +00:00
Philipp Maier
e345e1126d pySim-shell: fix reset command
The API of the lchan object has changed. It no longer features the reset
method used by the pySim-shell reset command. Let's fix this by using
the reset method of the card object.

Change-Id: I55511d1edb97e8fa014724598ec173dd47fe25c1
2023-06-11 19:11:37 +00:00
Harald Welte
f422eb1886 Add ".py" suffix to sphinx-argparse generated docs
This is important to produce the right command syntax when generating
command line reference in the user manual.  However, we shouldn't add
this kludge to the individual programs, but only to the documentation
using the :prog: syntax.

Change-Id: I2ec7ab00c63d5d386f187e54755c71ffc2dce429
2023-06-09 11:50:18 +02:00
Harald Welte
f9a5ba5e0f 31.102: Fix EF.Routing_Indicator for odd number of digits
The routing indicator is BCD-encoded but has an arbitrary length of
1, 2, 3 or 4 digits.

In order to support the odd lengths of 1 or 3, we must not pad on the
byte level, but on the nibble level. This requires a slight extension of
the Rpad() Adapter.

Change-Id: I6c26dccdd570de7b7a4cd48338068e230340ec7c
Fixes: OS#6054
2023-06-09 09:19:53 +02:00
Harald Welte
1dce498a67 README: remove redundancy 'Manual' and 'Documentation
Also, re-order sections oriented more towards the user (Docs first)

Change-Id: I4fc76222a1c22685131cb6926721ce24f0373046
2023-06-08 21:46:24 +02:00
Harald Welte
555cf6f6db README: rephrase initial section; add HPSIM; programmable vs. standard
Change-Id: Ied7bce9fc4ebc9a71093ac41d9c1b8e67fe04d7e
2023-06-08 21:46:24 +02:00
Harald Welte
75e31c5d5b test_ota: Add one first OTA SMS AES128 unit test
Change-Id: Id4a66bbfaec2d8610e8a7a2c72c0dfd08332edcd
2023-06-08 17:29:46 +02:00
Harald Welte
19b4a971e9 SJA5: EF.USIM_AUTH_KEY: Display / enforce proper length TUAK K
The K value in case of TUAK can be 16 or 32 bytes long.  We used to
permit/parse/display 32 bytes even if only 16 bytes was configured.

Let's enforce the correct length of "K".

Fixes: OS#6053
Change-Id: Ia0f9a2138f16dce72f3118001e95baa1c80f23ce
2023-06-08 17:28:40 +02:00
Harald Welte
7ec822373e ts_31_102: Add shell command for GET IDENTITY
GET IDENTITY is used in the "SUCI computation on USIM" feature.

Change-Id: I619d397900dbd6565f8f46acdabcee511903830c
2023-06-07 15:54:17 +00:00
Philipp Maier
621f78c943 serial: return a return code in reset_card()
The method reset_card does not return a return code, while the
coresponding pcsc implementation does return 1 on success.

Change-Id: I658dd6857580652696b4a77e7d6cfe5778f09eff
2023-06-07 10:00:52 +00:00
Matan Perelman
60951b0c17 utils: Remove format_xplmn leading zeros in MNC
Change-Id: I803edafbd892c2b32b884d0b39fed61967a3d68b
2023-06-07 10:00:07 +00:00
Matan Perelman
777ee9e54d Add FPLMN read and program
Change-Id: I9ce8c1af691c28ea9ed69e7b5f03f0c02d1f029b
2023-06-07 10:00:07 +00:00
Harald Welte
1de62c41d7 pySim/apdu/ts_31_102.py: Add Rel17 5G NSWO context for GET IDENTITY
Change-Id: I6ce5848ca4cf04430be7767e9cb2d18f4c5a5531
2023-06-07 11:14:07 +02:00
Harald Welte
b0e0dce80a ts_102221: Add "resume_uicc" command
We've had a "suspend_uicc" command since commit
ec95053249 in 2021, but didn't yet
have the corresponding "resume" pair.

Note that you cannot really execute this in a reasonable way from
within pySim, as it is required to power-cycle the card
between SUSPEND and RESUME, see TS 102 221 Section 11.1.22.3.2

Change-Id: I3322fde74f680e77954e1d3e18a32ef5662759f2
2023-06-07 11:13:34 +02:00
Harald Welte
659781cbe1 Move "suspend_uicc" command from pySim-shell to ts_102_221.py
The SUSPEND UICC command is a TS 102 221 (UICC) command, so move
it to the UICC Card Profile.

Also, make sure that any shell command sets specified in the
CardProfile are actually installed during equip().

Change-Id: I574348951f06b749aeff986589186110580328bc
2023-06-07 11:10:33 +02:00
Philipp Maier
4e5aa304fc ts_31_102: fix typo
Change-Id: Ic8f93a55b974984472356f48518da91c6a521409
2023-06-06 19:24:29 +02:00
Harald Welte
c85ae4188f Fix result parsing of "suspend_uicc"
prior to this patch, the suspend_uicc command would always cause a
python exception as a list of integers was returned by decode_duration rather than a single integer (that can be used with %u format string).

Change-Id: I981e9d46607193176b28cb574564e6da546501ba
2023-06-06 17:36:39 +02:00
Harald Welte
892526ffd0 pySim-shell: Unregister TS 102 222 commands during 'equip'
This avoids error messages about re-registering the same TS 102 222
commands during executing the 'equip' command.

Change-Id: I3567247fe84e928e3ef404c07eff8250ef04dfe9
2023-06-06 17:36:39 +02:00
Harald Welte
e619105249 HPSIM application support
Support HPSIM as specified in 3GPP TS 31.104

Change-Id: I2729fd2b88cd13c36d7128753ad8d3e3d08a9b52
2023-06-06 17:36:39 +02:00
Harald Welte
d75fa3f7c9 Switch from pycryptodome to pycryptodomex
So for some weird historical reasons, the same python module is
available as pycryptodome (Crypto.* namespace) and pycryptodomex
(Cryptodome.* namespace).  See the following information on the project
homepage: https://www.pycryptodome.org/src/installation

To make things extra-weird, Debian choose to package pycryptodomex as
python3-pycryptodome
(https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=886291).

So in order to support both Debian-packaged and differently-installed
packages, let's switch to pycryotodomex on all platforms/installers.

Change-Id: I04daed01f51f9702595ef9f9e0d7fcdf1e4adb62
2023-06-05 20:58:11 +02:00
Harald Welte
219a5f369c OTA: Fix padding of AES CMAC
When using AES CMAC for authentication of OTA messages, we must not pad
the user data before calling the CMAC function. This is unlike the DES
MAC, where padding to the DES block size is mandatory.

This bug was discovered when trying to talk OTA with AES to a
sysmoISIM-SJA5.  This patch makes the OTA AES interoperate with the
card.  Also, with this patch the cryptographic results of pySim/ota.py
are identical to those of the java code
org.opentelecoms.gsm0348.impl.crypto.CipheringManager

Change-Id: I4b40b5857f95ccb21c35795abe7a1995e368bac3
2023-06-03 12:45:35 +00:00
Harald Welte
03650582e0 SJA5: Proper encode/decode of TUAK data in EF.USIM_AUTH_KEY
Unfortunately, TUAK requires a number of additional (and
differently-sized) parameters, so the format of EF.USIM_AUTH_KEY
differs significantly depending on TUAK or non-TUAK case.

Change-Id: I0dcfe05777510fb34973dc2259b137133d8e199d
2023-06-03 12:45:35 +00:00
Harald Welte
557c13685e SJA5: Add TUAK + XOR-2G algorithm definitions for EF_[U]SIM_AUTH_KEY
Change-Id: I62a7255d991fa1ed09a7c9bcf8be4b68acfa61a7
2023-06-03 12:45:35 +00:00
Harald Welte
954ce95a16 SJA2: Implement DF.SYSTEM/EF.0348_KEY using construct
This implicitly adds support for JSON->binary encoding, not just
decoding (previous code predating construct support).

Change-Id: I0994d9f66a504dd3c60b43ed5cf6645515dcbc6a
2023-06-03 12:45:35 +00:00
Harald Welte
ba6d6ab64f ts_31_102: EF_SUPI_NAI: Decode/Encode GLI+GCI as UTF-8 strings
According to TS 23.003 Section 28.15 and 28.16 both GLI and GCI
are NAI as defined in IETF RFC 7542, which in turn specifies they
are encoded in UTF-8.

Change-Id: I0a82bd0d0a2badd7bc4a1f8de2c3e3c144ee5b12
2023-06-03 12:45:35 +00:00
Harald Welte
455611c9a3 ts_31_102: Add decoder/encoder for DF.5GS/EF.Routing_Indicator
This file is rather important for 5G SA operation, so we should have
a proper encoder/decoder in place.

Change-Id: I1b37fdfc2807976880b2cafb61951f08eebeb344
2023-06-03 12:45:35 +00:00
Tobias Engel
d70ac22618 modem_atcmd: raise ProtocolError instead of ReaderError on CME ERROR
Also accept ProtocolError in addition to SwMatchError in filesystem.py
when probing for applications

Change-Id: I82b50408328f8eaaee5c9e311c4620d20f930642
2023-06-02 15:35:43 +00:00
Vadim Yanitskiy
bca01523df setup.py: fix syntax errors (missing commas)
Change-Id: Ia53a659ad9652d582e2bf4a039a3e18631435072
Fixes: 2b15e315 "setup: add missing pyyaml to setup.py and README.md"
Fixes: 93aac3ab "pySim-shell: fix compatibility problem with cmd2 >= 2.0.0 (Settable)"
2023-05-28 17:13:21 +07:00
Matan Perelman
c296cb593e cards: Add support for Gialer SIM cards
Change-Id: Icd2021aec630ac018f66ab565e03112047389e17
2023-05-27 12:37:16 +02:00
Merlin Chlosta
69b69d4d84 docs: add SUPI/SUCI usage example
Change-Id: I2908ea9df7e78c596554731085902e2ab7278328
2023-05-27 12:37:12 +02:00
Harald Welte
0489ae67cf cards.py: support ATR-based detection of sysmoISIM-SJA5
The cards are 99% software-compatible to the SJA2, so let's just
derive the SJA5 class from the SJA2

Change-Id: I706631baaf447c49904277886bc9a3f6ba3f5532
2023-05-25 22:23:07 +02:00
Harald Welte
2bee70cbac ts_31_102: Add DF.SAIP support
DF.SAIP (SIMalliance Interoperable Profile) is not part of 31.102,
but something from the eSIM/eUICC universe of TCA (formerly known as
SIMalliance).  However, as 3GPP does not specify how/where the card
stores the information required for SUCI calculation, the
TCA/SIMalliance standard is the only standard there is.  Some CardOS
start to use this standard even for non-eSIM/eUICC use cases.

Change-Id: Iffb65af335dfdbd7791fca9a0a6ad4b79814a57c
2023-05-25 09:58:34 +02:00
Harald Welte
24e77a7758 ts_31_102: Fix FID + SFI of EF.MCHPPLMN
Change-Id: I7e24c904e47cc6f90e90b8634cbed478bd14231f
2023-05-25 07:55:44 +00:00
Harald Welte
5206429c0c ts_31_102: Fix FID of EF.OPL5G (it's 4F08 instead of 6F08)
Change-Id: I68c7ad93dabd768d80ae629498aee29d7bab5542
2023-05-25 07:55:44 +00:00
Harald Welte
04bd5140fd ts_31_102: Fix EF.NIA FID
The FID in ADF.USIM is different from the FID in DF.GSM.  So while
we can re-use the ts_51_011 EF_NIA class definition, we must pass in
a different fid to the constructor.

Change-Id: Ib414d5b476666e276824266e33b341175a2ee05a
2023-05-25 07:55:44 +00:00
Harald Welte
33eef850c0 ts_51_011: Fix EF.Phase FID (it's 6FAE, not 6FA3)
Change-Id: I11df83b17b8d6eaab309908cbee646c888abab0d
2023-05-25 07:55:44 +00:00
Harald Welte
10a1a0a22e ts_51_011: Fix FID of EF.BCCH
It's 6F74, not 6F7F! (see TS 51.011 Section 10.3.14)

Change-Id: I9d90fa05a0f926f99a5d4832341cc8a9449df7ae
2023-05-25 07:55:44 +00:00
Harald Welte
fc67de2219 ts_31_102: Extend from Rel16 to Rel17
This adds definitions for a variety of files which were added in Release
17 of 3GPP TS 31.102.

Change-Id: I61badc1988b006a1065bdfdcc8a93b758e31f79b
2023-05-25 07:55:44 +00:00
Harald Welte
c224b3b5f1 ts_51_011: Add sst_service_[de]{activate,allocate} shell commands
Just like the existing commands for UST/IST: Allow the user to
activate/deactivate individual services.  As EF.SST also contains
information about "allocation" of a service, let's have commands for
allocation and activation.

Change-Id: If959d06248cb1a9d2c0a21cdd40d438726cbc5f0
2023-05-25 07:55:44 +00:00
Vadim Yanitskiy
ade366d2a9 setup.py: add missing packages for pySim-trace.py
pySim-trace.py is broken if pySim is installed using setup.py:

  fixeria@DELL:~$ pySim-trace.py
  Traceback (most recent call last):
    File "/usr/bin/pySim-trace.py", line 8, in <module>
      from pySim.apdu import *
  ModuleNotFoundError: No module named 'pySim.apdu'

Change-Id: I371143cb4009db46275ec7a020497b909dcc3b4e
2023-05-24 09:03:01 +00:00
Vadim Yanitskiy
a793552b4f contrib/jenkins.sh: print pylint version before running it
Change-Id: Icc96ff16af482581dc97a387bcff1374fbb620f3
Related: OS#6034
2023-05-23 13:12:41 +02:00
Oliver Smith
e47ea5f2e5 Fix pylint errors
In a previous patch the dependency on cmd2 was changed from cmd2==1.5 to
cmd2>=1.5. After this was merged, this lead to the docker images getting
rebuilt and now having a higher cmd2 version that gets used in the CI
checks. So while the patch was in review, pylint was actually running
with a lower cmd2 version and was taking different code paths.

Fix for:
pySim-shell.py:30:4: E0611: No name 'fg' in module 'cmd2' (no-name-in-module)
pySim-shell.py:30:4: E0611: No name 'bg' in module 'cmd2' (no-name-in-module)
pySim-shell.py:154:8: E1123: Unexpected keyword argument 'use_ipython' in method call (unexpected-keyword-arg)
pySim-shell.py:171:30: E1120: No value for argument 'settable_object' in constructor call (no-value-for-parameter)
pySim-shell.py:173:30: E1120: No value for argument 'settable_object' in constructor call (no-value-for-parameter)
pySim-shell.py:175:30: E1120: No value for argument 'settable_object' in constructor call (no-value-for-parameter)
pySim-shell.py:176:30: E1120: No value for argument 'settable_object' in constructor call (no-value-for-parameter)

Fixes: f8a3d2b3 ("requirements.txt: allow cmd2 versions greater than 1.5")
Fixes: OS#6034
Change-Id: I182d3a2b87e70ed551a70c88d3d531a36bf53f53
2023-05-23 13:12:33 +02:00
Philipp Maier
3bcc22f73d README.md: add missing pycryptodome to dependency list
Change-Id: Ib3cf13a1ad38749ac82d1b36fa32d9c5aba29e1a
2023-05-17 17:30:49 +02:00
Philipp Maier
2b15e315e2 setup: add missing pyyaml to setup.py and README.md
Change-Id: I1d35f38b17a315dd58e8dd91a27bfa6c2c85905d
2023-05-17 17:30:49 +02:00
Philipp Maier
f8a3d2b3db requirements.txt: allow cmd2 versions greater than 1.5
Since we now have fixed the compatibility issues with recent cmd2
versions, we may allow also versions greater than 1.5 in the
requirements.txt

Change-Id: I87702c5250a3660c84458939167bffdca9c06059
2023-05-17 17:30:49 +02:00
Harald Welte
961b803ec4 pySim-shell: fix compatibility problem with cmd2 >= 2.3.0 (bg)
cmd2.fg and cmd2.bg have been deprecated in cmd2 2.3.0 and removed
in cmd2 2.4.0. Let's work around this by a version check.

Related upstream commits:
(See also: https://github.com/python-cmd2/cmd2)

Commit f57b08672af97f9d973148b6c30d74fe4e712d14
Author: Kevin Van Brunt <kmvanbrunt@gmail.com>
Date:   Mon Oct 11 15:20:46 2021 -0400

and

Commit f217861feae45a0a1abb56436e68c5dd859d64c0
Author: Kevin Van Brunt <kmvanbrunt@gmail.com>
Date:   Wed Feb 16 13:34:13 2022 -0500

Change-Id: I9fd32c0fd8f6d40e00a318602af97c288605e8e5
2023-05-17 17:30:49 +02:00
Harald Welte
c85d4067fd pySim-shell: fix compatibility problem with cmd2 >= 2.0.0 (include_ipy)
In version 2.0.0, the use_ipython parameter in the Cmd constructor is
renamed to include_ipy. There are still plenty of older cmd2
installations around, so let's work around this using a version check.

See also: https://github.com/python-cmd2/cmd2

Commit: 2397280cad072a27a51f5ec1cc64908039d14bd1
Author: Kevin Van Brunt <kmvanbrunt@gmail.com>
Date: 2021-03-26 18:56:33

This commit is based on pySim gerrit changes:
Ifce40410587c85ae932774144b9548b154ee8ad0
I19d28276e73e7024f64ed693c3b5e37c1344c687

Change-Id: Ibc0e18b002a03ed17933be4d0b4f4e86ad99c26e
2023-05-17 17:30:49 +02:00
Harald Welte
93aac3abe6 pySim-shell: fix compatibility problem with cmd2 >= 2.0.0 (Settable)
In cmd2 relase 2.0.0 the constructor of Settable adds a settable_object
parameter, which apparantly was optional at first, but then became
mandatory. Older versions must not have the settable_object parameter
but versions from 2.0.0 on require it. Let's add a version check so that
we stay compatible to cmd2 versions below and above 2.0.0.

See also: https://github.com/python-cmd2/cmd2

Commit 486734e85988d0d0160147b0b44a37759c833e8a
Author: Eric Lin <anselor@gmail.com>
Date:   2020-08-19 20:01:50

and

Commit 8f981f37eddcccc919329245b85fd44d5975a6a7
Author: Eric Lin <anselor@gmail.com>
Date: 2021-03-16 17:25:34

This commit is based on pySim gerrit change:
Ifce40410587c85ae932774144b9548b154ee8ad0

Change-Id: I38efe4702277ee092a5542d7d659df08cb0adeff
2023-05-17 17:30:49 +02:00
Vadim Yanitskiy
87dd020d5f Add very basic profile for R-UIM (CDMA) cards
R-UIM (CDMA) cards are pretty much like the normal GSM SIM cards and
"speak" the same 2G APDU protocol, except that they have their own file
hierarchy under MF(3f00)/DF.CDMA(7f25).  They also have DF.TELECOM(7f10)
and even DF.GSM(7f20) with a limited subset of active EFs.  The content
of DF.CDMA is specified in 3GPP2 C.S0023-D.

This patch adds a very limited card profile for R-UIM, including auto-
detecion and a few EF definitions under DF.CDMA.  This may be useful
for people willing to explore or backup their R-UIMs.  To me this was
useful for playing with an R-UIM card from Skylink [1] - a Russian
MNO, which provided 450 MHz CDMA coverage until 2016.

[1] https://en.wikipedia.org/wiki/Sky_Link_(Russia)

Change-Id: Iacdebdbc514d1cd1910d173d81edd28578ec436a
2023-05-10 00:14:13 +00:00
Vadim Yanitskiy
6b19d80229 ts_51_011: fix EF_ServiceTable: use self for static method
Even though _bit_byte_offset_for_service() is a @staticmethod, it's
still available via self, just like any non-static method.

Change-Id: I3590dda341d534deb1b7f4743ea31ab16dbd6912
2023-05-10 00:14:13 +00:00
Vadim Yanitskiy
e63cb2cc4d setup.py: add missing pySim-trace.py' to scripts[]
Change-Id: I44dfcf48ae22182bd7aaa908559f3d1e1e31acce
2023-05-05 15:12:17 +07:00
Vadim Yanitskiy
b34f23448c filesystem: define more convenient codec for EF.ACC
This patch improves the output of the 'read_binary_decoded' command:

pySIM-shell (MF/DF.GSM/EF.ACC)> read_binary_decoded
{
    "ACC0": false,
    "ACC1": false,
    "ACC2": false,
    "ACC3": false,
    "ACC4": false,
    "ACC5": false,
    "ACC6": false,
    "ACC7": false,
    "ACC8": false,
    "ACC9": false,
    "ACC10": false,
    "ACC11": false,
    "ACC12": false,
    "ACC13": false,
    "ACC14": false,
    "ACC15": true
}

And allows to set/unset individual ACCs using 'update_binary_decoded':

pySIM-shell (MF/DF.GSM/EF.ACC)> update_binary_decoded --json-path 'ACC15' 0
"0000"
pySIM-shell (MF/DF.GSM/EF.ACC)> update_binary_decoded --json-path 'ACC8' 1
"0100"
pySIM-shell (MF/DF.GSM/EF.ACC)> update_binary_decoded --json-path 'ACC0' 1
"0101"

Change-Id: I805b3277410745815d3fdc44b9c0f8c5be8d7a10
Related: SYS#6425
2023-04-18 04:36:34 +07:00
Vadim Yanitskiy
0d80fa9150 pySim-prog.py: fix SyntaxWarning: using is with a literal
Change-Id: If9460bf827242a1dfc518213e3faa9137a21869a
2023-04-14 00:11:19 +07:00
Philipp Maier
7b9e24482d pySim-shell: add cardinfo command
It may sometimes be helpful to get a bit of general information about
the card. To sort out problems it sometimes helps to get an idea what
card type and ICCID pySim-shell has in memory.

Change-Id: If31ed17102dc0108e27a5eb0344aabaaf19b19f9
2023-03-27 10:37:28 +02:00
Harald Welte
61ef1571f9 pySim-shell.py: add a command for RUN GSM ALGORITHM
Change-Id: Id7876d83d018aca79253784411d3a9d54a249a0a
2023-03-22 09:57:32 +00:00
Vadim Yanitskiy
9970f59f4f SimCardCommands.run_gsm(): use send_apdu_checksw()
Change-Id: Ib713cf8154a3aba72bc5776a8d99ec47631ade28
2023-03-22 09:57:32 +00:00
Vadim Yanitskiy
1dd5cb540d fix SimCardCommands.run_gsm(): always use CLA=0xa0
Depending on the card type (SIM or USIM/ISUM), self.cla_byte may
be either 0xa0 or 0x00.  Sending RUN GSM ALGORITHM with CLA=0x00
fails with SW=6985 (Command not allowed), so let's make sure
that we always use CLA=0xa0 regardless of the card type.

Change-Id: Ia0abba136dbd4cdea8dbbc3c4d6abe12c2863680
2023-03-22 09:57:32 +00:00
Oliver Smith
41fbf12dba gitignore: add manuals related files
Change-Id: I93a63b33032f93f381b8ef451aecc97d3011ce8c
2023-03-20 13:30:38 +01:00
Oliver Smith
308d7cdf78 docs/Makefile: don't forward shrink to sphinx
Adjust the catch-all target at the end of the Makefile that is supposed
to route all unknown targets to sphinx, so it doesn't do this for the
shrink target. The shrink target has recently been added to
Makefile.common.inc in osmo-gsm-manuals, which gets included right above
the catch-all target. So it isn't an unknown target, but for some reason
the sphinx catch-all runs in addition to the shrink target (runs
shrink-pdfs.sh, see output below) and fails. As I did not add the
catch-all logic, preserve it but add an exception for the shrink rule.

Fix for:
  + make -C docs publish publish-html
  make: Entering directory '/build/docs'
  /opt/osmo-gsm-manuals/build/shrink-pdfs.sh _build/latex/osmopysim-usermanual.pdf
  * _build/latex/osmopysim-usermanual.pdf: 272K (shrunk from 336K)
  Running Sphinx v5.3.0

  Sphinx error:
  Builder name shrink not registered or available through entry point

Related: SYS#6380
Change-Id: If2802bb93909aba90debe5e03f3047cec73e2f54
2023-03-20 12:28:06 +01:00
Harald Welte
0707b80ad3 ts_102_222: Implement support for RESIZE FILE for an EF
This adds pySim-shell support for the RESIZE FILE command in order
to change the size of linear fixed or transparent EF.

Change-Id: I03fbb683e26231c75f345330ac5f914ac88bbe7a
2023-03-09 09:49:40 +00:00
Oliver Smith
da1f562294 docs: change upload path for html docs
Upload it to pysim/master/html instead of latest/pysim.

Related: OS#5902
Change-Id: I0b338bd7d1fb2620d63e651eeb8e40c7d8e722e2
2023-03-07 12:44:14 +01:00
Harald Welte
a07d509de6 docs: Document the file-specific commands for ADF.USIM/EF.EST
Change-Id: Iddba9f25ba957f03ca25628a7742fe40fd79c030
2023-02-23 10:02:49 +01:00
Harald Welte
18b7539925 31.102: EF.EST enables/disables services; name commands accordingly
EF.EST is the *enabled* services table.  Let's call the shell commands
enable and disable, rather than activate/deactivate.

Change-Id: Iacbdab42bc08e2be38ad7233d903fa7cda0d95b6
2023-02-23 10:00:51 +01:00
Harald Welte
577312a04e docs: Add reference for various commands
A number of more recently introduced commands were not yet listed in the
manual, let's fix that.

Change-Id: I39150f55eecb5d8ff48292dc5cc0f9e16dd4398c
2023-02-23 09:52:44 +01:00
Philipp Maier
8490240ce6 cards: sysmo-isim-sja2: make sure an ADF is present in EF.DIR before selecting it
sysmo-isim-sja2 may come in different configurations, so some may
intentionally lack ADF.USIM or ADF.ISIM. Since select_adf_by_aid() may
raise an exception when selecting a non existent file we should make
sure that the ADF we intend to select is indeed present. A reliable way
to do this is to check if the application is registered in EF.DIR.

Change-Id: Icf6f6b36f246398af408ec432d493fe3f22963dd
2023-02-10 18:28:39 +01:00
Harald Welte
865eea68c3 filesystem: add unit tests for encoder/decoder methods
Lets add test vectors for the per-record/per-file encode/decode of
our various classes for the Elementary Files.

We keep the test vectors as class variables of the respective EF-classes
to ensure implementation and test vectors are next to each other.

The test classes then iterate over all EF subclasses and execute the
decode/encode functions using the test vectors from the class variables.

Change-Id: I02d884547f4982e0b8ed7ef21b8cda75237942e2
Related: OS#4963
2023-02-01 10:52:23 +01:00
Harald Welte
d2edd414a8 ts_51_011: Fix decoding/encoding of EF_LOCIGPRS
The P-TMSI signature is a 3-byte value, not a 1-byte value.

Change-Id: I06e8d3efe0b3cf3970159c913acfd2f72280302d
2023-01-31 17:26:09 +01:00
Harald Welte
caa94b5a81 Assume first record number if caller specifies none
This fixes a regression introduced in Change-Id
I02d6942016dd0631b21d1fd301711c13cb27962b which added support for
different encoding/decoding of records by their record number.

Change-Id: I0c5fd21a96d2344bfd9551f31030eba0769636bf
2023-01-31 17:26:09 +01:00
Harald Welte
9b9efb6a7a ts_31_102: Fix several bugs in EF_ECC encoder
The encoder function apparently was never tested, it didn't match at all
the output of the decoder, not even in terms of the string keys of the
dict.

Change-Id: Id67bc39d52c4dfb39dc7756d8041cbd552ccbbc4
2023-01-31 17:26:09 +01:00
Harald Welte
136bdb065b ts_51_011: EF_SMSP: Use integer division in ValidityPeriodAdapter
ValidityPeriodAdapter() must return integer values when encoding a
value, as only integer values can be expressed in the binary format.

Change-Id: I0b431a591ac1761d875b5697a71b6d59241db87d
2023-01-31 17:26:09 +01:00
Harald Welte
9181a69a55 gsm_r: EF_IC: Network String Table Index is 16bit, not 8bit
As per EIRENE GSM-R SIM-Card FFFIS, EF_IC conatains records of 1+2+2+2
bytes, the network string table index is 16bit and not 8bit as we
implemented so far.

Change-Id: I9e3d4a48b3cb6fb0ecf887b04c308e903a99f547
2023-01-31 16:00:20 +00:00
Harald Welte
5924ec4d97 ts_51_011: Improve decoding of SELECT response for classic SIM
When decoding the SELECT response of a clasic GSM SIM without
UICC functionality, we
* did not decode the record length or number of records
* accidentially reported the EF file_size as available_memory (like DF)

Let's fix those two, and also add a comment on how the output dict
of decode_select_response() should look like.

As a result, code like 'read_records' now knows the number of records
and can iterate over them rather than raising exceptions.

Change-Id: Ia8e890bda74e3b4dacca0673d6e5ed8692dabd87
Closes: OS#5874
2023-01-27 20:46:08 +01:00
Harald Welte
a1bb3f7147 ts_51_011: Support EF.LND
This file is a optional file specified by TS 51.011, storing the last
numbers dialled.  As the EIRENE FFFIS for GSM-R SIM refers to this,
we must implement it to have full GSM-R support in pySim.

Change-Id: I3b7d6c7e7504b7cc8a1b62f13e8c0ae83a91d0f0
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
0dc6c201e5 ts_51_011, ts_31_102: point to proper EF_EXTn file
We're using a shared class to implement the identical file encoding
for EF.{ADN,SDN,MBDN,BDN,FDN,CFIS}.  However, they all point to
different extension files.

Previosly for EF.SDN:
    "ext1_record_id": 255

Now for EF.SDN:
    "ext3_record_id": 255

Change-Id: I5301d41225266d35c05e41588811502e5595520d
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
f11f1308b1 ts_51_011: Implement Extended BCD Coding
TS 51.011 specifies an "Extended BCD Coding" in Table 12 of Section
10.5.1. It allows to express the '*' and '#' symbols used in GSM
SS and/or USSD codes.

This improves decoding from
    "dialing_nr": "a753b1200f",
to
    "dialing_nr": "*753#1200f",

Change-Id: Ifcec13e9b296dba7bec34b7872192b7ce185c23c
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
9ba68df3cc ts_51_011: Support EF.SDN
DF.TELECOM/EF.SDN (Service Dialling Numbers) is specified in section
10.5.9 of TS 51.011 and required by EIRENE for GSM-R.

Let's use the pre-existing EF.ADN decoder to decode this file.

Change-Id: If91332b10138096d465a9dccf90744de2c14b2be
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
5b9472db7a ts_51_011: Fix bit-order in EF.VGCSS and EF.VBSS
Those files contain a bit-mask of active group IDs stored at the
respective positions in EV.VGCS and EF.VBS.  However, the bit-order
of each byte is reversed.

Change-Id: I77674c23823aae71c9504b1a85cd75266edadc6f
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
73a7fea357 gsm_r: Fix byte/nibble ordering of predefined_value1
Change-Id: Ia0dd8994556548a17a0a3101225c23e804511717
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
6bf2d5f216 gsm_r: EF_Predefined: Decode first record different from others
In their infinite wisdom, the authors of the EIRENE FFFIS for GSM-R SIM
cards invented yet a new way of encoding data in SIM card files: The
first record of a file may be encoded differently than further records
of files.

This patch implements the feature based on the newly-introduced way by
which we pass the record number to the encoder and decoder methods.

Change-Id: Ib526f6c3c2ac9a945b8242e2e54536628376efc0
Related: OS#5784
2023-01-27 20:46:08 +01:00
Harald Welte
f6b37af721 Prepare for decoding/encoding records differently based on record number
In their infinite wisdom, the authors of the EIRENE FFFIS for GSM-R SIM
cards invented yet a new way of encoding data in SIM card files: The
first record of a file may be encoded differently than further records
of files.

Let's add the required infrastructure to pySim so that the encode and
decode methods for record-oriented files get passed in the current
record number.

Change-Id: I02d6942016dd0631b21d1fd301711c13cb27962b
Related: OS#5784
2023-01-24 20:03:02 +01:00
Harald Welte
8dbf714e96 gsm_r: Fix decoding of EF.FN
This fixes the below exception when trying to decode records of EF.FN:

EXCEPTION of type 'TypeError' occurred with message: 'unsupported operand type(s) for &: 'str' and 'int''

Change-Id: I3723a0d59f862fa818bea1622fe43a7b56c92847
Related: OS#5784
2023-01-24 14:37:18 +01:00
Harald Welte
e6d7b14f43 gsm_r: Fix typo (it's EF.FN, not EF.EN)
Related: OS#5784
Change-Id: I2c97a02973d2a1eda2cea5412391144726bb0525
2023-01-24 14:37:13 +01:00
Harald Welte
bc7437d3b6 pySim-trace: Also consider SW 91xx as successful
Change-Id: I9e4170721be30342bdce7fb4beeefd1927263ca6
2023-01-24 13:50:51 +01:00
Harald Welte
7489947046 pySim-trace: Fix missing MANAGE CHANNEL decode
old output:

00 MANAGE CHANNEL 01       9110

new output:

00 MANAGE CHANNEL 01       9110 {'mode': 'open_channel', 'created_channel': 1}

Change-Id: Iac5b24c14d2b68d526ab347462b72548b8731b30
2023-01-24 13:50:51 +01:00
Harald Welte
c95f6e2124 pySim-trace: Add support for reading GSMTAP from pcap files
So far we supported
* GSMTAP live traces via a UDP socket
* RSPRO traces from pcap files (or live)

We were lacking support for reading GSMTAP stored in pcap, which
is what this patch implements.

Change-Id: I46d42774b39a2735500ff5804206ddcfa545568c
2023-01-24 13:50:51 +01:00
Philipp Maier
284ac104af cards: also program EF.AD under ADF.USIM
DF.GSM and ADF.USIM have an EF.AD with nearly the same contents. Usually
there is one file physically present and the other is just a link.
Apparantly this is not always the case for sysmo-ismi-sja2 cards, so
lets program EF.AD in both locations.

Change-Id: Ic9dd4acc8d9a72acbb7376ddf3e2128125d4a8f5
Related: OS#5830
2023-01-19 10:32:13 +01:00
Philipp Maier
de0cf1648c cards: fix typo
Change-Id: I81a6074776bdf67b7bea359fe7a24f906936f46d
2023-01-03 13:30:02 +01:00
Philipp Maier
4237ccfb45 pySim-prog: add python docstring for read_params_csv
Change-Id: I098ff56ef38208f2f321194625ff4279ece2023c
2022-12-20 11:33:03 +01:00
Philipp Maier
5f0cb3c5f2 pySim-prog: rename write_parameters function.
The function name "write_parameters" is very generic and since it is
called during the programming cycle it should be made clear that it is
not about writing parameters to the card.

Change-Id: Idaba672987230d7d0dd500409f9fe0b94ba39370
2022-12-20 11:33:03 +01:00
Philipp Maier
cbb8c02d25 pySim-prog: make dry-run more realistic
The process_card function has a dry-run mode where one can test
parameters without actually writing to the card. However, the dry-run
feature also does not perform read operations and connects to the card
reader at a different point in time. Lets be more accurate here and
perform all operations a normal programming cycle would perform but
without calling the card.program() method.

Change-Id: I3653bada1ad26bcf75109a935105273ee9d1525c
2022-12-20 11:33:03 +01:00
Philipp Maier
0a8d9f05b8 cards: check length of mnc more restrictively
Since we now ensure that mnc always has a valid length lets make the
check in cards.py more strict.

Related: OS#5830
Change-Id: Iee8f25416e0cc3be96dff025affb1dc11d919fcd
2022-12-20 11:33:03 +01:00
Philipp Maier
32c0434540 pySim-prog: fix handling of mnclen parameter.
The handling of the mnclen parameter does not work. Lets fix it so that
it can be used again with CSV and normal card programming. Lets ensure
that depending on the parameter and the defaults it is always ensured
that the mnc string has the correct length so that lower layers can
deduct the length of the mnc properly by the string length of the mnc.

Change-Id: I48a109296efcd7a3f37c183ff53c5fc9544e3cfc
Related: OS#5830
2022-12-20 11:21:07 +01:00
Philipp Maier
2688ddf459 pySim-prog: clean up csv file reader function
The function that goes through the CSV file and searches for either IMSI
or ICCID or picks a specific line by number is very hard to read and
understand. Lets clean it up and add useful error messages

Change-Id: I7ae995aa3297e77b983e59c75e1c3ef17e1d7cd4
Related: OS#5830
2022-12-20 11:12:52 +01:00
Philipp Maier
4f888a0414 sysmocom_sja2: simplify and fix op/opc decoder/encoder
The decoder/encoder of that decodes the EF.xSIM_AUTH_KEY files has an
overcomplicated handling for op/and opc. There is a condition that
checks if milenage is configured and another one that checks if the
string is recognized as OP or OPc. Both is not correct and seems not to
work (op and opc is always displayed as "null")

The encoder/decoder should focus on the physical file layout and
regardless of any other conriguration the OP/OPc field is physically
present and should be displayd and presented for editing.

Change-Id: I6fa3a07e5e473273498d3f13d4cfa33743b787e1
2022-12-02 12:38:08 +01:00
Christian Amsüss
5d26311efc OTA: Adjust IV length for AES
Change-Id: I854c844418244c100c328f9e76c0f37850d3db00
2022-11-25 04:00:55 +01:00
Oliver Smith
8e45b75711 contrib/jenkins.sh: split test/pylint/docs
Split the jenkins job up in three parts, so each of them can run in
parallel, and the test part that has to run on a specific node (and
blocks it while running), finishes faster.

Don't install depends of pylint/docs jobs as they will run in docker
and the depends get installed once in the container.

Related: OS#5497
Depends: docker-playground Id5c75725d2fab46b29773fa4f637fa2d73fa7291
Depends: osmo-ci Iea4f15fd9c9f8f36cb8d638c48da000eafe746a4
Change-Id: I5245c529db729e209d78a02ab9c917a90d0e0206
2022-11-04 13:13:14 +01:00
Oliver Smith
0529c1906d docs: allow overriding OSMO_GSM_MANUALS_DIR
Related: OS#5497
Change-Id: I433217b7aa1cdcddc52a89721e03e44b417bacb1
2022-10-21 16:24:47 +02:00
Oliver Smith
507b5271ac contrib/jenkins.sh: set PYTHONUNBUFFERED=1
Make sure all python output is printed immediatelly.

Change-Id: I5d334bbc34e4df39ac54472642299c567894f449
2022-10-18 16:50:03 +02:00
Vadim Yanitskiy
4e64e72766 Revert "contrib/jenkins.sh: pylint v2.15 is unstable, pin v2.14.5"
This reverts commit 12175d3588.

The upstream has fixed the regression:

https://github.com/PyCQA/pylint/issues/7375

which was actually not in pylint itself but in its dependency:

https://github.com/PyCQA/astroid/pull/1763
https://github.com/PyCQA/astroid/releases/tag/v2.12.6

Change-Id: I1bf36e0c6db14a10ff4eab57bae238401dbd7fd0
Closes: OS#5668
2022-09-14 05:00:33 +00:00
Harald Welte
75a58d1a87 Add new pySim.ota library, implement SIM OTA crypto
This introduces a hierarchy of classes implementing

* ETS TS 102 225 (general command structure)
* 3GPP TS 31.115 (dialects for SMS-PP)

In this initial patch only the SMS "dialect" is supported,
but it is foreseen that USSD/SMSCB/HTTPS dialects can be
added at a later point.

Change-Id: I193ff4712c8503279c017b4b1324f0c3d38b9f84
2022-09-08 15:45:55 +02:00
220 changed files with 20640 additions and 4843 deletions

2
.checkpatch.conf Normal file
View File

@@ -0,0 +1,2 @@
--exclude ^pySim/esim/asn1/.*\.asn$
--exclude ^smdpp-data/.*$

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
*.pyc
.*.swp
/docs/_*
/docs/generated

111
README.md
View File

@@ -1,22 +1,61 @@
pySim - Read, Write and Browse Programmable SIM/USIM Cards
====================================================
pySim - Read, Write and Browse Programmable SIM/USIM/ISIM/HPSIM Cards
=====================================================================
This repository contains Python programs that can be used
to read, program (write) and browse certain fields/parameters on so-called programmable
SIM/USIM cards.
This repository contains a number of Python programs that can be used
to read, program (write) and browse all fields/parameters/files on
SIM/USIM/ISIM/HPSIM cards used in 3GPP cellular networks from 2G to 5G.
Such SIM/USIM cards are special cards, which - unlike those issued by
regular commercial operators - come with the kind of keys that allow you
to write the files/fields that normally only an operator can program.
Note that the access control configuration of normal production cards
issue by operators will restrict significantly which files a normal
user can read, and particularly write to.
The full functionality of pySim hence can only be used with on so-called
programmable SIM/USIM/ISIM/HPSIM cards.
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
issued by regular commercial operators - come with the kind of keys that
allow you to write the files/fields that normally only an operator can
program.
This is useful particularly if you are running your own cellular
network, and want to issue your own SIM/USIM cards for that network.
network, and want to configure your own SIM/USIM/ISIM/HPSIM cards for
that network.
Homepage and Manual
-------------------
Homepage
--------
Please visit the [official homepage](https://osmocom.org/projects/pysim/wiki)
for usage instructions, manual and examples.
Documentation
-------------
The pySim user manual can be built from this very source code by means
of sphinx (with sphinxcontrib-napoleon and sphinx-argparse). See the
Makefile in the 'docs' directory.
A pre-rendered HTML user manual of the current pySim 'git master' is
available from <https://downloads.osmocom.org/docs/latest/pysim/> and
a downloadable PDF version is published at
<https://downloads.osmocom.org/docs/latest/osmopysim-usermanual.pdf>.
A slightly dated video presentation about pySim-shell can be found at
<https://media.ccc.de/v/osmodevcall-20210409-laforge-pysim-shell>.
pySim-shell vs. legacy tools
----------------------------
While you will find a lot of online resources still describing the use of
pySim-prog.py and pySim-read.py, those tools are considered legacy by
now and have by far been superseded by the much more capable
pySim-shell. We strongly encourage users to adopt pySim-shell, unless
they have very specific requirements like batch programming of large
quantities of cards, which is about the only remaining use case for the
legacy tools.
Please visit the [official homepage](https://osmocom.org/projects/pysim/wiki) for usage instructions, manual and examples. The user manual can also be built locally from this source code by ``cd docs && make html latexpdf`` for HTML and PDF format, respectively.
Git Repository
--------------
@@ -34,17 +73,20 @@ Installation
Please install the following dependencies:
- bidict
- cmd2 >= 1.5.0
- colorlog
- construct >= 2.9.51
- gsm0338
- jsonpath-ng
- packaging
- pycryptodomex
- pyscard
- pyserial
- pytlv
- cmd2 >= 1.3.0 but < 2.0.0
- jsonpath-ng
- construct >= 2.9.51
- bidict
- gsm0338
- pyyaml >= 5.1
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
- termcolor
- colorlog
Example for Debian:
```sh
@@ -52,6 +94,7 @@ sudo apt-get install --no-install-recommends \
pcscd libpcsclite-dev \
python3 \
python3-setuptools \
python3-pycryptodome \
python3-pyscard \
python3-pip
pip3 install --user -r requirements.txt
@@ -59,6 +102,9 @@ pip3 install --user -r requirements.txt
After installing all dependencies, the pySim applications ``pySim-read.py``, ``pySim-prog.py`` and ``pySim-shell.py`` may be started directly from the cloned repository.
In addition to the dependencies above ``pySim-trace.py`` requires ``tshark`` and the python package ``pyshark`` to be installed. It is known that the ``tshark`` package
in Debian versions before 11 may not work with pyshark.
### Archlinux Package
Archlinux users may install the package ``python-pysim-git``
@@ -99,32 +145,3 @@ Our coding standards are described at
We are using a gerrit-based patch review process explained at
<https://osmocom.org/projects/cellular-infrastructure/wiki/Gerrit>
Documentation
-------------
The pySim user manual can be built from this very source code by means
of sphinx (with sphinxcontrib-napoleon and sphinx-argparse). See the
Makefile in the 'docs' directory.
A pre-rendered HTML user manual of the current pySim 'git master' is
available from <https://downloads.osmocom.org/docs/latest/pysim/> and
a downloadable PDF version is published at
<https://downloads.osmocom.org/docs/latest/osmopysim-usermanual.pdf>.
A slightly dated video presentation about pySim-shell can be found at
<https://media.ccc.de/v/osmodevcall-20210409-laforge-pysim-shell>.
pySim-shell vs. legacy tools
----------------------------
While you will find a lot of online resources still describing the use of
pySim-prog.py and pySim-read.py, those tools are considered legacy by
now and have by far been superseded by the much more capable
pySim-shell. We strongly encourage users to adopt pySim-shell, unless
they have very specific requirements like batch programming of large
quantities of cards, which is about the only remaining use case for the
legacy tools.

73
contrib/eidtool.py Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# Command line tool to compute or verify EID (eUICC ID) values
#
# (C) 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 sys
import argparse
from pySim.euicc import compute_eid_checksum, verify_eid_checksum
option_parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
description="""pySim EID Tool
This utility program can be used to compute or verify the checksum of an EID
(eUICC Identifier). See GSMA SGP.29 for the algorithm details.
Example (verification):
$ eidtool.py --verify 89882119900000000000000000001654
EID checksum verified successfully
Example (generation, passing first 30 digits):
$ eidtool.py --compute 898821199000000000000000000016
89882119900000000000000000001654
Example (generation, passing all 32 digits):
$ eidtool.py --compute 89882119900000000000000000001600
89882119900000000000000000001654
Example (generation, specifying base 30 digits and number to add):
$ eidtool.py --compute 898821199000000000000000000000 --add 16
89882119900000000000000000001654
""")
group = option_parser.add_mutually_exclusive_group(required=True)
group.add_argument('--verify', help='Verify given EID csum')
group.add_argument('--compute', help='Generate EID csum')
option_parser.add_argument('--add', type=int, help='Add value to EID base before computing')
if __name__ == '__main__':
opts = option_parser.parse_args()
if opts.verify:
res = verify_eid_checksum(opts.verify)
if res:
print("EID checksum verified successfully")
sys.exit(0)
else:
print("EID checksum invalid")
sys.exit(1)
elif opts.compute:
eid = opts.compute
if opts.add:
if len(eid) != 30:
print("EID base must be 30 digits when using --add")
sys.exit(2)
eid = str(int(eid) + int(opts.add))
res = compute_eid_checksum(eid)
print(res)

79
contrib/es2p_client.py Executable file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
# (C) 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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
import argparse
from pySim.esim import es2p
EID_HELP='EID of the eUICC for which eSIM shall be made available'
ICCID_HELP='The ICCID of the eSIM that shall be made available'
MATCHID_HELP='MatchingID that shall be used by profile download'
parser = argparse.ArgumentParser(description="""
Utility to manuall issue requests against the ES2+ API of an SM-DP+ according to GSMA SGP.22.""")
parser.add_argument('--url', required=True, help='Base URL of ES2+ API endpoint')
parser.add_argument('--id', required=True, help='Entity identifier passed to SM-DP+')
parser.add_argument('--client-cert', help='X.509 client certificate used to authenticate to server')
parser.add_argument('--server-ca-cert', help="""X.509 CA certificates acceptable for the server side. In
production use cases, this would be the GSMA Root CA (CI) certificate.""")
subparsers = parser.add_subparsers(dest='command',help="The command (API function) to call")
parser_dlo = subparsers.add_parser('download-order', help="ES2+ DownloadOrder function")
parser_dlo.add_argument('--eid', help=EID_HELP)
parser_dlo.add_argument('--iccid', help=ICCID_HELP)
parser_dlo.add_argument('--profileType', help='The profile type of which one eSIM shall be made available')
parser_cfo = subparsers.add_parser('confirm-order', help="ES2+ ConfirmOrder function")
parser_cfo.add_argument('--iccid', required=True, help=ICCID_HELP)
parser_cfo.add_argument('--eid', help=EID_HELP)
parser_cfo.add_argument('--matchingId', help=MATCHID_HELP)
parser_cfo.add_argument('--confirmationCode', help='Confirmation code that shall be used by profile download')
parser_cfo.add_argument('--smdsAddress', help='SM-DS Address')
parser_cfo.add_argument('--releaseFlag', action='store_true', help='Shall the profile be immediately released?')
parser_co = subparsers.add_parser('cancel-order', help="ES2+ CancelOrder function")
parser_co.add_argument('--iccid', required=True, help=ICCID_HELP)
parser_co.add_argument('--eid', help=EID_HELP)
parser_co.add_argument('--matchingId', help=MATCHID_HELP)
parser_co.add_argument('--finalProfileStatusIndicator', required=True, choices=['Available','Unavailable'])
parser_rp = subparsers.add_parser('release-profile', help='ES2+ ReleaseProfile function')
parser_rp.add_argument('--iccid', required=True, help=ICCID_HELP)
if __name__ == '__main__':
opts = parser.parse_args()
#print(opts)
peer = es2p.Es2pApiClient(opts.url, opts.id, server_cert_verify=opts.server_ca_cert, client_cert=opts.client_cert)
data = {}
for k, v in vars(opts).items():
if k in ['url', 'id', 'client_cert', 'server_ca_cert', 'command']:
# remove keys from dict that shold not end up in JSON...
continue
if v is not None:
data[k] = v
print(data)
if opts.command == 'download-order':
res = peer.call_downloadOrder(data)
elif opts.command == 'confirm-order':
res = peer.call_confirmOrder(data)
elif opts.command == 'cancel-order':
res = peer.call_cancelOrder(data)
elif opts.command == 'release-profile':
res = peer.call_releaseProfile(data)

View File

@@ -4,8 +4,11 @@
# environment variables:
# * WITH_MANUALS: build manual PDFs if set to "1"
# * PUBLISH: upload manuals after building if set to "1" (ignored without WITH_MANUALS = "1")
# * JOB_TYPE: one of 'test', 'pylint', 'docs'
#
export PYTHONUNBUFFERED=1
if [ ! -d "./pysim-testdata/" ] ; then
echo "###############################################"
echo "Please call from pySim-prog top directory"
@@ -13,36 +16,48 @@ if [ ! -d "./pysim-testdata/" ] ; then
exit 1
fi
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt
case "$JOB_TYPE" in
"test")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/
pip install -r requirements.txt
pip install pyshark
# Run pylint to find potential errors
# Ignore E1102: not-callable
# pySim/filesystem.py: E1102: method is not callable (not-callable)
# Ignore E0401: import-error
# pySim/utils.py:276: E0401: Unable to import 'Crypto.Cipher' (import-error)
# pySim/utils.py:277: E0401: Unable to import 'Crypto.Util.strxor' (import-error)
pip install pylint==2.14.5 # FIXME: 2.15 is crashing, see OS#5668
python -m pylint -j0 --errors-only \
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim *.py
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/
# attempt to build documentation
pip install sphinx
pip install sphinxcontrib-napoleon
pip3 install -e 'git+https://github.com/osmocom/sphinx-argparse@master#egg=sphinx-argparse'
(cd docs && make html latexpdf)
# Run the test with physical cards
cd pysim-testdata
../tests/pySim-prog_test.sh
../tests/pySim-trace_test.sh
;;
"pylint")
# Print pylint version
pip3 freeze | grep pylint
# Run pylint to find potential errors
# Ignore E1102: not-callable
# pySim/filesystem.py: E1102: method is not callable (not-callable)
# Ignore E0401: import-error
# pySim/utils.py:276: E0401: Unable to import 'Crypto.Cipher' (import-error)
# pySim/utils.py:277: E0401: Unable to import 'Crypto.Util.strxor' (import-error)
python3 -m pylint -j0 --errors-only \
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim tests/*.py *.py \
contrib/es2p_client.py
;;
"docs")
rm -rf docs/_build
make -C "docs" html latexpdf
if [ "$WITH_MANUALS" = "1" ] && [ "$PUBLISH" = "1" ]; then
make -C "docs" publish publish-html
fi
# run the test with physical cards
cd pysim-testdata
../tests/pysim-test.sh
if [ "$WITH_MANUALS" = "1" ] && [ "$PUBLISH" = "1" ]; then
make -C "docs" publish publish-html
fi
;;
*)
set +x
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
exit 1
esac

View File

@@ -26,7 +26,10 @@ from klein import Klein
from pySim.transport import ApduTracer
from pySim.transport.pcsc import PcscSimLink
from pySim.commands import SimCardCommands
from pySim.cards import UsimCard
from pySim.cards import UiccCardBase
from pySim.utils import dec_iccid, dec_imsi
from pySim.ts_51_011 import EF_IMSI
from pySim.ts_102_221 import EF_ICCID
from pySim.exceptions import *
class ApduPrintTracer(ApduTracer):
@@ -35,18 +38,20 @@ class ApduPrintTracer(ApduTracer):
pass
def connect_to_card(slot_nr:int):
tp = PcscSimLink(slot_nr, apdu_tracer=ApduPrintTracer())
tp = PcscSimLink(argparse.Namespace(pcsc_dev=slot_nr), apdu_tracer=ApduPrintTracer())
tp.connect()
scc = SimCardCommands(tp)
card = UsimCard(scc)
card = UiccCardBase(scc)
# this should be part of UsimCard, but FairewavesSIM breaks with that :/
scc.cla_byte = "00"
scc.sel_ctrl = "0004"
card.read_aids()
card.select_adf_by_aid(adf='usim')
# ensure that MF is selected when we are done.
card._scc.select_file('3f00')
return tp, scc, card
@@ -96,7 +101,7 @@ class SimRestServer:
elif sw == '6982':
return str(ApiError("Security Status not satisfied - Card PIN enabled?", sw))
else:
return str(ApiError("Card Communication Error %s" % failure.value), sw)
return str(ApiError("Card Communication Error %s" % failure.value, sw))
@app.route('/sim-auth-api/v1/slot/<int:slot>')
@@ -132,10 +137,14 @@ class SimRestServer:
tp, scc, card = connect_to_card(slot)
ef_iccid = EF_ICCID()
(iccid, sw) = card._scc.read_binary(ef_iccid.fid)
card.select_adf_by_aid(adf='usim')
iccid, sw = card.read_iccid()
imsi, sw = card.read_imsi()
res = {"imsi": imsi, "iccid": iccid }
ef_imsi = EF_IMSI()
(imsi, sw) = card._scc.read_binary(ef_imsi.fid)
res = {"imsi": dec_imsi(imsi), "iccid": dec_iccid(iccid) }
tp.disconnect()

39
contrib/unber.py Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
# A more useful verion of the 'unber' tool provided with asn1c:
# Give a hierarchical decode of BER/DER-encoded ASN.1 TLVs
import sys
import argparse
from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
def process_one_level(content: bytes, indent: int):
remainder = content
while len(remainder):
tdict, l, v, remainder = bertlv_parse_one(remainder)
#print(tdict)
rawtag = bertlv_encode_tag(tdict)
if tdict['constructed']:
print("%s%s l=%d" % (indent*" ", b2h(rawtag), l))
process_one_level(v, indent + 1)
else:
print("%s%s l=%d %s" % (indent*" ", b2h(rawtag), l, b2h(v)))
option_parser = argparse.ArgumentParser(description='BER/DER data dumper')
group = option_parser.add_mutually_exclusive_group(required=True)
group.add_argument('--file', help='Input file')
group.add_argument('--hex', help='Input hexstring')
if __name__ == '__main__':
opts = option_parser.parse_args()
if opts.file:
with open(opts.file, 'rb') as f:
content = f.read()
elif opts.hex:
content = h2b(opts.hex)
process_one_level(content, 0)

View File

@@ -9,11 +9,17 @@ SOURCEDIR = .
BUILDDIR = _build
# for osmo-gsm-manuals
OSMO_GSM_MANUALS_DIR=$(shell pkg-config osmo-gsm-manuals --variable=osmogsmmanualsdir 2>/dev/null)
OSMO_GSM_MANUALS_DIR ?= $(shell pkg-config osmo-gsm-manuals --variable=osmogsmmanualsdir 2>/dev/null)
OSMO_REPOSITORY = "pysim"
UPLOAD_FILES = $(BUILDDIR)/latex/osmopysim-usermanual.pdf
CLEAN_FILES = $(UPLOAD_FILES)
# Copy variables from Makefile.common.inc that are used in publish-html,
# as Makefile.common.inc must be included after publish-html
PUBLISH_REF ?= master
PUBLISH_TEMPDIR = _publish_tmpdir
SSH_COMMAND = ssh -o 'UserKnownHostsFile=$(OSMO_GSM_MANUALS_DIR)/build/known_hosts' -p 48
# Put it first so that "make" without argument is like "make help".
.PHONY: help
help:
@@ -23,7 +29,16 @@ $(BUILDDIR)/latex/pysim.pdf: latexpdf
@/bin/true
publish-html: html
rsync -avz -e "ssh -o 'UserKnownHostsFile=$(OSMO_GSM_MANUALS_DIR)/build/known_hosts' -p 48" $(BUILDDIR)/html/ docs@ftp.osmocom.org:web-files/latest/pysim/
rm -rf "$(PUBLISH_TEMPDIR)"
mkdir -p "$(PUBLISH_TEMPDIR)/pysim/$(PUBLISH_REF)"
cp -r "$(BUILDDIR)"/html "$(PUBLISH_TEMPDIR)/pysim/$(PUBLISH_REF)"
cd "$(PUBLISH_TEMPDIR)" && \
rsync \
-avzR \
-e "$(SSH_COMMAND)" \
"pysim" \
docs@ftp.osmocom.org:web-files/
rm -rf "$(PUBLISH_TEMPDIR)"
# put this before the catch-all below
include $(OSMO_GSM_MANUALS_DIR)/build/Makefile.common.inc
@@ -32,4 +47,6 @@ include $(OSMO_GSM_MANUALS_DIR)/build/Makefile.common.inc
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%:
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@if [ "$@" != "shrink" ]; then \
$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O); \
fi

View File

@@ -18,8 +18,8 @@ sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = 'osmopysim-usermanual'
copyright = '2009-2022 by Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle'
author = 'Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle'
copyright = '2009-2023 by Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle, Merlin Chlosta'
author = 'Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle, Merlin Chlosta'
# -- General configuration ---------------------------------------------------

View File

@@ -31,6 +31,7 @@ pySim consists of several parts:
* a python :ref:`library<pySim library>` containing plenty of objects and methods that can be used for
writing custom programs interfacing with SIM cards.
* the [new] :ref:`interactive pySim-shell command line program<pySim-shell>`
* the [new] :ref:`pySim-trace APDU trace decoder<pySim-trace>`
* the [legacy] :ref:`pySim-prog and pySim-read tools<Legacy tools>`
.. toctree::
@@ -38,8 +39,10 @@ pySim consists of several parts:
:caption: Contents:
shell
trace
legacy
library
osmo-smdpp
Indices and tables

View File

@@ -98,3 +98,4 @@ pySim-read usage
.. argparse::
:module: pySim-read
:func: option_parser
:prog: pySim-read.py

114
docs/osmo-smdpp.rst Normal file
View File

@@ -0,0 +1,114 @@
osmo-smdpp
==========
`osmo-smdpp` is a proof-of-concept implementation of a minimal **SM-DP+** as specified for the *GSMA
Consumer eSIM Remote SIM provisioning*.
At least at this point, it is intended to be used for research and development, and not as a
production SM-DP+.
Unless you are a GSMA SAS-SM accredited SM-DP+ operator and have related DPtls, DPauth and DPpb
certificates signed by the GSMA CI, you **can not use osmo-smdpp with regular production eUICC**.
This is due to how the GSMA eSIM security architecture works. You can, however, use osmo-smdpp with
so-called *test-eUICC*, which contain certificates/keys signed by GSMA test certificates as laid out
in GSMA SGP.26.
At this point, osmo-smdpp does not support anything beyond the bare minimum required to download
eSIM profiles to an eUICC. Specifically, there is no ES2+ interface, and there is no built-in
support for profile personalization yet.
osmo-smdpp currently
* uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your osmo-smdppp
would be running at the host name `testsmdpplus1.example.com`
* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
of the EID or whether it was donwloaded before
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical
* **is absolutely insecure**, as it
* does not perform any certificate verification
* does not evaluate/consider any *Matching ID* or *Confirmation Code*
* stores the sessions in an unencrypted _python shelve_ and is hence leaking one-time key materials
used for profile encryption and signing.
Running osmo-smdpp
------------------
osmo-smdpp does not have built-in TLS support as the used *twisted* framework appears to have
problems when using the example elliptic curve certificates (both NIST and Brainpool) from GSMA.
So in order to use it, you have to put it behind a TLS reverse proxy, which terminates the ES9+
HTTPS from the LPA, and then forwards it as plain HTTP to osmo-smdpp.
nginx as TLS proxy
~~~~~~~~~~~~~~~~~~
If you use `nginx` as web server, you can use the following configuration snippet::
upstream smdpp {
server localhost:8000;
}
server {
listen 443 ssl;
server_name testsmdpplus1.example.com;
ssl_certificate /my/path/to/pysim/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem;
ssl_certificate_key /my/path/to/pysim/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_NIST.pem;
location / {
proxy_read_timeout 600s;
proxy_hide_header X-Powered-By;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port $proxy_port;
proxy_set_header Host $host;
proxy_pass http://smdpp/;
}
}
You can of course achieve a similar functionality with apache, lighttpd or many other web server
software.
osmo-smdpp
~~~~~~~~~~
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
used; they are copied from GSMA SGP.26 v2.
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
DNS setup for your LPA
~~~~~~~~~~~~~~~~~~~~~~
The LPA must resolve `testsmdpplus1.example.com` to the IP address of your TLS proxy.
It must also accept the TLS certificates used by your TLS proxy.
Supported eUICC
~~~~~~~~~~~~~~~
If you run osmo-smdpp with the included SGP.26 certificates, you must use an eUICC with matching SGP.26
certificates, i.e. the EUM certificate must be signed by a SGP.26 test root CA and the eUICC certificate
in turn must be signed by that SGP.26 EUM certificate.
sysmocom (sponsoring development and maintenance of pySim and osmo-smdpp) is selling SGP.26 test eUICC
as `sysmoEUICC1-C2T`. They are publicly sold in the `sysmocom webshop <https://shop.sysmocom.de/eUICC-for-consumer-eSIM-RSP-with-SGP.26-Test-Certificates/sysmoEUICC1-C2T>`_.
In general you can use osmo-smdpp also with certificates signed by any other certificate authority. You
just always must ensure that the certificates of the SM-DP+ are signed by the same root CA as those of your
eUICCs.
Hypothetically, osmo-smdpp could also be operated with GSMA production certificates, but it would require
that somebody brings the code in-line with all the GSMA security requirements (HSM support, ...) and operate
it in a GSMA SAS-SM accredited environment and pays for the related audits.

File diff suppressed because it is too large Load Diff

195
docs/suci-tutorial.rst Normal file
View File

@@ -0,0 +1,195 @@
Guide: Enabling 5G SUCI
========================
SUPI/SUCI Concealment is a feature of 5G-Standalone (SA) to encrypt the
IMSI/SUPI with a network operator public key. 3GPP Specifies two different
variants for this:
* SUCI calculation *in the UE*, using data from the SIM
* SUCI calculation *on the card itself*
pySIM supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming that
your cards contain the required files, and you have the privileges/credentials to write to them. This is
the case using sysmocom sysmoISIM-SJA2 cards (or successor products).
In short, you can enable SUCI with these steps:
* activate USIM **Service 124**
* make sure USIM **Service 125** is disabled
* store the public keys in **SUCI_Calc_Info**
* set the **Routing Indicator** (required)
If you want to disable the feature, you can just disable USIM Service 124 (and 125).
Technical References
~~~~~~~~~~~~~~~~~~~~
This guide covers the basic workflow of provisioning SIM cards with the 5G SUCI feature. For detailed information on the SUCI feature and file contents, the following documents are helpful:
* USIM files and structure: `TS 31.102 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131102/16.06.00_60/ts_131102v160600p.pdf>`__
* USIM tests (incl. file content examples) `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
For specific information on sysmocom SIM cards, refer to Section 9.1 of the `sysmoUSIM User
Manual <https://www.sysmocom.de/manuals/sysmousim-manual.pdf>`__.
--------------
Admin PIN
---------
The usual way to authenticate yourself to the card as the cellular
operator is to validate the so-called ADM1 (admin) PIN. This may differ
from card model/vendor to card model/vendor.
Start pySIM-shell and enter the admin PIN for your card. If you bought
the SIM card from your network operator and dont have the admin PIN,
you cannot change SIM contents!
Launch pySIM:
::
$ ./pySim-shell.py -p 0
Using PC/SC reader interface
Autodetected card type: sysmoISIM-SJA2
Welcome to pySim-shell!
pySIM-shell (00:MF)>
Enter the ADM PIN:
::
pySIM-shell (00:MF)> verify_adm XXXXXXXX
Otherwise, write commands will fail with ``SW Mismatch: Expected 9000 and got 6982.``
Key Provisioning
----------------
::
pySIM-shell (00:MF)> select MF
pySIM-shell (00:MF)> select ADF.USIM
pySIM-shell (00:MF/ADF.USIM)> select DF.5GS
pySIM-shell (00:MF/ADF.USIM/DF.5GS)> select EF.SUCI_Calc_Info
By default, the file is present but empty:
::
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> read_binary_decoded
missing Protection Scheme Identifier List data object tag
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
The following JSON config defines the testfile from `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__ Section 4.9.4 with
test keys from `TS 33.501 <hhttps://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__ Annex C.4. Highest priority (``0``) has a
Profile-B (``identifier: 2``) key in key slot ``1``, which means the key
with ``hnet_pubkey_identifier: 27``.
.. code:: json
{
"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": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"},
{"hnet_pubkey_identifier": 30,
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
}
Write the config to file (must be single-line input as for now):
::
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "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": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
WARNING: These are TEST KEYS with publicly known/specified private keys, and hence unsafe for live/secure
deployments! For use in production networks, you need to generate your own set[s] of keys.
Routing Indicator
-----------------
The Routing Indicator must be present for the SUCI feature. By default,
the contents of the file is **invalid** (ffffffff):
::
pySIM-shell (00:MF)> select MF
pySIM-shell (00:MF)> select ADF.USIM
pySIM-shell (00:MF/ADF.USIM)> select DF.5GS
pySIM-shell (00:MF/ADF.USIM/DF.5GS)> select EF.Routing_Indicator
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.Routing_Indicator)> read_binary_decoded
9000: ffffffff -> {'raw': 'ffffffff'}
The Routing Indicator is a four-byte file but the actual Routing
Indicator goes into bytes 0 and 1 (the other bytes are reserved). To set
the Routing Indicator to 0x71:
::
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.Routing_Indicator)> update_binary 17ffffff
You can also set the routing indicator to **0x0**, which is *valid* and
means “routing indicator not specified”, leaving it to the modem.
USIM Service Table
------------------
First, check out the USIM Service Table (UST):
::
pySIM-shell (00:MF)> select MF
pySIM-shell (00:MF)> select ADF.USIM
pySIM-shell (00:MF/ADF.USIM)> select EF.UST
pySIM-shell (00:MF/ADF.USIM/EF.UST)> read_binary_decoded
9000: beff9f9de73e0408400170730000002e00000000 -> [2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 25, 27, 28, 29, 33, 34, 35, 38, 39, 42, 43, 44, 45, 46, 51, 60, 71, 73, 85, 86, 87, 89, 90, 93, 94, 95, 122, 123, 124, 126]
.. list-table:: From TS31.102
:widths: 15 40
:header-rows: 1
* - Service No.
- Description
* - 122
- 5GS Mobility Management Information
* - 123
- 5G Security Parameters
* - 124
- Subscription identifier privacy support
* - 125
- SUCI calculation by the USIM
* - 126
- UAC Access Identities support
* - 129
- 5GS Operator PLMN List
If youd like to enable/disable any UST service:
::
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_deactivate 124
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 124
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_deactivate 125
In this case, UST Service 124 is already enabled and youre good to go. The
sysmoISIM-SJA2 does not support on-SIM calculation, so service 125 must
be disabled.
USIM Error with 5G and sysmoISIM
--------------------------------
sysmoISIMs come 5GS-enabled. By default however, the configuration stored
in the card file-system is **not valid** for 5G networks: Service 124 is enabled,
but EF.SUCI_Calc_Info and EF.Routing_Indicator are empty files (hence
do not contain valid data).
At least for Qualcomms X55 modem, this results in an USIM error and the
whole modem shutting 5G down. If you dont need SUCI concealment but the
smartphone refuses to connect to any 5G network, try to disable the UST
service 124.

64
docs/trace.rst Normal file
View File

@@ -0,0 +1,64 @@
pySim-trace
===========
pySim-trace is a utility for high-level decode of APDU protocol traces such as those obtained with
`Osmocom SIMtrace2 <https://osmocom.org/projects/simtrace2/wiki>`_ or `osmo-qcdiag <https://osmocom.org/projects/osmo-qcdiag/wiki>`_.
pySim-trace leverages the existing knowledge of pySim-shell on anything related to SIM cards,
including the structure/encoding of the various files on SIM/USIM/ISIM/HPSIM cards, and applies this
to decoding protocol traces. This means that it shows not only the name of the command (like READ
BINARY), but actually understands what the currently selected file is, and how to decode the
contents of that file.
pySim-trace also understands the parameters passed to commands and how to decode them, for example
of the AUTHENTICATE command within the USIM/ISIM/HPSIM application.
Demo
----
To get an idea how pySim-trace usage looks like, you can watch the relevant part of the 11/2022
SIMtrace2 tutorial whose `recording is freely accessible <https://media.ccc.de/v/osmodevcall-20221019-laforge-simtrace2-tutorial#t=2134>`_.
Running pySim-trace
-------------------
Running pySim-trace requires you to specify the *source* of the to-be-decoded APDUs. There are several
supported options, each with their own respective parameters (like a file name for PCAP decoding).
See the detailed command line reference below for details.
A typical execution of pySim-trace for doing live decodes of *GSMTAP (SIM APDU)* e.g. from SIMtrace2 or
osmo-qcdiag would look like this:
::
./pySim-trace.py gsmtap-udp
This binds to the default UDP port 4729 (GSMTAP) on localhost (127.0.0.1), and decodes any APDUs received
there.
pySim-trace command line reference
----------------------------------
.. argparse::
:module: pySim-trace
:func: option_parser
:prog: pySim-trace.py
Constraints
-----------
* In order to properly track the current location in the filesystem tree and other state, it is
important that the trace you're decoding includes all of the communication with the SIM, ideally
from the very start (power up).
* pySim-trace currently only supports ETSI UICC (USIM/ISIM/HPSIM) and doesn't yet support legacy GSM
SIM. This is not a fundamental technical constraint, it's just simply that nobody got around
developing and testing that part. Contributions are most welcome.

566
osmo-smdpp.py Executable file
View File

@@ -0,0 +1,566 @@
#!/usr/bin/env python3
# Early proof-of-concept towards a SM-DP+ HTTP service for GSMA consumer eSIM RSP
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import sys
import argparse
import uuid
import os
import functools
from typing import Optional, Dict, List
from pprint import pprint as pp
import base64
from base64 import b64decode
from klein import Klein
from twisted.web.iweb import IRequest
import asn1tools
from pySim.utils import h2b, b2h, swap_nibbles
import pySim.esim.rsp as rsp
from pySim.esim import saip
from pySim.esim.es8p import *
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
# HACK: make this configurable
DATA_DIR = './smdpp-data'
HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
def b64encode2str(req: bytes) -> str:
"""Encode given input bytes as base64 and return result as string."""
return base64.b64encode(req).decode('ascii')
def set_headers(request: IRequest):
"""Set the request headers as mandatory by GSMA eSIM RSP."""
request.setHeader('Content-Type', 'application/json;charset=UTF-8')
request.setHeader('X-Admin-Protocol', 'gsma/rsp/v2.1.0')
def build_status_code(subject_code: str, reason_code: str, subject_id: Optional[str], message: Optional[str]) -> Dict:
r = {'subjectCode': subject_code, 'reasonCode': reason_code }
if subject_id:
r['subjectIdentifier'] = subject_id
if message:
r['message'] = message
return r
def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_data = None) -> None:
# SGP.22 v3.0 6.5.1.4
js['header'] = {
'functionExecutionStatus': {
'status': status,
}
}
if status_code_data:
js['header']['functionExecutionStatus']['statusCodeData'] = status_code_data
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
from cryptography import x509
def ecdsa_tr03111_to_dss(sig: bytes) -> bytes:
"""convert an ECDSA signature from BSI TR-03111 format to DER: first get long integers; then encode those."""
assert len(sig) == 64
r = int.from_bytes(sig[0:32], 'big')
s = int.from_bytes(sig[32:32*2], 'big')
return encode_dss_signature(r, s)
class ApiError(Exception):
def __init__(self, subject_code: str, reason_code: str, message: Optional[str] = None,
subject_id: Optional[str] = None):
self.status_code = build_status_code(subject_code, reason_code, subject_id, message)
def encode(self) -> str:
"""Encode the API Error into a responseHeader string."""
js = {}
build_resp_header(js, 'Failed', self.status_code)
return json.dumps(js)
class SmDppHttpServer:
app = Klein()
@staticmethod
def load_certs_from_path(path: str) -> List[x509.Certificate]:
"""Load all DER + PEM files from given directory path and return them as list of x509.Certificate
instances."""
certs = []
for dirpath, dirnames, filenames in os.walk(path):
for filename in filenames:
cert = None
if filename.endswith('.der'):
with open(os.path.join(dirpath, filename), 'rb') as f:
cert = x509.load_der_x509_certificate(f.read())
elif filename.endswith('.pem'):
with open(os.path.join(dirpath, filename), 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read())
if cert:
# verify it is a CI certificate (keyCertSign + i-rspRole-ci)
if not cert_policy_has_oid(cert, oid.id_rspRole_ci):
raise ValueError("alleged CI certificate %s doesn't have CI policy" % filename)
certs.append(cert)
return certs
def ci_get_cert_for_pkid(self, ci_pkid: bytes) -> Optional[x509.Certificate]:
"""Find CI certificate for given key identifier."""
for cert in self.ci_certs:
print("cert: %s" % cert)
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), cert.extensions))
print(subject_exts)
subject_pkid = subject_exts[0].value
print(subject_pkid)
if subject_pkid and subject_pkid.key_identifier == ci_pkid:
return cert
return None
def __init__(self, server_hostname: str, ci_certs_path: str, use_brainpool: bool = False):
self.server_hostname = server_hostname
self.upp_dir = os.path.realpath(os.path.join(DATA_DIR, 'upp'))
self.ci_certs = self.load_certs_from_path(ci_certs_path)
# load DPauth cert + key
self.dp_auth = CertAndPrivkey(oid.id_rspRole_dp_auth_v2)
cert_dir = os.path.join(DATA_DIR, 'certs')
if use_brainpool:
self.dp_auth.cert_from_der_file(os.path.join(cert_dir, 'DPauth', 'CERT_S_SM_DPauth_ECDSA_BRP.der'))
self.dp_auth.privkey_from_pem_file(os.path.join(cert_dir, 'DPauth', 'SK_S_SM_DPauth_ECDSA_BRP.pem'))
else:
self.dp_auth.cert_from_der_file(os.path.join(cert_dir, 'DPauth', 'CERT_S_SM_DPauth_ECDSA_NIST.der'))
self.dp_auth.privkey_from_pem_file(os.path.join(cert_dir, 'DPauth', 'SK_S_SM_DPauth_ECDSA_NIST.pem'))
# load DPpb cert + key
self.dp_pb = CertAndPrivkey(oid.id_rspRole_dp_pb_v2)
if use_brainpool:
self.dp_pb.cert_from_der_file(os.path.join(cert_dir, 'DPpb', 'CERT_S_SM_DPpb_ECDSA_BRP.der'))
self.dp_pb.privkey_from_pem_file(os.path.join(cert_dir, 'DPpb', 'SK_S_SM_DPpb_ECDSA_BRP.pem'))
else:
self.dp_pb.cert_from_der_file(os.path.join(cert_dir, 'DPpb', 'CERT_S_SM_DPpb_ECDSA_NIST.der'))
self.dp_pb.privkey_from_pem_file(os.path.join(cert_dir, 'DPpb', 'SK_S_SM_DPpb_ECDSA_NIST.pem'))
self.rss = rsp.RspSessionStore(os.path.join(DATA_DIR, "sm-dp-sessions"))
@app.handle_errors(ApiError)
def handle_apierror(self, request: IRequest, failure):
request.setResponseCode(200)
pp(failure)
return failure.value.encode()
@staticmethod
def _ecdsa_verify(cert: x509.Certificate, signature: bytes, data: bytes) -> bool:
pubkey = cert.public_key()
dss_sig = ecdsa_tr03111_to_dss(signature)
try:
pubkey.verify(dss_sig, data, ec.ECDSA(hashes.SHA256()))
return True
except InvalidSignature:
return False
@staticmethod
def rsp_api_wrapper(func):
"""Wrapper that can be used as decorator in order to perform common REST API endpoint entry/exit
functionality, such as JSON decoding/encoding and debug-printing."""
@functools.wraps(func)
def _api_wrapper(self, request: IRequest):
# TODO: evaluate User-Agent + X-Admin-Protocol header
# TODO: reject any non-JSON Content-type
content = json.loads(request.content.read())
print("Rx JSON: %s" % json.dumps(content))
set_headers(request)
output = func(self, request, content) or {}
build_resp_header(output)
print("Tx JSON: %s" % json.dumps(output))
return json.dumps(output)
return _api_wrapper
@app.route('/gsma/rsp2/es9plus/initiateAuthentication', methods=['POST'])
@rsp_api_wrapper
def initiateAutentication(self, request: IRequest, content: dict) -> dict:
"""See ES9+ InitiateAuthentication SGP.22 Section 5.6.1"""
# Verify that the received address matches its own SM-DP+ address, where the comparison SHALL be
# case-insensitive. Otherwise, the SM-DP+ SHALL return a status code "SM-DP+ Address - Refused".
if content['smdpAddress'] != self.server_hostname:
raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
euiccChallenge = b64decode(content['euiccChallenge'])
if len(euiccChallenge) != 16:
raise ValueError
euiccInfo1_bin = b64decode(content['euiccInfo1'])
euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
print("Rx euiccInfo1: %s" % euiccInfo1)
#euiccInfo1['svn']
# TODO: If euiccCiPKIdListForSigningV3 is present ...
pkid_list = euiccInfo1['euiccCiPKIdListForSigning']
if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
# verify it supports one of the keys indicated by euiccCiPKIdListForSigning
ci_cert = None
for x in pkid_list:
ci_cert = self.ci_get_cert_for_pkid(x)
# we already support multiple CI certificates but only one set of DPauth + DPpb keys. So we must
# make sure we choose a CI key-id which has issued both the eUICC as well as our own SM-DP side
# certs.
if ci_cert and cert_get_subject_key_id(ci_cert) == self.dp_auth.get_authority_key_identifier().key_identifier:
break
else:
ci_cert = None
if not ci_cert:
raise ApiError('8.8.2', '3.1', 'None of the proposed Public Key Identifiers is supported by the SM-DP+')
# TODO: Determine the set of CERT.DPauth.SIG that satisfy the following criteria:
# * Part of a certificate chain ending at one of the eSIM CA RootCA Certificate, whose Public Keys is
# supported by the eUICC (indicated by euiccCiPKIdListForVerification).
# * Using a certificate chain that the eUICC and the LPA both support:
#euiccInfo1['euiccCiPKIdListForVerification']
# raise ApiError('8.8.4', '3.7', 'The SM-DP+ has no CERT.DPauth.SIG which chains to one of the eSIM CA Root CA CErtificate with a Public Key supported by the eUICC')
# Generate a TransactionID which is used to identify the ongoing RSP session. The TransactionID
# SHALL be unique within the scope and lifetime of each SM-DP+.
transactionId = uuid.uuid4().hex
assert not transactionId in self.rss
# Generate a serverChallenge for eUICC authentication attached to the ongoing RSP session.
serverChallenge = os.urandom(16)
# Generate a serverSigned1 data object as expected by the eUICC and described in section 5.7.13 "ES10b.AuthenticateServer". If and only if both eUICC and LPA indicate crlStaplingV3Support, the SM-DP+ SHALL indicate crlStaplingV3Used in sessionContext.
serverSigned1 = {
'transactionId': h2b(transactionId),
'euiccChallenge': euiccChallenge,
'serverAddress': self.server_hostname,
'serverChallenge': serverChallenge,
}
print("Tx serverSigned1: %s" % serverSigned1)
serverSigned1_bin = rsp.asn1.encode('ServerSigned1', serverSigned1)
print("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin))
output = {}
output['serverSigned1'] = b64encode2str(serverSigned1_bin)
# Generate a signature (serverSignature1) as described in section 5.7.13 "ES10b.AuthenticateServer" using the SK related to the selected CERT.DPauth.SIG.
# serverSignature1 SHALL be created using the private key associated to the RSP Server Certificate for authentication, and verified by the eUICC using the contained public key as described in section 2.6.9. serverSignature1 SHALL apply on serverSigned1 data object.
output['serverSignature1'] = b64encode2str(b'\x5f\x37\x40' + self.dp_auth.ecdsa_sign(serverSigned1_bin))
output['transactionId'] = transactionId
server_cert_aki = self.dp_auth.get_authority_key_identifier()
output['euiccCiPKIdToBeUsed'] = b64encode2str(b'\x04\x14' + server_cert_aki.key_identifier)
output['serverCertificate'] = b64encode2str(self.dp_auth.get_cert_as_der()) # CERT.DPauth.SIG
# FIXME: add those certificate
#output['otherCertsInChain'] = b64encode2str()
# create SessionState and store it in rss
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge,
cert_get_subject_key_id(ci_cert))
return output
@app.route('/gsma/rsp2/es9plus/authenticateClient', methods=['POST'])
@rsp_api_wrapper
def authenticateClient(self, request: IRequest, content: dict) -> dict:
"""See ES9+ AuthenticateClient in SGP.22 Section 5.6.3"""
transactionId = content['transactionId']
authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
print("Rx %s: %s" % authenticateServerResp)
if authenticateServerResp[0] == 'authenticateResponseError':
r_err = authenticateServerResp[1]
#r_err['transactionId']
#r_err['authenticateErrorCode']
raise ValueError("authenticateResponseError %s" % r_err)
r_ok = authenticateServerResp[1]
euiccSigned1 = r_ok['euiccSigned1']
# TODO: use original data, don't re-encode?
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
euiccSignature1_bin = r_ok['euiccSignature1']
euiccCertificate_dec = r_ok['euiccCertificate']
# TODO: use original data, don't re-encode?
euiccCertificate_bin = rsp.asn1.encode('Certificate', euiccCertificate_dec)
eumCertificate_dec = r_ok['eumCertificate']
eumCertificate_bin = rsp.asn1.encode('Certificate', eumCertificate_dec)
# TODO v3: otherCertsInChain
# load certificate
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
# Verify that the transactionId is known and relates to an ongoing RSP session. Otherwise, the SM-DP+
# SHALL return a status code "TransactionId - Unknown"
ss = self.rss.get(transactionId, None)
if ss is None:
raise ApiError('8.10.1', '3.9', 'Unknown')
ss.euicc_cert = euicc_cert
ss.eum_cert = eum_cert # TODO: do we need this in the state?
# Verify that the Root Certificate of the eUICC certificate chain corresponds to the
# euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3
if cert_get_auth_key_id(eum_cert) != ss.ci_cert_id:
raise ApiError('8.11.1', '3.9', 'Unknown')
# Verify the validity of the eUICC certificate chain
cs = CertificateSet(self.ci_get_cert_for_pkid(ss.ci_cert_id))
cs.add_intermediate_cert(eum_cert)
# TODO v3: otherCertsInChain
try:
cs.verify_cert_chain(euicc_cert)
except VerifyError:
raise ApiError('8.1.3', '6.1', 'Verification failed')
# raise ApiError('8.1.3', '6.3', 'Expired')
# Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
# Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
raise ApiError('8.1', '6.1', 'Verification failed')
# TODO: verify EID of eUICC cert is within permitted range of EUM cert
ss.eid = ss.euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
print("EID (from eUICC cert): %s" % ss.eid)
# Verify that the serverChallenge attached to the ongoing RSP session matches the
# serverChallenge returned by the eUICC. Otherwise, the SM-DP+ SHALL return a status code "eUICC -
# Verification failed".
if euiccSigned1['serverChallenge'] != ss.serverChallenge:
raise ApiError('8.1', '6.1', 'Verification failed')
# If ctxParams1 contains a ctxParamsForCommonAuthentication data object, the SM-DP+ Shall [...]
# TODO: We really do a very simplistic job here, this needs to be properly implemented later,
# considering all the various cases, profile state, etc.
if euiccSigned1['ctxParams1'][0] == 'ctxParamsForCommonAuthentication':
cpca = euiccSigned1['ctxParams1'][1]
matchingId = cpca.get('matchingId', None)
if not matchingId:
# TODO: check if any pending profile downloads for the EID
raise ApiError('8.2.6', '3.8', 'Refused')
if matchingId:
# look up profile based on matchingID. We simply check if a given file exists for now..
path = os.path.join(self.upp_dir, matchingId) + '.der'
# prevent directory traversal attack
if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
raise ApiError('8.2.6', '3.8', 'Refused')
if not os.path.isfile(path) or not os.access(path, os.R_OK):
raise ApiError('8.2.6', '3.8', 'Refused')
ss.matchingId = matchingId
with open(path, 'rb') as f:
pes = saip.ProfileElementSequence.from_der(f.read())
iccid_str = b2h(pes.get_pe_for_type('header').decoded['iccid'])
# FIXME: we actually want to perform the profile binding herr, and read the profile metadat from the profile
# Put together profileMetadata + _bin
ss.profileMetadata = ProfileMetadata(iccid_bin=h2b(swap_nibbles(iccid_str)), spn="OsmocomSPN", profile_name=matchingId)
profileMetadata_bin = ss.profileMetadata.gen_store_metadata_request()
# Put together smdpSigned2 + _bin
smdpSigned2 = {
'transactionId': h2b(ss.transactionId),
'ccRequiredFlag': False, # whether the Confirmation Code is required
#'bppEuiccOtpk': None, # whether otPK.EUICC.ECKA already used for binding the BPP, tag '5F49'
}
smdpSigned2_bin = rsp.asn1.encode('SmdpSigned2', smdpSigned2)
ss.smdpSignature2_do = b'\x5f\x37\x40' + self.dp_pb.ecdsa_sign(smdpSigned2_bin + b'\x5f\x37\x40' + euiccSignature1_bin)
# update non-volatile state with updated ss object
self.rss[transactionId] = ss
return {
'transactionId': transactionId,
'profileMetadata': b64encode2str(profileMetadata_bin),
'smdpSigned2': b64encode2str(smdpSigned2_bin),
'smdpSignature2': b64encode2str(ss.smdpSignature2_do),
'smdpCertificate': b64encode2str(self.dp_pb.get_cert_as_der()), # CERT.DPpb.SIG
}
@app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST'])
@rsp_api_wrapper
def getBoundProfilePackage(self, request: IRequest, content: dict) -> dict:
"""See ES9+ GetBoundProfilePackage SGP.22 Section 5.6.2"""
transactionId = content['transactionId']
# Verify that the received transactionId is known and relates to an ongoing RSP session
ss = self.rss.get(transactionId, None)
if not ss:
raise ApiError('8.10.1', '3.9', 'The RSP session identified by the TransactionID is unknown')
prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
print("Rx %s: %s" % prepDownloadResp)
if prepDownloadResp[0] == 'downloadResponseError':
r_err = prepDownloadResp[1]
#r_err['transactionId']
#r_err['downloadErrorCode']
raise ValueError("downloadResponseError %s" % r_err)
r_ok = prepDownloadResp[1]
# Verify the euiccSignature2 computed over euiccSigned2 and smdpSignature2 using the PK.EUICC.SIG attached to the ongoing RSP session
euiccSigned2 = r_ok['euiccSigned2']
# TODO: use original data, don't re-encode?
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
if not self._ecdsa_verify(ss.euicc_cert, r_ok['euiccSignature2'], euiccSigned2_bin + ss.smdpSignature2_do):
raise ApiError('8.1', '6.1', 'eUICC signature is invalid')
# not in spec: Verify that signed TransactionID is outer transaction ID
if h2b(transactionId) != euiccSigned2['transactionId']:
raise ApiError('8.10.1', '3.9', 'The signed transactionId != outer transactionId')
# store otPK.EUICC.ECKA in session state
ss.euicc_otpk = euiccSigned2['euiccOtpk']
print("euiccOtpk: %s" % (b2h(ss.euicc_otpk)))
# Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
# Reference value of CERT.DPpb.ECDDSA
print("curve = %s" % self.dp_pb.get_curve())
ss.smdp_ot = ec.generate_private_key(self.dp_pb.get_curve())
# extract the public key in (hopefully) the right format for the ES8+ interface
ss.smdp_otpk = ss.smdp_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
print("smdpOtpk: %s" % b2h(ss.smdp_otpk))
print("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())))
ss.host_id = b'mahlzeit'
# Generate Session Keys using the CRT, opPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
euicc_public_key = ec.EllipticCurvePublicKey.from_encoded_point(ss.smdp_ot.curve, ss.euicc_otpk)
ss.shared_secret = ss.smdp_ot.exchange(ec.ECDH(), euicc_public_key)
print("shared_secret: %s" % b2h(ss.shared_secret))
# TODO: Check if this order requires a Confirmation Code verification
# Perform actual protection + binding of profile package (or return pre-bound one)
with open(os.path.join(self.upp_dir, ss.matchingId)+'.der', 'rb') as f:
upp = UnprotectedProfilePackage.from_der(f.read(), metadata=ss.profileMetadata)
# HACK: Use empty PPP as we're still debuggin the configureISDP step, and we want to avoid
# cluttering the log with stuff happening after the failure
#upp = UnprotectedProfilePackage.from_der(b'', metadata=ss.profileMetadata)
if False:
# Use random keys
bpp = BoundProfilePackage.from_upp(upp)
else:
# Use sesssion keys
ppp = ProtectedProfilePackage.from_upp(upp, BspInstance(b'\x00'*16, b'\x11'*16, b'\x22'*16))
bpp = BoundProfilePackage.from_ppp(ppp)
# update non-volatile state with updated ss object
self.rss[transactionId] = ss
return {
'transactionId': transactionId,
'boundProfilePackage': b64encode2str(bpp.encode(ss, self.dp_pb)),
}
@app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST'])
@rsp_api_wrapper
def handleNotification(self, request: IRequest, content: dict) -> dict:
"""See ES9+ HandleNotification in SGP.22 Section 5.6.4"""
pendingNotification_bin = b64decode(content['pendingNotification'])
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
print("Rx %s: %s" % pendingNotification)
if pendingNotification[0] == 'profileInstallationResult':
profileInstallRes = pendingNotification[1]
pird = profileInstallRes['profileInstallationResultData']
transactionId = b2h(pird['transactionId'])
ss = self.rss.get(transactionId, None)
if ss is None:
print("Unable to find session for transactionId")
return
profileInstallRes['euiccSignPIR']
# TODO: use original data, don't re-encode?
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
# verify eUICC signature
if not self._ecdsa_verify(ss.euicc_cert, profileInstallRes['euiccSignPIR'], pird_bin):
print("Unable to verify eUICC signature")
print("Profile Installation Final Result: ", pird['finalResult'])
# remove session state
del self.rss[transactionId]
elif pendingNotification[0] == 'otherSignedNotification':
# TODO
pass
else:
raise ValueError(pendingNotification)
#@app.route('/gsma/rsp3/es9plus/handleDeviceChangeRequest, methods=['POST']')
#@rsp_api_wrapper
#"""See ES9+ ConfirmDeviceChange in SGP.22 Section 5.6.6"""
# TODO: implement this
@app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST'])
@rsp_api_wrapper
def cancelSession(self, request: IRequest, content: dict) -> dict:
"""See ES9+ CancelSession in SGP.22 Section 5.6.5"""
print("Rx JSON: %s" % content)
transactionId = content['transactionId']
# Verify that the received transactionId is known and relates to an ongoing RSP session
ss = self.rss.get(transactionId, None)
if ss is None:
raise ApiError('8.10.1', '3.9', 'The RSP session identified by the transactionId is unknown')
cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
print("Rx %s: %s" % cancelSessionResponse)
if cancelSessionResponse[0] == 'cancelSessionResponseError':
# FIXME: print some error
return
cancelSessionResponseOk = cancelSessionResponse[1]
# TODO: use original data, don't re-encode?
ecsr = cancelSessionResponseOk['euiccCancelSessionSigned']
ecsr_bin = rsp.asn1.encode('EuiccCancelSessionSigned', ecsr)
# Verify the eUICC signature (euiccCancelSessionSignature) using the PK.EUICC.SIG attached to the ongoing RSP session
if not self._ecdsa_verify(ss.euicc_cert, cancelSessionResponseOk['euiccCancelSessionSignature'], ecsr_bin):
raise ApiError('8.1', '6.1', 'eUICC signature is invalid')
# Verify that the received smdpOid corresponds to the one in SM-DP+ CERT.DPauth.SIG
subj_alt_name = self.dp_auth.get_subject_alt_name()
if x509.ObjectIdentifier(ecsr['smdpOid']) != subj_alt_name.oid:
raise ApiError('8.8', '3.10', 'The provided SM-DP+ OID is invalid.')
if ecsr['transactionId'] != h2b(transactionId):
raise ApiError('8.10.1', '3.9', 'The signed transactionId != outer transactionId')
# TODO: 1. Notify the Operator using the function "ES2+.HandleNotification" function
# TODO: 2. Terminate the corresponding pending download process.
# TODO: 3. If required, execute the SM-DS Event Deletion procedure described in section 3.6.3.
# delete actual session data
del self.rss[transactionId]
return { 'transactionId': transactionId }
def main(argv):
parser = argparse.ArgumentParser()
#parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost")
#parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
#parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
args = parser.parse_args()
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=True)
#hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
hs.app.run("localhost", 8000)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -32,12 +32,14 @@ import re
import sys
import traceback
import json
import csv
from pySim.commands import SimCardCommands
from pySim.transport import init_reader
from pySim.cards import _cards_classes, card_detect
from pySim.legacy.cards import _cards_classes, card_detect
from pySim.utils import h2b, swap_nibbles, rpad, derive_milenage_opc, calculate_luhn, dec_iccid
from pySim.ts_51_011 import EF, EF_AD
from pySim.ts_51_011 import EF_AD
from pySim.legacy.ts_51_011 import EF
from pySim.card_handler import *
from pySim.utils import *
@@ -113,8 +115,8 @@ def parse_options():
)
parser.add_option("--mnclen", dest="mnclen", type="choice",
help="Length of Mobile Network Code [default: %default]",
default=2,
choices=[2, 3],
default="auto",
choices=["2", "3", "auto"],
)
parser.add_option("-m", "--smsc", dest="smsc",
help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
@@ -149,6 +151,9 @@ def parse_options():
default=None,
choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
)
parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
)
parser.add_option("--epdgid", dest="epdgid",
help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
)
@@ -318,8 +323,15 @@ def gen_parameters(opts):
# MCC always has 3 digits
mcc = lpad(mcc, 3, "0")
# MNC must be at least 2 digits
mnc = lpad(mnc, 2, "0")
# The MNC must be at least 2 digits long. This is also the most common case.
# The user may specify an explicit length using the --mnclen option.
if opts.mnclen != "auto":
if len(mnc) > int(opts.mnclen):
raise ValueError('mcc is longer than specified in option --mnclen')
mnc = lpad(mnc, int(opts.mnclen), "0")
else:
mnc = lpad(mnc, 2, "0")
# Digitize country code (2 or 3 digits)
cc_digits = _cc_digits(opts.country)
@@ -346,8 +358,8 @@ def gen_parameters(opts):
# ICCID (19 digits, E.118), though some phase1 vendors use 20 :(
if opts.iccid is not None:
iccid = opts.iccid
if not _isnum(iccid, 19) and not _isnum(iccid, 20):
raise ValueError('ICCID must be 19 or 20 digits !')
if not _isnum(iccid, 18) and not _isnum(iccid, 19) and not _isnum(iccid, 20):
raise ValueError('ICCID must be 18, 19 or 20 digits !')
else:
if opts.num is None:
@@ -490,6 +502,7 @@ def gen_parameters(opts):
'impi': opts.impi,
'impu': opts.impu,
'opmode': opts.opmode,
'fplmn': opts.fplmn,
}
@@ -524,40 +537,86 @@ def write_params_csv(opts, params):
f.close()
def _read_params_csv(opts, iccid=None, imsi=None):
import csv
f = open(opts.read_csv, 'r')
def find_row_in_csv_file(csv_file_name:str, num=None, iccid=None, imsi=None):
"""
Pick a matching row in a CSV file by row number or ICCID or IMSI. When num
is not None, the search parameters iccid and imsi are ignored. When
searching for a specific ICCID or IMSI the caller must set num to None. It
is possible to search for an ICCID or an IMSI at the same time. The first
line that either contains a matching ICCID or IMSI is returned. Unused
search parameters must be set to None.
"""
f = open(csv_file_name, 'r')
cr = csv.DictReader(f)
# Make sure the CSV file contains at least the fields we are searching for
if not 'iccid' in cr.fieldnames:
raise Exception("wrong CSV file format - no field \"iccid\" or missing header!")
if not 'imsi' in cr.fieldnames:
raise Exception("wrong CSV file format - no field \"imsi\" or missing header!")
# Enforce at least one search parameter
if not num and not iccid and not imsi:
raise Exception("no CSV file search parameters!")
# Lower-case fieldnames
cr.fieldnames = [field.lower() for field in cr.fieldnames]
i = 0
if not 'iccid' in cr.fieldnames:
raise Exception("CSV file in wrong format!")
for row in cr:
if opts.num is not None and opts.read_iccid is False and opts.read_imsi is False:
if opts.num == i:
# Pick a specific row by line number (num)
if num is not None and iccid is None and imsi is None:
if num == i:
f.close()
return row
i += 1
# Pick the first row that contains the specified ICCID
if row['iccid'] == iccid:
f.close()
return row
# Pick the first row that contains the specified IMSI
if row['imsi'] == imsi:
f.close()
return row
i += 1
f.close()
print("Could not read card parameters from CSV file, no matching entry found.")
return None
def read_params_csv(opts, imsi=None, iccid=None):
row = _read_params_csv(opts, iccid=iccid, imsi=imsi)
"""
Read the card parameters from a CSV file. This function will generate the
same dictionary that gen_parameters would generate from parameters passed as
commandline arguments.
"""
row = find_row_in_csv_file(opts.read_csv, opts.num, iccid=iccid, imsi=imsi)
if row is not None:
row['mcc'] = row.get('mcc', mcc_from_imsi(row.get('imsi')))
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi')))
# We cannot determine the MNC length (2 or 3 digits?) from the IMSI
# alone. In cases where the user has specified an mnclen via the
# commandline options we can use that info, otherwise we guess that
# the length is 2, which is also the most common case.
if opts.mnclen != "auto":
if opts.mnclen == "2":
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
elif opts.mnclen == "3":
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), True))
else:
raise ValueError("invalid parameter --mnclen, must be 2 or 3 or auto")
else:
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
# NOTE: We might concider to specify a new CSV field "mnclen" in our
# CSV files for a better automatization. However, this only makes sense
# when the tools and databases we export our files from will also add
# such a field.
pin_adm = None
# We need to escape the pin_adm we get from the csv
@@ -621,7 +680,7 @@ def write_params_hlr(opts, params):
conn.close()
def write_parameters(opts, params):
def write_parameters_to_csv_and_hlr(opts, params):
write_params_csv(opts, params)
write_params_hlr(opts, params)
@@ -668,24 +727,23 @@ def save_batch(opts):
fh.close()
def process_card(opts, first, ch):
def process_card(scc, opts, first, ch):
# Connect transport
ch.get(first)
# Get card
card = card_detect(opts.type, scc)
if card is None:
print("No card detected!")
return -1
# Probe only
if opts.probe:
return 0
# Erase if requested (not in dry run mode!)
if opts.dry_run is False:
# Connect transport
ch.get(first)
if opts.dry_run is False:
# Get card
card = card_detect(opts.type, scc)
if card is None:
print("No card detected!")
return -1
# Probe only
if opts.probe:
return 0
# Erase if requested
if opts.erase:
print("Formatting ...")
card.erase()
@@ -698,22 +756,15 @@ def process_card(opts, first, ch):
imsi = None
iccid = None
if opts.read_iccid:
if opts.dry_run:
# Connect transport
ch.get(False)
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
iccid = dec_iccid(res)
elif opts.read_imsi:
if opts.dry_run:
# Connect transport
ch.get(False)
(res, _) = scc.read_binary(EF['IMSI'])
imsi = swap_nibbles(res)[3:]
else:
imsi = opts.imsi
cp = read_params_csv(opts, imsi=imsi, iccid=iccid)
if cp is None:
print("Error reading parameters from CSV file!\n")
return 2
print_parameters(cp)
@@ -724,8 +775,8 @@ def process_card(opts, first, ch):
else:
print("Dry Run: NOT PROGRAMMING!")
# Write parameters permanently
write_parameters(opts, cp)
# Write parameters to a specified CSV file or an HLR database (not the card)
write_parameters_to_csv_and_hlr(opts, cp)
# Batch mode state update and save
if opts.num is not None:
@@ -743,8 +794,6 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts)
if sl is None:
exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
@@ -770,7 +819,7 @@ if __name__ == '__main__':
while 1:
try:
rc = process_card(opts, first, ch)
rc = process_card(scc, opts, first, ch)
except (KeyboardInterrupt):
print("")
print("Terminated by user!")

View File

@@ -28,20 +28,22 @@ import os
import random
import re
import sys
from pySim.ts_51_011 import EF, DF, EF_SST_map, EF_AD
from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
from pySim.ts_51_011 import EF_SST_map, EF_AD
from pySim.legacy.ts_51_011 import EF, DF
from pySim.ts_31_102 import EF_UST_map
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
from pySim.ts_31_103 import EF_IST_map
from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
from pySim.commands import SimCardCommands
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.exceptions import SwMatchError
from pySim.cards import card_detect, SimCard, UsimCard, IsimCard
from pySim.utils import h2b, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
from pySim.utils import format_xplmn_w_act, dec_st
from pySim.utils import h2s, format_ePDGSelection
from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
from pySim.utils import h2b, h2s, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
from pySim.legacy.utils import format_xplmn_w_act, dec_st
option_parser = argparse.ArgumentParser(prog='pySim-read',
description='Legacy tool for reading some parts of a SIM card',
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
@@ -72,8 +74,6 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts)
if sl is None:
exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
@@ -253,6 +253,14 @@ if __name__ == '__main__':
else:
print("EHPLMN: Can't read, response code = %s" % (sw,))
# EF.FPLMN
if usim_card.file_exists(EF_USIM_ADF_map['FPLMN']):
res, sw = usim_card.read_fplmn()
if sw == '9000':
print(f'FPLMN:\n{res}')
else:
print(f'FPLMN: Can\'t read, response code = {sw}')
# EF.UST
try:
if usim_card.file_exists(EF_USIM_ADF_map['UST']):

View File

@@ -2,7 +2,7 @@
# Interactive shell for working with SIM / UICC / USIM / ISIM cards
#
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
# (C) 2021-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
@@ -17,125 +17,87 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List
from typing import List, Optional
import json
import traceback
import cmd2
from cmd2 import style, fg
from packaging import version
from cmd2 import style
# cmd2 >= 2.3.0 has deprecated the bg/fg in favor of Bg/Fg :(
if version.parse(cmd2.__version__) < version.parse("2.3.0"):
from cmd2 import fg, bg # pylint: disable=no-name-in-module
RED = fg.red
LIGHT_RED = fg.bright_red
LIGHT_GREEN = fg.bright_green
else:
from cmd2 import Fg, Bg # pylint: disable=no-name-in-module
RED = Fg.RED
LIGHT_RED = Fg.LIGHT_RED
LIGHT_GREEN = Fg.LIGHT_GREEN
from cmd2 import CommandSet, with_default_category, with_argparser
import argparse
import os
import sys
import inspect
from pathlib import Path
from io import StringIO
from pprint import pprint as pp
from pySim.exceptions import *
from pySim.commands import SimCardCommands
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
from pySim.cards import card_detect, SimCard
from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, bertlv_parse_one, sw_match
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr, dec_iccid
from pySim.utils import is_hexstr_or_decimal, is_hexstr, is_decimal
from pySim.card_handler import CardHandler, CardHandlerAuto
from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
from pySim.profile import CardProfile
from pySim.ts_102_221 import CardProfileUICC
from pySim.filesystem import CardMF, CardDF, CardADF
from pySim.ts_102_222 import Ts102222Commands
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
from pySim.ara_m import CardApplicationARAM
from pySim.global_platform import CardApplicationISD
from pySim.gsm_r import DF_EIRENE
from pySim.cat import ProactiveCommand
# we need to import this module so that the SysmocomSJA2 sub-class of
# CardModel is created, which will add the ATR-based matching and
# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
import pySim.sysmocom_sja2
from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
def init_card(sl):
"""
Detect card in reader and setup card profile and runtime state. This
function must be called at least once on startup. The card and runtime
state object (rs) is required for all pySim-shell commands.
"""
# Wait up to three seconds for a card in reader and try to detect
# the card type.
print("Waiting for card...")
try:
sl.wait_for_card(3)
except NoCardError:
print("No card detected!")
return None, None
except:
print("Card not readable!")
return None, None
generic_card = False
card = card_detect("auto", scc)
if card is None:
print("Warning: Could not detect card type - assuming a generic card type...")
card = SimCard(scc)
generic_card = True
profile = CardProfile.pick(scc)
if profile is None:
print("Unsupported card type!")
return None, card
# ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
# references, however card manufactures may still decide to pick an
# arbitrary key reference. In case we run on a generic card class that is
# detected as an UICC, we will pick the key reference that is officially
# specified.
if generic_card and isinstance(profile, CardProfileUICC):
card._adm_chv_num = 0x0A
print("Info: Card is of type: %s" % str(profile))
# FIXME: This shouln't be here, the profile should add the applications,
# however, we cannot simply put his into ts_102_221.py since we would
# have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
# imports from ts_102_221.py. This means we will end up with a circular
# import, which needs to be resolved first.
if isinstance(profile, CardProfileUICC):
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
profile.add_application(CardApplicationARAM())
profile.add_application(CardApplicationISD())
# Create runtime state with card profile
rs = RuntimeState(card, profile)
# FIXME: This is an GSM-R related file, it needs to be added throughout,
# the profile. At the moment we add it for all cards, this won't hurt,
# but regular SIM and UICC will not have it and fail to select it.
rs.mf.add_file(DF_EIRENE())
CardModel.apply_matching_models(scc, rs)
# inform the transport that we can do context-specific SW interpretation
sl.set_sw_interpreter(rs)
return rs, card
from pySim.app import init_card
class PysimApp(cmd2.Cmd):
class Cmd2Compat(cmd2.Cmd):
"""Backwards-compatibility wrapper around cmd2.Cmd to support older and newer
releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
def run_editor(self, file_path: Optional[str] = None) -> None:
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
return self._run_editor(file_path) # pylint: disable=no-member
else:
return super().run_editor(file_path) # pylint: disable=no-member
class Settable2Compat(cmd2.Settable):
"""Backwards-compatibility wrapper around cmd2.Settable to support older and newer
releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
def __init__(self, name, val_type, description, settable_object, **kwargs):
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
super().__init__(name, val_type, description, **kwargs) # pylint: disable=no-value-for-parameter
else:
super().__init__(name, val_type, description, settable_object, **kwargs) # pylint: disable=too-many-function-args
class PysimApp(Cmd2Compat):
CUSTOM_CATEGORY = 'pySim Commands'
BANNER = """Welcome to pySim-shell!
(C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html """
def __init__(self, card, rs, sl, ch, script=None):
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
kwargs = {'use_ipython': True}
else:
kwargs = {'include_ipy': True}
# pylint: disable=unexpected-keyword-arg
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
use_ipython=True, auto_load_commands=False, startup_script=script)
self.intro = style('Welcome to pySim-shell!', fg=fg.red)
auto_load_commands=False, startup_script=script, **kwargs)
self.intro = style(self.BANNER, fg=RED)
self.default_category = 'pySim-shell built-in commands'
self.card = None
self.rs = None
@@ -145,18 +107,17 @@ class PysimApp(cmd2.Cmd):
self.ch = ch
self.numeric_path = False
self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
onchange_cb=self._onchange_numeric_path))
self.conserve_write = True
self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
onchange_cb=self._onchange_conserve_write))
self.json_pretty_print = True
self.add_settable(cmd2.Settable('json_pretty_print',
bool, 'Pretty-Print JSON output'))
self.apdu_trace = False
self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
onchange_cb=self._onchange_apdu_trace))
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
onchange_cb=self._onchange_numeric_path))
self.add_settable(Settable2Compat('conserve_write', bool, 'Read and compare before write', self,
onchange_cb=self._onchange_conserve_write))
self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
onchange_cb=self._onchange_apdu_trace))
self.equip(card, rs)
def equip(self, card, rs):
@@ -171,7 +132,11 @@ class PysimApp(cmd2.Cmd):
if self.rs:
lchan = self.rs.lchan[0]
lchan.unregister_cmds(self)
for cmds in [Iso7816Commands, PySimCommands]:
if self.rs.profile:
for cmd_set in self.rs.profile.shell_cmdsets:
self.unregister_command_set(cmd_set)
for cmds in [Iso7816Commands, Ts102222Commands, PySimCommands]:
cmd_set = self.find_commandsets(cmds)
if cmd_set:
self.unregister_command_set(cmd_set[0])
@@ -186,10 +151,19 @@ class PysimApp(cmd2.Cmd):
self._onchange_conserve_write(
'conserve_write', False, self.conserve_write)
self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
if self.rs.profile:
for cmd_set in self.rs.profile.shell_cmdsets:
self.register_command_set(cmd_set)
self.register_command_set(Iso7816Commands())
self.register_command_set(Ts102222Commands())
self.register_command_set(PySimCommands())
self.iccid, sw = self.card.read_iccid()
try:
self.lchan.select('MF/EF.ICCID', self)
self.iccid = dec_iccid(self.lchan.read_binary()[0])
except:
self.iccid = None
self.lchan.select('MF', self)
rc = True
else:
@@ -222,7 +196,7 @@ class PysimApp(cmd2.Cmd):
class Cmd2ApduTracer(ApduTracer):
def __init__(self, cmd2_app):
self.cmd2 = app
self.cmd2 = cmd2_app
def trace_response(self, cmd, sw, resp):
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
@@ -231,7 +205,11 @@ class PysimApp(cmd2.Cmd):
def update_prompt(self):
if self.lchan:
path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
self.prompt = 'pySIM-shell (%s)> ' % (path_str)
scp = self.lchan.scc.scp
if scp:
self.prompt = 'pySIM-shell (%s:%02u:%s)> ' % (str(scp), self.lchan.lchan_nr, path_str)
else:
self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
else:
if self.card:
self.prompt = 'pySIM-shell (no card profile)> '
@@ -250,20 +228,32 @@ class PysimApp(cmd2.Cmd):
@cmd2.with_category(CUSTOM_CATEGORY)
def do_equip(self, opts):
"""Equip pySim-shell with card"""
rs, card = init_card(sl)
if self.rs and self.rs.profile:
for cmd_set in self.rs.profile.shell_cmdsets:
self.unregister_command_set(cmd_set)
rs, card = init_card(self.sl)
self.equip(card, rs)
apdu_cmd_parser = argparse.ArgumentParser()
apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
@cmd2.with_argparser(apdu_cmd_parser)
def do_apdu(self, opts):
"""Send a raw APDU to the card, and print SW + Response.
DANGEROUS: pySim-shell will not know any card state changes, and
not continue to work as expected if you e.g. select a different
file."""
data, sw = self.card._scc._tp.send_apdu(opts.APDU)
CAUTION: this command bypasses the logical channel handling of pySim-shell and card state changes are not
tracked. Dpending on the raw APDU sent, pySim-shell may not continue to work as expected if you e.g. select
a different file."""
# When sending raw APDUs we access the scc object through _scc member of the card object. It should also be
# noted that the apdu command plays an exceptional role since it is the only card accessing command that
# can be executed without the presence of a runtime state (self.rs) object. However, this also means that
# self.lchan is also not present (see method equip).
if opts.raw or self.lchan is None:
data, sw = self.card._scc.send_apdu(opts.APDU)
else:
data, sw = self.lchan.scc.send_apdu(opts.APDU)
if data:
self.poutput("SW: %s, RESP: %s" % (sw, data))
else:
@@ -272,6 +262,15 @@ class PysimApp(cmd2.Cmd):
if not sw_match(sw, opts.expect_sw):
raise SwMatchError(sw, opts.expect_sw)
@cmd2.with_category(CUSTOM_CATEGORY)
def do_reset(self, opts):
"""Reset the Card."""
atr = self.card.reset()
if self.lchan and self.lchan.scc.scp:
self.lchan.scc.scp = None
self.poutput('Card ATR: %s' % i2h(atr))
self.update_prompt()
class InterceptStderr(list):
def __init__(self):
self._stderr_backup = sys.stderr
@@ -287,23 +286,23 @@ class PysimApp(cmd2.Cmd):
sys.stderr = self._stderr_backup
def _show_failure_sign(self):
self.poutput(style(" +-------------+", fg=fg.bright_red))
self.poutput(style(" + ## ## +", fg=fg.bright_red))
self.poutput(style(" + ## ## +", fg=fg.bright_red))
self.poutput(style(" + ### +", fg=fg.bright_red))
self.poutput(style(" + ## ## +", fg=fg.bright_red))
self.poutput(style(" + ## ## +", fg=fg.bright_red))
self.poutput(style(" +-------------+", fg=fg.bright_red))
self.poutput(style(" +-------------+", fg=LIGHT_RED))
self.poutput(style(" + ## ## +", fg=LIGHT_RED))
self.poutput(style(" + ## ## +", fg=LIGHT_RED))
self.poutput(style(" + ### +", fg=LIGHT_RED))
self.poutput(style(" + ## ## +", fg=LIGHT_RED))
self.poutput(style(" + ## ## +", fg=LIGHT_RED))
self.poutput(style(" +-------------+", fg=LIGHT_RED))
self.poutput("")
def _show_success_sign(self):
self.poutput(style(" +-------------+", fg=fg.bright_green))
self.poutput(style(" + ## +", fg=fg.bright_green))
self.poutput(style(" + ## +", fg=fg.bright_green))
self.poutput(style(" + # ## +", fg=fg.bright_green))
self.poutput(style(" + ## # +", fg=fg.bright_green))
self.poutput(style(" + ## +", fg=fg.bright_green))
self.poutput(style(" +-------------+", fg=fg.bright_green))
self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
self.poutput(style(" + ## +", fg=LIGHT_GREEN))
self.poutput(style(" + ## +", fg=LIGHT_GREEN))
self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
self.poutput(style(" + ## +", fg=LIGHT_GREEN))
self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
self.poutput("")
def _process_card(self, first, script_path):
@@ -314,7 +313,7 @@ class PysimApp(cmd2.Cmd):
rc = self.equip(card, rs)
except:
self.poutput("")
self.poutput("Card initialization failed with an exception:")
self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
self.poutput("---------------------8<---------------------")
traceback.print_exc()
self.poutput("---------------------8<---------------------")
@@ -383,7 +382,7 @@ class PysimApp(cmd2.Cmd):
# In case of failure, try multiple times.
for i in range(opts.tries):
# fetch card into reader bay
ch.get(first)
self.ch.get(first)
# if necessary execute an action before we start processing the card
if(opts.pre_card_action):
@@ -406,9 +405,9 @@ class PysimApp(cmd2.Cmd):
# Depending on success or failure, the card goes either in the "error" bin or in the
# "done" bin.
if rc < 0:
ch.error()
self.ch.error()
else:
ch.done()
self.ch.done()
# In most cases it is possible to proceed with the next card, but the
# user may decide to halt immediately when an error occurs
@@ -429,7 +428,7 @@ class PysimApp(cmd2.Cmd):
return
except:
self.poutput("")
self.poutput("Card handling failed with an exception:")
self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
self.poutput("---------------------8<---------------------")
traceback.print_exc()
self.poutput("---------------------8<---------------------")
@@ -442,13 +441,13 @@ class PysimApp(cmd2.Cmd):
first = False
echo_parser = argparse.ArgumentParser()
echo_parser.add_argument('string', help="string to echo on the shell")
echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
@cmd2.with_argparser(echo_parser)
@cmd2.with_category(CUSTOM_CATEGORY)
def do_echo(self, opts):
"""Echo (print) a string on the console"""
self.poutput(opts.string)
self.poutput(' '.join(opts.string))
@cmd2.with_category(CUSTOM_CATEGORY)
def do_version(self, opts):
@@ -502,7 +501,7 @@ class PySimCommands(CommandSet):
if isinstance(self._cmd.lchan.selected_file, CardDF):
if action_df:
action_df(context, opts)
action_df(context, **kwargs)
files = self._cmd.lchan.selected_file.get_selectables(
flags=['FNAMES', 'ANAMES'])
@@ -536,7 +535,32 @@ class PySimCommands(CommandSet):
# below, so we must not move up.
if skip_df == False:
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
fcp_dec = self._cmd.lchan.select("..", self._cmd)
parent = self._cmd.lchan.selected_file.parent
df = self._cmd.lchan.selected_file
adf = self._cmd.lchan.selected_adf
if isinstance(parent, CardMF) and (adf and adf.has_fs == False):
# Not every application that may be present on a GlobalPlatform card will support the SELECT
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
# "select by name" method, which means we can only select an application and not a file.
# The consequence of this is that we may get trapped in an application that does not have
# ISIM/USIM like file system support and the only way to leave that application is to select
# an ISIM/USIM application in order to get the file system access back.
#
# To automate this escape-route while traversing the file system we will check whether
# the parent file is the MF. When this is the case and the selected ADF has no file system
# support, we will select an arbitrary ADF that has file system support first and from there
# we will then select the MF.
for selectable in parent.get_selectables().items():
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
self._cmd.lchan.select(selectable[1].name, self._cmd)
break
self._cmd.lchan.select(df.get_mf().name, self._cmd)
else:
# Normal DF/ADF selection
fcp_dec = self._cmd.lchan.select("..", self._cmd)
elif action_ef:
df_before_action = self._cmd.lchan.selected_file
@@ -695,12 +719,6 @@ class PySimCommands(CommandSet):
raise RuntimeError(
"unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
def do_reset(self, opts):
"""Reset the Card."""
atr = self._cmd.lchan.reset(self._cmd)
self._cmd.poutput('Card ATR: %s' % atr)
self._cmd.update_prompt()
def do_desc(self, opts):
"""Display human readable file description for the currently selected file"""
desc = self._cmd.lchan.selected_file.desc
@@ -709,11 +727,19 @@ class PySimCommands(CommandSet):
else:
self._cmd.poutput("no description available")
def do_verify_adm(self, arg):
"""VERIFY the ADM1 PIN"""
if arg:
verify_adm_parser = argparse.ArgumentParser()
verify_adm_parser.add_argument('ADM1', nargs='?', type=is_hexstr_or_decimal,
help='ADM1 pin value. If none given, CSV file will be queried')
@cmd2.with_argparser(verify_adm_parser)
def do_verify_adm(self, opts):
"""Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
to get write/update permissions to most of the files on SIM cards.
Currently only ADM1 is supported."""
if opts.ADM1:
# use specified ADM-PIN
pin_adm = sanitize_pin_adm(arg)
pin_adm = sanitize_pin_adm(opts.ADM1)
else:
# try to find an ADM-PIN if none is specified
result = card_key_provider_get_field(
@@ -727,10 +753,21 @@ class PySimCommands(CommandSet):
"cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
if pin_adm:
self._cmd.card.verify_adm(h2b(pin_adm))
self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
else:
raise ValueError("error: cannot authenticate, no adm-pin!")
def do_cardinfo(self, opts):
"""Display information about the currently inserted card"""
self._cmd.poutput("Card info:")
self._cmd.poutput(" Name: %s" % self._cmd.card.name)
self._cmd.poutput(" ATR: %s" % b2h(self._cmd.lchan.scc.get_atr()))
self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte)
self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl)
self._cmd.poutput(" AIDs:")
for a in self._cmd.rs.mf.applications:
self._cmd.poutput(" %s" % a)
@with_default_category('ISO7816 Commands')
class Iso7816Commands(CommandSet):
@@ -777,7 +814,7 @@ class Iso7816Commands(CommandSet):
verify_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
verify_chv_parser.add_argument(
'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(verify_chv_parser)
def do_verify_chv(self, opts):
@@ -785,23 +822,23 @@ class Iso7816Commands(CommandSet):
call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
PIN2."""
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
(data, sw) = self._cmd.lchan.scc.verify_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV verification successful")
unblock_chv_parser = argparse.ArgumentParser()
unblock_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
unblock_chv_parser.add_argument(
'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
'puk_code', type=is_decimal, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
unblock_chv_parser.add_argument(
'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(unblock_chv_parser)
def do_unblock_chv(self, opts):
"""Unblock PIN code using specified PUK code"""
new_pin = self.get_code(opts.new_pin_code)
puk = self.get_code(opts.puk_code)
(data, sw) = self._cmd.card._scc.unblock_chv(
(data, sw) = self._cmd.lchan.scc.unblock_chv(
opts.pin_nr, h2b(puk), h2b(new_pin))
self._cmd.poutput("CHV unblock successful")
@@ -809,16 +846,16 @@ class Iso7816Commands(CommandSet):
change_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
change_chv_parser.add_argument(
'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
'pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
change_chv_parser.add_argument(
'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(change_chv_parser)
def do_change_chv(self, opts):
"""Change PIN code to a new PIN code"""
new_pin = self.get_code(opts.new_pin_code)
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.card._scc.change_chv(
(data, sw) = self._cmd.lchan.scc.change_chv(
opts.pin_nr, h2b(pin), h2b(new_pin))
self._cmd.poutput("CHV change successful")
@@ -826,38 +863,45 @@ class Iso7816Commands(CommandSet):
disable_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
disable_chv_parser.add_argument(
'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(disable_chv_parser)
def do_disable_chv(self, opts):
"""Disable PIN code using specified PIN code"""
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
(data, sw) = self._cmd.lchan.scc.disable_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV disable successful")
enable_chv_parser = argparse.ArgumentParser()
enable_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
enable_chv_parser.add_argument(
'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(enable_chv_parser)
def do_enable_chv(self, opts):
"""Enable PIN code using specified PIN code"""
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
(data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV enable successful")
def do_deactivate_file(self, opts):
"""Deactivate the currently selected EF"""
(data, sw) = self._cmd.card._scc.deactivate_file()
(data, sw) = self._cmd.lchan.scc.deactivate_file()
activate_file_parser = argparse.ArgumentParser()
activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
@cmd2.with_argparser(activate_file_parser)
def do_activate_file(self, opts):
"""Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
SIM. You need to specify the name or FID of the file to activate."""
"""Activate the specified EF by sending an ACTIVATE FILE apdu command (used to be called REHABILITATE
in TS 11.11 for classic SIM).
This command is used to (re-)activate a file that is currently in deactivated (sometimes also called
"invalidated") state. You need to call this from the DF above the to-be-activated EF and specify the name or
FID of the file to activate.
Note that for *deactivation* the to-be-deactivated EF must be selected, but for *activation*, the DF
above the to-be-activated EF must be selected!"""
(data, sw) = self._cmd.lchan.activate_file(opts.NAME)
def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
@@ -872,8 +916,10 @@ class Iso7816Commands(CommandSet):
@cmd2.with_argparser(open_chan_parser)
def do_open_channel(self, opts):
"""Open a logical channel."""
(data, sw) = self._cmd.card._scc.manage_channel(
(data, sw) = self._cmd.lchan.scc.manage_channel(
mode='open', lchan_nr=opts.chan_nr)
# this is executed only in successful case, as unsuccessful raises exception
self._cmd.lchan.add_lchan(opts.chan_nr)
close_chan_parser = argparse.ArgumentParser()
close_chan_parser.add_argument(
@@ -882,28 +928,28 @@ class Iso7816Commands(CommandSet):
@cmd2.with_argparser(close_chan_parser)
def do_close_channel(self, opts):
"""Close a logical channel."""
(data, sw) = self._cmd.card._scc.manage_channel(
(data, sw) = self._cmd.lchan.scc.manage_channel(
mode='close', lchan_nr=opts.chan_nr)
# this is executed only in successful case, as unsuccessful raises exception
self._cmd.rs.del_lchan(opts.chan_nr)
switch_chan_parser = argparse.ArgumentParser()
switch_chan_parser.add_argument(
'chan_nr', type=int, default=0, help='Channel Number')
@cmd2.with_argparser(switch_chan_parser)
def do_switch_channel(self, opts):
"""Switch currently active logical channel."""
self._cmd.lchan._select_pre(self._cmd)
self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
self._cmd.lchan._select_post(self._cmd)
self._cmd.update_prompt()
def do_status(self, opts):
"""Perform the STATUS command."""
fcp_dec = self._cmd.lchan.status()
self._cmd.poutput_json(fcp_dec)
suspend_uicc_parser = argparse.ArgumentParser()
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
help='Proposed minimum duration of suspension')
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
help='Proposed maximum duration of suspension')
# not ISO7816-4 but TS 102 221
@cmd2.with_argparser(suspend_uicc_parser)
def do_suspend_uicc(self, opts):
"""Perform the SUSPEND UICC command. Only supported on some UICC."""
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
max_len_secs=opts.max_duration_secs)
self._cmd.poutput(
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
class Proact(ProactiveHandler):
def receive_fetch(self, pcmd: ProactiveCommand):
@@ -913,7 +959,7 @@ class Proact(ProactiveHandler):
option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
@@ -931,6 +977,11 @@ adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', de
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
help='ADM PIN used for provisioning, as hex string (16 characters long)')
option_parser.add_argument("command", nargs='?',
help="A pySim-shell command that would optionally be executed at startup")
option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
help="Optional Arguments for command")
if __name__ == '__main__':
@@ -953,11 +1004,6 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts, proactive_handler = Proact())
if sl is None:
exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
# Create a card handler (for bulk provisioning)
if opts.card_handler_config:
@@ -972,7 +1018,7 @@ if __name__ == '__main__':
rs, card = init_card(sl)
app = PysimApp(card, rs, sl, ch, opts.script)
except:
print("Card initialization failed with an exception:")
print("Card initialization (%s) failed with an exception:" % str(sl))
print("---------------------8<---------------------")
traceback.print_exc()
print("---------------------8<---------------------")
@@ -981,7 +1027,9 @@ if __name__ == '__main__':
" it should also be noted that some readers may behave strangely when no card")
print(" is inserted.)")
print("")
app = PysimApp(card, None, sl, ch, opts.script)
if opts.script:
print("will not execute startup script due to card initialization errors!")
app = PysimApp(None, None, sl, ch)
# If the user supplies an ADM PIN at via commandline args authenticate
# immediately so that the user does not have to use the shell commands
@@ -990,8 +1038,11 @@ if __name__ == '__main__':
if not card:
print("Card error, cannot do ADM verification with supplied ADM pin now.")
try:
card.verify_adm(h2b(pin_adm))
card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
except Exception as e:
print(e)
app.cmdloop()
if opts.command:
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
else:
app.cmdloop()

View File

@@ -6,18 +6,19 @@ import argparse
from pprint import pprint as pp
from pySim.apdu import *
from pySim.filesystem import RuntimeState
from pySim.runtime import RuntimeState
from pySim.cards import UsimCard
from pySim.cards import UiccCardBase
from pySim.commands import SimCardCommands
from pySim.profile import CardProfile
from pySim.ts_102_221 import CardProfileUICCSIM
from pySim.ts_102_221 import CardProfileUICC
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
from pySim.transport import LinkBase
from pySim.apdu_source.gsmtap import GsmtapApduSource
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
@@ -35,18 +36,21 @@ ApduCommands = UiccApduCommands + UsimApduCommands #+ GpApduCommands
class DummySimLink(LinkBase):
"""A dummy implementation of the LinkBase abstract base class. Currently required
as the UsimCard doesn't work without SimCardCommands, which in turn require
as the UiccCardBase doesn't work without SimCardCommands, which in turn require
a LinkBase implementation talking to a card.
In the tracer, we don't actually talk to any card, so we simply drop everything
and claim it is successful.
The UsimCard / SimCardCommands should be refactored to make this obsolete later."""
The UiccCardBase / SimCardCommands should be refactored to make this obsolete later."""
def __init__(self, debug: bool = False, **kwargs):
super().__init__(**kwargs)
self._debug = debug
self._atr = h2i('3B9F96801F878031E073FE211B674A4C753034054BA9')
def __str__(self):
return "dummy"
def _send_apdu_raw(self, pdu):
#print("DummySimLink-apdu: %s" % pdu)
return [], '9000'
@@ -69,34 +73,49 @@ class DummySimLink(LinkBase):
class Tracer:
def __init__(self, **kwargs):
# we assume a generic SIM + UICC + USIM + ISIM card
profile = CardProfileUICCSIM()
# we assume a generic UICC profile; as all APDUs return 9000 in DummySimLink above,
# all CardProfileAddon (including SIM) will probe successful.
profile = CardProfileUICC()
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
scc = SimCardCommands(transport=DummySimLink())
card = UsimCard(scc)
card = UiccCardBase(scc)
self.rs = RuntimeState(card, profile)
# APDU Decoder
self.ad = ApduDecoder(ApduCommands)
# parameters
self.suppress_status = kwargs.get('suppress_status', True)
self.suppress_select = kwargs.get('suppress_select', True)
self.show_raw_apdu = kwargs.get('show_raw_apdu', False)
self.source = kwargs.get('source', None)
def format_capdu(self, inst: ApduCommand):
def format_capdu(self, apdu: Apdu, inst: ApduCommand):
"""Output a single decoded + processed ApduCommand."""
if self.show_raw_apdu:
print(apdu)
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id, inst.col_sw, inst.processed))
print("===============================")
def format_reset(self, apdu: CardReset):
"""Output a single decoded CardReset."""
print(apdu)
print("===============================")
def main(self):
"""Main loop of tracer: Iterates over all Apdu received from source."""
apdu_counter = 0
while True:
# obtain the next APDU from the source (blocking read)
apdu = self.source.read()
#print(apdu)
try:
apdu = self.source.read()
apdu_counter = apdu_counter + 1
except StopIteration:
print("%i APDUs parsed, stop iteration." % apdu_counter)
return 0
if isinstance(apdu, CardReset):
self.rs.reset()
self.format_reset(apdu)
continue
# ask ApduDecoder to look-up (INS,CLA) + instantiate an ApduCommand derived
@@ -110,34 +129,51 @@ class Tracer:
continue
if self.suppress_status and isinstance(inst, UiccStatus):
continue
#print(inst)
self.format_capdu(inst)
option_parser = argparse.ArgumentParser(prog='pySim-trace', description='Osmocom pySim high-level SIM card trace decoder',
self.format_capdu(apdu, inst)
option_parser = argparse.ArgumentParser(description='Osmocom pySim high-level SIM card trace decoder',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
global_group = option_parser.add_argument_group('General Options')
global_group.add_argument('--no-suppress-select', action='store_false', dest='suppress_select',
help="Don't suppress displaying SELECT APDUs")
help="""
Don't suppress displaying SELECT APDUs. We normally suppress them as they just clutter up
the output without giving any useful information. Any subsequent READ/UPDATE/... operations
on the selected file will log the file name most recently SELECTed.""")
global_group.add_argument('--no-suppress-status', action='store_false', dest='suppress_status',
help="Don't suppress displaying STATUS APDUs")
help="""
Don't suppress displaying STATUS APDUs. We normally suppress them as they don't provide any
information that was not already received in resposne to the most recent SEELCT.""")
global_group.add_argument('--show-raw-apdu', action='store_true', dest='show_raw_apdu',
help="""Show the raw APDU in addition to its parsed form.""")
subparsers = option_parser.add_subparsers(help='APDU Source', dest='source', required=True)
parser_gsmtap = subparsers.add_parser('gsmtap-udp', help='Live capture of GSMTAP-SIM on UDP port')
parser_gsmtap = subparsers.add_parser('gsmtap-udp', help="""
Read APDUs from live capture by receiving GSMTAP-SIM packets on specified UDP port.
Use this for live capture from SIMtrace2 or osmo-qcdiag.""")
parser_gsmtap.add_argument('-i', '--bind-ip', default='127.0.0.1',
help='Local IP address to which to bind the UDP port')
parser_gsmtap.add_argument('-p', '--bind-port', default=4729,
help='Local UDP port')
parser_gsmtap_pyshark_pcap = subparsers.add_parser('gsmtap-pyshark-pcap', help="""
Read APDUs from PCAP file containing GSMTAP (SIM APDU) communication; processed via pyshark.
Use this if you have recorded a PCAP file containing GSMTAP (SIM APDU) e.g. via tcpdump or
wireshark/tshark.""")
parser_gsmtap_pyshark_pcap.add_argument('-f', '--pcap-file', required=True,
help='Name of the PCAP[ng] file to be read')
parser_rspro_pyshark_pcap = subparsers.add_parser('rspro-pyshark-pcap', help="""
PCAP file containing RSPRO (osmo-remsim) communication; processed via pyshark.
Read APDUs from PCAP file containing RSPRO (osmo-remsim) communication; processed via pyshark.
REQUIRES OSMOCOM PATCHED WIRESHARK!""")
parser_rspro_pyshark_pcap.add_argument('-f', '--pcap-file', required=True,
help='Name of the PCAP[ng] file to be read')
parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
Live capture of RSPRO (osmo-remsim) communication; processed via pyshark.
Read APDUs from live capture of RSPRO (osmo-remsim) communication; processed via pyshark.
REQUIRES OSMOCOM PATCHED WIRESHARK!""")
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
help='Name of the network interface to capture on')
@@ -146,15 +182,18 @@ if __name__ == '__main__':
opts = option_parser.parse_args()
logger.info('Opening source %s...' % opts.source)
logger.info('Opening source %s...', opts.source)
if opts.source == 'gsmtap-udp':
s = GsmtapApduSource(opts.bind_ip, opts.bind_port)
elif opts.source == 'rspro-pyshark-pcap':
s = PysharkRsproPcap(opts.pcap_file)
elif opts.source == 'rspro-pyshark-live':
s = PysharkRsproLive(opts.interface)
elif opts.source == 'gsmtap-pyshark-pcap':
s = PysharkGsmtapPcap(opts.pcap_file)
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select)
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
show_raw_apdu=opts.show_raw_apdu)
logger.info('Entering main loop...')
tracer.main()

View File

@@ -1,4 +1,3 @@
# coding=utf-8
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
The File (and its classes) represent the structure / hierarchy
@@ -27,15 +26,16 @@ we already know in pySim about the filesystem structure, file encoding, etc.
import abc
from termcolor import colored
import typing
from typing import List, Dict, Optional
from termcolor import colored
from construct import *
from construct import Byte, GreedyBytes
from construct import Optional as COptional
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import RuntimeLchan, RuntimeState, lchan_nr_from_cla
from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
"""There are multiple levels of decode:
@@ -52,8 +52,8 @@ from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
class ApduCommandMeta(abc.ABCMeta):
"""A meta-class that we can use to set some class variables when declaring
a derived class of ApduCommand."""
def __new__(metacls, name, bases, namespace, **kwargs):
x = super().__new__(metacls, name, bases, namespace)
def __new__(mcs, name, bases, namespace, **kwargs):
x = super().__new__(mcs, name, bases, namespace)
x._name = namespace.get('name', kwargs.get('n', None))
x._ins = namespace.get('ins', kwargs.get('ins', None))
x._cla = namespace.get('cla', kwargs.get('cla', None))
@@ -135,7 +135,12 @@ class Apdu(Tpdu):
if callable(method):
return method()
# default case: only 9000 is success
return self.sw == b'\x90\x00'
if self.sw == b'\x90\x00':
return True
# This is not really a generic positive APDU SW but specific to UICC/SIM
if self.sw[0] == 0x91:
return True
return False
class ApduCommand(Apdu, metaclass=ApduCommandMeta):
@@ -182,44 +187,39 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
if apdu_case in [1, 2]:
# data is part of response
return cls(buffer[:5], buffer[5:])
elif apdu_case in [3, 4]:
if apdu_case in [3, 4]:
# data is part of command
lc = buffer[4]
return cls(buffer[:5+lc], buffer[5+lc:])
else:
raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
@property
def path(self) -> List[str]:
"""Return (if known) the path as list of files to the file on which this command operates."""
if self.file:
return self.file.fully_qualified_path()
else:
return []
return []
@property
def path_str(self) -> str:
"""Return (if known) the path as string to the file on which this command operates."""
if self.file:
return self.file.fully_qualified_path_str()
else:
return ''
return ''
@property
def col_sw(self) -> str:
"""Return the ansi-colorized status word. Green==OK, Red==Error"""
if self.successful:
return colored(b2h(self.sw), 'green')
else:
return colored(b2h(self.sw), 'red')
return colored(b2h(self.sw), 'red')
@property
def lchan_nr(self) -> int:
"""Logical channel number over which this ApduCommand was transmitted."""
if self.lchan:
return self.lchan.lchan_nr
else:
return lchan_nr_from_cla(self.cla)
return lchan_nr_from_cla(self.cla)
def __str__(self) -> str:
return '%02u %s(%s): %s' % (self.lchan_nr, type(self).__name__, self.path_str, self.to_dict())
@@ -231,7 +231,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
"""Fall-back function to be called if there is no derived-class-specific
process_global or process_on_lchan method. Uses information from APDU decode."""
self.processed = {}
if not 'p1' in self.cmd_dict:
if 'p1' not in self.cmd_dict:
self.processed = self.to_dict()
else:
self.processed['p1'] = self.cmd_dict['p1']
@@ -423,8 +423,7 @@ class TpduFilter(ApduHandler):
apdu = Apdu(icmd, tpdu.rsp)
if self.apdu_handler:
return self.apdu_handler.input(apdu)
else:
return Apdu(icmd, tpdu.rsp)
return Apdu(icmd, tpdu.rsp)
def input(self, cmd: bytes, rsp: bytes):
if isinstance(cmd, str):
@@ -443,4 +442,10 @@ class ApduDecoder(ApduHandler):
class CardReset:
pass
def __init__(self, atr: bytes):
self.atr = atr
def __str__(self):
if self.atr:
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
return '%s' % (type(self).__name__)

View File

@@ -17,11 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from typing import Optional, Dict
import logging
from construct import GreedyRange, Struct
from pySim.construct import *
from pySim.filesystem import *
from pySim.runtime import RuntimeLchan
from pySim.apdu import ApduCommand, ApduCommandSet
from typing import Optional, Dict, Tuple
from pySim.utils import i2h
logger = logging.getLogger(__name__)
@@ -76,7 +81,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
pass
# iterate to next element in path
continue
logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path])))
logger.warning('SELECT UNKNOWN FID %s (%s)', file_hex, '/'.join([b2h(x) for x in path]))
elif mode == 'df_ef_or_mf_by_file_id':
if len(self.cmd_data) != 2:
raise ValueError('Expecting a 2-byte FID')
@@ -90,7 +95,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
#print("\tSELECT %s FAILED" % sels[file_hex])
pass
else:
logger.warning('SELECT UNKNOWN FID %s' % (file_hex))
logger.warning('SELECT UNKNOWN FID %s', file_hex)
elif mode == 'df_name':
# Select by AID (can be sub-string!)
aid = self.cmd_dict['body']
@@ -101,8 +106,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
lchan.selected_file = lchan.selected_adf
#print("\tSELECT AID %s" % adf)
else:
logger.warning('SELECT UNKNOWN AID %s' % aid)
pass
logger.warning('SELECT UNKNOWN AID %s', aid)
else:
raise ValueError('Select Mode %s not implemented' % mode)
# decode the SELECT response
@@ -201,7 +205,7 @@ class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['0X', '4X', '6X'])
return b2h(self.rsp_data)
method = getattr(self.file, 'decode_record_bin', None)
if self.successful and callable(method):
return method(self.rsp_data)
return method(self.rsp_data, self.cmd_dict['record_number'])
# TS 102 221 Section 11.1.6
class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6X']):
@@ -217,7 +221,7 @@ class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6
return b2h(self.cmd_data)
method = getattr(self.file, 'decode_record_bin', None)
if self.successful and callable(method):
return method(self.cmd_data)
return method(self.cmd_data, self.cmd_dict['record_number'])
# TS 102 221 Section 11.1.7
class SearchRecord(ApduCommand, n='SEARCH RECORD', ins=0xA2, cla=['0X', '4X', '6X']):
@@ -289,12 +293,9 @@ class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
@staticmethod
def _pin_is_success(sw):
if sw[0] == 0x63:
return True
else:
return False
return bool(sw[0] == 0x63)
def process_on_lchan(self, lchan: RuntimeLchan):
def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -306,7 +307,7 @@ class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -318,7 +319,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -329,7 +330,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -341,7 +342,7 @@ class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X'])
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -393,12 +394,13 @@ class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X',
manage_channel = rs.get_lchan_by_cla(self.cla)
manage_channel.add_lchan(created_channel_nr)
self.col_id = '%02u' % created_channel_nr
elif mode == 'close_channel':
closed_channel_nr = self.cmd_dict['p2']
return {'mode': mode, 'created_channel': created_channel_nr }
if mode == 'close_channel':
closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
rs.del_lchan(closed_channel_nr)
self.col_id = '%02u' % closed_channel_nr
else:
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
return {'mode': mode, 'closed_channel': closed_channel_nr }
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
# TS 102 221 Section 11.1.18
class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
@@ -416,13 +418,13 @@ class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=
p2 = hdr[3]
if p1 & 0x7 == 0: # retrieve UICC Endpoints
return 2
elif p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
if p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
p2_cmd = p2 >> 5
if p2_cmd in [0,2,4]: # command data
return 3
elif p2_cmd in [1,3,5]: # response data
if p2_cmd in [1,3,5]: # response data
return 2
elif p1 & 0xf == 4: # terminate secure channel SA
if p1 & 0xf == 4: # terminate secure channel SA
return 3
raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
@@ -433,8 +435,7 @@ class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6
p1 = hdr[2]
if p1 & 0x04:
return 3
else:
return 2
return 2
# TS 102 221 Section 11.1.22
class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
@@ -487,7 +488,7 @@ class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'E
elif self.p2 & 0xdf == 0x40:
c['mode'] = 'retransmit_previous_block'
else:
logger.warning('%s: invalid P2=%02x' % (self, self.p2))
logger.warning('%s: invalid P2=%02x', self, self.p2)
return c
def _decode_cmd(self):

View File

@@ -9,12 +9,12 @@ APDU commands of 3GPP TS 31.102 V16.6.0
"""
from typing import Dict
from construct import *
from construct import BitStruct, Enum, BitsInteger, Int8ub, Bytes, this, Struct, If, Switch, Const
from construct import Optional as COptional
from pySim.filesystem import *
from pySim.construct import *
from pySim.ts_31_102 import SUCI_TlvDataObject
from pySim.apdu import ApduCommand, ApduCommandSet
# Copyright (C) 2022 Harald Welte <laforge@osmocom.org>
@@ -35,8 +35,6 @@ from pySim.apdu import ApduCommand, ApduCommandSet
# Mapping between USIM Service Number and its description
from pySim.apdu import ApduCommand, ApduCommandSet
# TS 31.102 Section 7.1
class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
_apdu_case = 4
@@ -108,7 +106,7 @@ class UsimAuthenticateOdd(ApduCommand, n='AUTHENTICATE', ins=0x89, cla=['0X', '4
class UsimGetIdentity(ApduCommand, n='GET IDENTITY', ins=0x78, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
_construct_p2 = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1),
'identity_context'/Enum(BitsInteger(7), suci=1))
'identity_context'/Enum(BitsInteger(7), suci=1, suci_5g_nswo=2))
_tlv_rsp = SUCI_TlvDataObject
ApduCommands = ApduCommandSet('TS 31.102', cmds=[UsimAuthenticateEven, UsimAuthenticateOdd,

View File

@@ -14,7 +14,6 @@ class ApduSource(abc.ABC):
@abc.abstractmethod
def read_packet(self) -> PacketType:
"""Read one packet from the source."""
pass
def read(self) -> Union[Apdu, CardReset]:
"""Main function to call by the user: Blocking read, returns Apdu or CardReset."""
@@ -31,5 +30,5 @@ class ApduSource(abc.ABC):
elif isinstance(r, CardReset):
apdu = r
else:
ValueError('Unknown read_packet() return %s' % r)
raise ValueError('Unknown read_packet() return %s' % r)
return apdu

View File

@@ -16,12 +16,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pySim.gsmtap import GsmtapMessage, GsmtapSource
from . import ApduSource, PacketType, CardReset
from pySim.gsmtap import GsmtapSource
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
from . import ApduSource, PacketType, CardReset
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
class GsmtapApduSource(ApduSource):
@@ -41,16 +43,16 @@ class GsmtapApduSource(ApduSource):
self.gsmtap = GsmtapSource(bind_ip, bind_port)
def read_packet(self) -> PacketType:
gsmtap_msg, addr = self.gsmtap.read_packet()
gsmtap_msg, _addr = self.gsmtap.read_packet()
if gsmtap_msg['type'] != 'sim':
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
sub_type = gsmtap_msg['sub_type']
if sub_type == 'apdu':
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
elif sub_type == 'atr':
if sub_type == 'atr':
# card has been reset
return CardReset()
elif sub_type in ['pps_req', 'pps_rsp']:
return CardReset(gsmtap_msg['body'])
if sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:

View File

@@ -0,0 +1,88 @@
# coding=utf-8
# (C) 2022 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 logging
from typing import Tuple
import pyshark
from pySim.utils import h2b
from pySim.gsmtap import GsmtapMessage
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
from . import ApduSource, PacketType, CardReset
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
logger = logging.getLogger(__name__)
class _PysharkGsmtap(ApduSource):
"""APDU Source [provider] base class for reading GSMTAP SIM APDU via tshark."""
def __init__(self, pyshark_inst):
self.pyshark = pyshark_inst
self.bank_id = None
self.bank_slot = None
self.cmd_tpdu = None
super().__init__()
def read_packet(self) -> PacketType:
p = self.pyshark.next()
return self._parse_packet(p)
def _set_or_verify_bank_slot(self, bsl: Tuple[int, int]):
"""Keep track of the bank:slot to make sure we don't mix traces of multiple cards"""
if not self.bank_id:
self.bank_id = bsl[0]
self.bank_slot = bsl[1]
else:
if self.bank_id != bsl[0] or self.bank_slot != bsl[1]:
raise ValueError('Received data for unexpected B(%u:%u)' % (bsl[0], bsl[1]))
def _parse_packet(self, p) -> PacketType:
udp_layer = p['udp']
udp_payload_hex = udp_layer.get_field('payload').replace(':','')
gsmtap = GsmtapMessage(h2b(udp_payload_hex))
gsmtap_msg = gsmtap.decode()
if gsmtap_msg['type'] != 'sim':
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
sub_type = gsmtap_msg['sub_type']
if sub_type == 'apdu':
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
if sub_type == 'atr':
# card has been reset
return CardReset(gsmtap_msg['body'])
if sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:
raise ValueError('Unsupported GSMTAP-SIM sub-type %s' % sub_type)
class PysharkGsmtapPcap(_PysharkGsmtap):
"""APDU Source [provider] class for reading GSMTAP from a PCAP
file via pyshark, which in turn uses tshark (part of wireshark).
"""
def __init__(self, pcap_filename):
"""
Args:
pcap_filename: File name of the pcap file to be opened
"""
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', use_json=True, keep_packets=False)
super().__init__(pyshark_inst)

View File

@@ -16,13 +16,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import logging
from pprint import pprint as pp
from typing import Tuple
import pyshark
from pySim.utils import h2b, b2h
from pySim.utils import h2b
from pySim.apdu import Tpdu
from . import ApduSource, PacketType, CardReset
@@ -89,7 +87,7 @@ class _PysharkRspro(ApduSource):
bsl = self.get_bank_slot(bank_slot)
self._set_or_verify_bank_slot(bsl)
data = modem2card.get_field('data').replace(':','')
logger.debug("C(%u:%u) -> B(%u:%u): %s" % (csl[0], csl[1], bsl[0], bsl[1], data))
logger.debug("C(%u:%u) -> B(%u:%u): %s", csl[0], csl[1], bsl[0], bsl[1], data)
# store the CMD portion until the RSP portion arrives later
self.cmd_tpdu = h2b(data)
elif msg_type == '13': # tpduCardToModem
@@ -101,7 +99,7 @@ class _PysharkRspro(ApduSource):
bsl = self.get_bank_slot(bank_slot)
self._set_or_verify_bank_slot(bsl)
data = card2modem.get_field('data').replace(':','')
logger.debug("C(%u:%u) <- B(%u:%u): %s" % (csl[0], csl[1], bsl[0], bsl[1], data))
logger.debug("C(%u:%u) <- B(%u:%u): %s", csl[0], csl[1], bsl[0], bsl[1], data)
rsp_tpdu = h2b(data)
if self.cmd_tpdu:
# combine this R-TPDU with the C-TPDU we saw earlier
@@ -117,7 +115,8 @@ class _PysharkRspro(ApduSource):
vccPresent, resetActive, clkActive = self.get_pstatus(slot_pstatus)
if vccPresent and clkActive and not resetActive:
logger.debug("RESET")
return CardReset()
#TODO: extract ATR from RSPRO message and use it here
return CardReset(None)
else:
print("Unhandled msg type %s: %s" % (msg_type, rspro_msg))

109
pySim/app.py Normal file
View File

@@ -0,0 +1,109 @@
# (C) 2021-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/>.
from typing import Tuple
from pySim.transport import LinkBase
from pySim.commands import SimCardCommands
from pySim.filesystem import CardModel, CardApplication
from pySim.cards import card_detect, SimCardBase, UiccCardBase
from pySim.runtime import RuntimeState
from pySim.profile import CardProfile
from pySim.cdma_ruim import CardProfileRUIM
from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import all_subclasses
# we need to import this module so that the SysmocomSJA2 sub-class of
# CardModel is created, which will add the ATR-based matching and
# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
import pySim.sysmocom_sja2
# we need to import these modules so that the various sub-classes of
# CardProfile are created, which will be used in init_card() to iterate
# over all known CardProfile sub-classes.
import pySim.ts_31_102
import pySim.ts_31_103
import pySim.ts_31_104
import pySim.ara_m
import pySim.global_platform
import pySim.euicc
def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
"""
Detect card in reader and setup card profile and runtime state. This
function must be called at least once on startup. The card and runtime
state object (rs) is required for all pySim-shell commands.
"""
# Create command layer
scc = SimCardCommands(transport=sl)
# Wait up to three seconds for a card in reader and try to detect
# the card type.
print("Waiting for card...")
sl.wait_for_card(3)
generic_card = False
card = card_detect(scc)
if card is None:
print("Warning: Could not detect card type - assuming a generic card type...")
card = SimCardBase(scc)
generic_card = True
profile = CardProfile.pick(scc)
if profile is None:
# It is not an unrecoverable error in case profile detection fails. It
# just means that pySim was unable to recognize the card profile. This
# may happen in particular with unprovisioned cards that do not have
# any files on them yet.
print("Unsupported card type!")
return None, card
# ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
# references, however card manufactures may still decide to pick an
# arbitrary key reference. In case we run on a generic card class that is
# detected as an UICC, we will pick the key reference that is officially
# specified.
if generic_card and isinstance(profile, CardProfileUICC):
card._adm_chv_num = 0x0A
print("Info: Card is of type: %s" % str(profile))
# FIXME: this shouldn't really be here but somewhere else/more generic.
# We cannot do it within pySim/profile.py as that would create circular
# dependencies between the individual profiles and profile.py.
if isinstance(profile, CardProfileUICC):
for app_cls in all_subclasses(CardApplication):
# skip any intermediary sub-classes such as CardApplicationSD
if hasattr(app_cls, '_' + app_cls.__name__ + '__intermediate'):
continue
profile.add_application(app_cls())
# We have chosen SimCard() above, but we now know it actually is an UICC
# so it's safe to assume it supports USIM application (which we're adding above).
# IF we don't do this, we will have a SimCard but try USIM specific commands like
# the update_ust method (see https://osmocom.org/issues/6055)
if generic_card:
card = UiccCardBase(scc)
# Create runtime state with card profile
rs = RuntimeState(card, profile)
CardModel.apply_matching_models(scc, rs)
# inform the transport that we can do context-specific SW interpretation
sl.set_sw_interpreter(rs)
return rs, card

View File

@@ -26,11 +26,13 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
#
from construct import *
from construct import GreedyBytes, GreedyString, Struct, Enum, Int8ub, Int16ub
from construct import Optional as COptional
from pySim.construct import *
from pySim.filesystem import *
from pySim.tlv import *
from pySim.utils import Hexstr
import pySim.global_platform
# various BER-TLV encoded Data Objects (DOs)
@@ -67,11 +69,10 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if do[0] == 0x00:
self.decoded = {'generic_access_rule': 'never'}
return self.decoded
elif do[0] == 0x01:
if do[0] == 0x01:
self.decoded = {'generic_access_rule': 'always'}
return self.decoded
else:
return ValueError('Invalid 1-byte generic APDU access rule')
return ValueError('Invalid 1-byte generic APDU access rule')
else:
if len(do) % 8:
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
@@ -87,10 +88,9 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if 'generic_access_rule' in self.decoded:
if self.decoded['generic_access_rule'] == 'never':
return b'\x00'
elif self.decoded['generic_access_rule'] == 'always':
if self.decoded['generic_access_rule'] == 'always':
return b'\x01'
else:
return ValueError('Invalid 1-byte generic APDU access rule')
return ValueError('Invalid 1-byte generic APDU access rule')
else:
if not 'apdu_filter' in self.decoded:
return ValueError('Invalid APDU AR DO')
@@ -115,6 +115,7 @@ class NfcArDO(BER_TLV_IE, tag=0xd1):
class PermArDO(BER_TLV_IE, tag=0xdb):
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
# based on Table 6-8 of GlobalPlatform Device API Access Control v1.0
_construct = Struct('permissions'/HexAdapter(Bytes(8)))
@@ -259,6 +260,9 @@ class ADF_ARAM(CardADF):
files = []
self.add_files(files)
def decode_select_response(self, data_hex):
return pySim.global_platform.decode_select_response(data_hex)
@staticmethod
def xceive_apdu_tlv(tp, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
"""Transceive an APDU with the card, transparently encoding the command data from TLV
@@ -272,14 +276,13 @@ class ADF_ARAM(CardADF):
cmd_do_enc = b''
cmd_do_len = 0
c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
(data, sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
(data, _sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
if data:
if resp_cls:
resp_do = resp_cls()
resp_do.from_tlv(h2b(data))
return resp_do
else:
return data
return data
else:
return None
@@ -295,24 +298,21 @@ class ADF_ARAM(CardADF):
@staticmethod
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
cmd_do = DeviceConfigDO()
cmd_do.from_dict([{'DeviceInterfaceVersionDO': {
cmd_do.from_dict([{'device_interface_version_do': {
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
def __init(self):
super().__init__()
def do_aram_get_all(self, opts):
def do_aram_get_all(self, _opts):
"""GET DATA [All] on the ARA-M Applet"""
res_do = ADF_ARAM.get_all(self._cmd.card._scc._tp)
res_do = ADF_ARAM.get_all(self._cmd.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
def do_aram_get_config(self, opts):
def do_aram_get_config(self, _opts):
"""Perform GET DATA [Config] on the ARA-M Applet: Tell it our version and retrieve its version."""
res_do = ADF_ARAM.get_config(self._cmd.card._scc._tp)
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -348,39 +348,39 @@ class ADF_ARAM(CardADF):
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a (new) access rule."""
# REF
ref_do_content = []
if opts.aid:
ref_do_content += [{'AidRefDO': opts.aid}]
if opts.aid is not None:
ref_do_content += [{'aid_ref_do': opts.aid}]
elif opts.aid_empty:
ref_do_content += [{'AidRefEmptyDO': None}]
ref_do_content += [{'DevAppIdRefDO': opts.device_app_id}]
ref_do_content += [{'aid_ref_empty_do': None}]
ref_do_content += [{'dev_app_id_ref_do': opts.device_app_id}]
if opts.pkg_ref:
ref_do_content += [{'PkgRefDO': opts.pkg_ref}]
ref_do_content += [{'pkg_ref_do': {'package_name_string': opts.pkg_ref}}]
# AR
ar_do_content = []
if opts.apdu_never:
ar_do_content += [{'ApduArDO': {'generic_access_rule': 'never'}}]
ar_do_content += [{'apdu_ar_od': {'generic_access_rule': 'never'}}]
elif opts.apdu_always:
ar_do_content += [{'ApduArDO': {'generic_access_rule': 'always'}}]
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
elif opts.apdu_filter:
# TODO: multiple filters
ar_do_content += [{'ApduArDO': {'apdu_filter': [opts.apdu_filter]}}]
ar_do_content += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
if opts.nfc_always:
ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'always'}}]
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
elif opts.nfc_never:
ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'never'}}]
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'never'}}]
if opts.android_permissions:
ar_do_content += [{'PermArDO': {'permissions': opts.android_permissions}}]
d = [{'RefArDO': [{'RefDO': ref_do_content}, {'ArDO': ar_do_content}]}]
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
csrado = CommandStoreRefArDO()
csrado.from_dict(d)
res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, csrado)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, csrado)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
def do_aram_delete_all(self, opts):
def do_aram_delete_all(self, _opts):
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
deldo = CommandDelete()
res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, deldo)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, deldo)
if res_do:
self._cmd.poutput_json(res_do.to_dict())

View File

@@ -24,12 +24,11 @@ there are also automatic card feeders.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pySim.transport import LinkBase
import subprocess
import sys
import yaml
from pySim.transport import LinkBase
class CardHandlerBase:
"""Abstract base class representing a mechanism for card insertion/removal."""
@@ -97,7 +96,7 @@ class CardHandlerAuto(CardHandlerBase):
print("Card handler Config-file: " + str(config_file))
with open(config_file) as cfg:
self.cmds = yaml.load(cfg, Loader=yaml.FullLoader)
self.verbose = (self.cmds.get('verbose') == True)
self.verbose = self.cmds.get('verbose') is True
def __print_outout(self, out):
print("")

View File

@@ -54,11 +54,11 @@ class CardKeyProvider(abc.ABC):
dictionary of {field, value} strings for each requested field from 'fields'
"""
for f in fields:
if (f not in self.VALID_FIELD_NAMES):
if f not in self.VALID_FIELD_NAMES:
raise ValueError("Requested field name '%s' is not a valid field name, valid field names are: %s" %
(f, str(self.VALID_FIELD_NAMES)))
if (key not in self.VALID_FIELD_NAMES):
if key not in self.VALID_FIELD_NAMES:
raise ValueError("Key field name '%s' is not a valid field name, valid field names are: %s" %
(key, str(self.VALID_FIELD_NAMES)))

File diff suppressed because it is too large Load Diff

View File

@@ -18,14 +18,14 @@ as described in 3GPP TS 31.111."""
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from bidict import bidict
from typing import List
from pySim.utils import b2h, h2b, dec_xplmn_w_act
from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from pySim.construct import BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger
from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil
from bidict import bidict
from construct import Int8ub, Int16ub, Byte, Bytes, BitsInteger
from construct import Struct, Enum, BitStruct, this
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from pySim.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
from pySim.utils import b2h, dec_xplmn_w_act
# Tag values as per TS 101 220 Table 7.23
@@ -583,11 +583,11 @@ class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
# TS 31.111 Section 8.90
class PlmnWactList(COMPR_TLV_IE, tag=0xF2):
def _from_bytes(self, x):
def _from_bytes(self, do: bytes):
r = []
i = 0
while i < len(x):
r.append(dec_xplmn_w_act(b2h(x[i:i+5])))
while i < len(do):
r.append(dec_xplmn_w_act(b2h(do[i:i+5])))
i += 5
return r
@@ -597,7 +597,7 @@ class ContactlessFunctionalityState(COMPR_TLV_IE, tag=0xD4):
# TS 31.111 Section 8.91
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
_construct = Struct('mcc_mnc'/BcdAdapter(Bytes(3)),
_construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
'lac'/HexAdapter(Bytes(2)),
'rac'/Int8ub)
@@ -645,7 +645,7 @@ class GeographicalLocationParameters(COMPR_TLV_IE, tag=0xF6):
# TS 31.111 Section 8.97
class PlmnList(COMPR_TLV_IE, tag=0xF9):
_construct = GreedyRange('mcc_mnc'/HexAdapter(Bytes(3)))
_construct = GreedyRange('mcc_mnc'/PlmnAdapter(Bytes(3)))
# TS 102 223 Section 8.98
class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
@@ -978,8 +978,8 @@ class ProactiveCommandBase(BER_TLV_IE, tag=0xD0, nested=[CommandDetails]):
for c in self.children:
if type(c).__name__ == 'CommandDetails':
return c
else:
return None
else:
return None
class ProactiveCommand(TLV_IE_Collection,
nested=[Refresh, MoreTime, PollInterval, PollingOff, SetUpEventList, SetUpCall,
@@ -997,7 +997,7 @@ class ProactiveCommand(TLV_IE_Collection,
more difficult than any normal TLV IE Collection, because the content of one of the IEs defines the
definitions of all the other IEs. So we first need to find the CommandDetails, and then parse according
to the command type indicated in that IE data."""
def from_bytes(self, binary: bytes) -> List[TLV_IE]:
def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
# do a first parse step to get the CommandDetails
pcmd = ProactiveCommandBase()
pcmd.from_tlv(binary)
@@ -1007,7 +1007,7 @@ class ProactiveCommand(TLV_IE_Collection,
if cmd_type in self.members_by_tag:
cls = self.members_by_tag[cmd_type]
inst = cls()
dec, remainder = inst.from_tlv(binary)
_dec, remainder = inst.from_tlv(binary)
self.decoded = inst
else:
self.decoded = pcmd
@@ -1019,7 +1019,7 @@ class ProactiveCommand(TLV_IE_Collection,
def to_dict(self):
return self.decoded.to_dict()
def to_bytes(self):
def to_bytes(self, context: dict = {}):
return self.decoded.to_tlv()

206
pySim/cdma_ruim.py Normal file
View File

@@ -0,0 +1,206 @@
# coding=utf-8
"""R-UIM (Removable User Identity Module) card profile (see 3GPP2 C.S0023-D)
(C) 2023 by Vadim Yanitskiy <fixeria@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 enum
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile import match_ruim
from pySim.profile import CardProfile, CardProfileAddon
from pySim.ts_51_011 import CardProfileSIM
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
from pySim.ts_51_011 import EF_ServiceTable
from pySim.construct import *
# Mapping between CDMA Service Number and its description
EF_CST_map = {
1 : 'CHV disable function',
2 : 'Abbreviated Dialing Numbers (ADN)',
3 : 'Fixed Dialing Numbers (FDN)',
4 : 'Short Message Storage (SMS)',
5 : 'HRPD',
6 : 'Enhanced Phone Book',
7 : 'Multi Media Domain (MMD)',
8 : 'SF_EUIMID-based EUIMID',
9 : 'MEID Support',
10 : 'Extension1',
11 : 'Extension2',
12 : 'SMS Parameters',
13 : 'Last Number Dialled (LND)',
14 : 'Service Category Program for BC-SMS',
15 : 'Messaging and 3GPD Extensions',
16 : 'Root Certificates',
17 : 'CDMA Home Service Provider Name',
18 : 'Service Dialing Numbers (SDN)',
19 : 'Extension3',
20 : '3GPD-SIP',
21 : 'WAP Browser',
22 : 'Java',
23 : 'Reserved for CDG',
24 : 'Reserved for CDG',
25 : 'Data Download via SMS Broadcast',
26 : 'Data Download via SMS-PP',
27 : 'Menu Selection',
28 : 'Call Control',
29 : 'Proactive R-UIM',
30 : 'AKA',
31 : 'IPv6',
32 : 'RFU',
33 : 'RFU',
34 : 'RFU',
35 : 'RFU',
36 : 'RFU',
37 : 'RFU',
38 : '3GPD-MIP',
39 : 'BCMCS',
40 : 'Multimedia Messaging Service (MMS)',
41 : 'Extension 8',
42 : 'MMS User Connectivity Parameters',
43 : 'Application Authentication',
44 : 'Group Identifier Level 1',
45 : 'Group Identifier Level 2',
46 : 'De-Personalization Control Keys',
47 : 'Cooperative Network List',
}
######################################################################
# DF.CDMA
######################################################################
class EF_SPN(TransparentEF):
'''3.4.31 CDMA Home Service Provider Name'''
_test_de_encode = [
( "010801536b796c696e6b204e57ffffffffffffffffffffffffffffffffffffffffffff",
{ 'rfu1' : 0, 'show_in_hsa' : True, 'rfu2' : 0,
'char_encoding' : 8, 'lang_ind' : 1, 'spn' : 'Skylink NW' } ),
]
def __init__(self, fid='6f41', sfid=None, name='EF.SPN',
desc='Service Provider Name', size=(35, 35), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = BitStruct(
# Byte 1: Display Condition
'rfu1'/BitsRFU(7),
'show_in_hsa'/Flag,
# Byte 2: Character Encoding
'rfu2'/BitsRFU(3),
'char_encoding'/BitsInteger(5), # see C.R1001-G
# Byte 3: Language Indicator
'lang_ind'/BitsInteger(8), # see C.R1001-G
# Bytes 4-35: Service Provider Name
'spn'/Bytewise(GsmString(32))
)
class EF_AD(TransparentEF):
'''3.4.33 Administrative Data'''
_test_de_encode = [
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
]
_test_no_pad = True
class OP_MODE(enum.IntEnum):
normal = 0x00
type_approval = 0x80
normal_and_specific_facilities = 0x01
type_approval_and_specific_facilities = 0x81
maintenance_off_line = 0x02
cell_test = 0x04
def __init__(self, fid='6f43', sfid=None, name='EF.AD',
desc='Service Provider Name', size=(3, None), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct(
# Byte 1: Display Condition
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information
'additional_info'/HexAdapter(Bytes(2)),
# Bytes 4..: RFU
'rfu'/HexAdapter(GreedyBytesRFU),
)
class EF_SMS(LinFixedEF):
'''3.4.27 Short Messages'''
def __init__(self, fid='6f3c', sfid=None, name='EF.SMS', desc='Short messages', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(2, 255), **kwargs)
self._construct = Struct(
# Byte 1: Status
'status'/BitStruct(
'rfu87'/BitsRFU(2),
'protection'/Flag,
'rfu54'/BitsRFU(2),
'status'/FlagsEnum(BitsInteger(2), read=0, to_be_read=1, sent=2, to_be_sent=3),
'used'/Flag,
),
# Byte 2: Length
'length'/Int8ub,
# Bytes 3..: SMS Transport Layer Message
'tpdu'/Bytes(lambda ctx: ctx.length if ctx.status.used else 0),
)
class DF_CDMA(CardDF):
def __init__(self):
super().__init__(fid='7f25', name='DF.CDMA',
desc='CDMA related files (3GPP2 C.S0023-D)')
files = [
# TODO: lots of other files
EF_ServiceTable('6f32', None, 'EF.CST',
'CDMA Service Table', table=EF_CST_map, size=(5, 16)),
EF_SPN(),
EF_AD(),
EF_SMS(),
]
self.add_files(files)
class CardProfileRUIM(CardProfile):
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
ORDER = 2
def __init__(self):
super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
@staticmethod
def decode_select_response(data_hex: str) -> object:
# TODO: Response parameters/data in case of DF_CDMA (section 2.6)
return CardProfileSIM.decode_select_response(data_hex)
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_ruim(scc)
class AddonRUIM(CardProfileAddon):
"""An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""
def __init__(self):
files = [
DF_CDMA()
]
super().__init__('RUIM', desc='CDMA RUIM', files_in_mf=files)
def probe(self, card: 'CardBase') -> bool:
return card.file_exists(self.files_in_mf[0].fid)

View File

@@ -5,7 +5,7 @@
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2010-2021 Harald Welte <laforge@gnumonks.org>
# Copyright (C) 2010-2024 Harald Welte <laforge@gnumonks.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
@@ -21,20 +21,178 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from construct import *
from pySim.construct import LV
from pySim.utils import rpad, b2h, h2b, sw_match, bertlv_encode_len, Hexstr, h2i, str_sanitize, expand_hex
from pySim.exceptions import SwMatchError
from typing import List, Tuple
import typing # construct also has a Union, so we do typing.Union below
from construct import Construct, Struct, Const, Select
from construct import Optional as COptional
from pySim.construct import LV, filter_dict
from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, h2i, i2h, str_sanitize, expand_hex, SwMatchstr
from pySim.utils import Hexstr, SwHexstr, ResTuple
from pySim.exceptions import SwMatchError
from pySim.transport import LinkBase
# A path can be either just a FID or a list of FID
Path = typing.Union[Hexstr, List[Hexstr]]
def lchan_nr_to_cla(cla: int, lchan_nr: int) -> int:
"""Embed a logical channel number into the CLA byte."""
# TS 102 221 10.1.1 Coding of Class Byte
if lchan_nr < 4:
# standard logical channel number
if cla >> 4 in [0x0, 0xA, 0x8]:
return (cla & 0xFC) | (lchan_nr & 3)
else:
raise ValueError('Undefined how to use CLA %2X with logical channel %u' % (cla, lchan_nr))
elif lchan_nr < 16:
# extended logical channel number
if cla >> 6 in [1, 3]:
return (cla & 0xF0) | ((lchan_nr - 4) & 0x0F)
else:
raise ValueError('Undefined how to use CLA %2X with logical channel %u' % (cla, lchan_nr))
else:
raise ValueError('logical channel outside of range 0 .. 15')
def cla_with_lchan(cla_byte: Hexstr, lchan_nr: int) -> Hexstr:
"""Embed a logical channel number into the hex-string encoded CLA value."""
cla_int = h2i(cla_byte)[0]
return i2h([lchan_nr_to_cla(cla_int, lchan_nr)])
class SimCardCommands:
def __init__(self, transport):
"""Class providing methods for various card-specific commands such as SELECT, READ BINARY, etc.
Historically one instance exists below CardBase, but with the introduction of multiple logical
channels there can be multiple instances. The lchan number will then be patched into the CLA
byte by the respective instance. """
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
self._tp = transport
self.cla_byte = "a0"
self._cla_byte = None
self.sel_ctrl = "0000"
self.lchan_nr = lchan_nr
# invokes the setter below
self.cla_byte = "a0"
self.scp = None # Secure Channel Protocol
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
ret = SimCardCommands(transport = self._tp, lchan_nr = lchan_nr)
ret.cla_byte = self._cla_byte
ret.sel_ctrl = self.sel_ctrl
return ret
@property
def cla_byte(self) -> Hexstr:
"""Return the (cached) patched default CLA byte for this card."""
return self._cla4lchan
@property
def max_cmd_len(self) -> int:
"""Maximum length of the command apdu data section. Depends on secure channel protocol used."""
if self.scp:
return 255 - self.scp.overhead
else:
return 255
@cla_byte.setter
def cla_byte(self, new_val: Hexstr):
"""Set the (raw, without lchan) default CLA value for this card."""
self._cla_byte = new_val
# compute cached result
self._cla4lchan = cla_with_lchan(self._cla_byte, self.lchan_nr)
def cla4lchan(self, cla: Hexstr) -> Hexstr:
"""Compute the lchan-patched value of the given CLA value. If no CLA
value is provided as argument, the lchan-patched version of the SimCardCommands._cla_byte
value is used. Most commands will use the latter, while some wish to override it and
can pass it as argument here."""
if not cla:
# return cached result to avoid re-computing this over and over again
return self._cla4lchan
else:
return cla_with_lchan(cla, self.lchan_nr)
def send_apdu(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
else:
return self._tp.send_apdu(pdu)
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
digits using a '?' to add some ambiguity if needed.
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
else:
return self._tp.send_apdu_checksw(pdu, sw)
def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,
cmd_data: Hexstr, resp_constr: Construct) -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
cla : string (in hex) ISO 7816 class byte
ins : string (in hex) ISO 7816 instruction byte
p1 : string (in hex) ISO 7116 Parameter 1 byte
p2 : string (in hex) ISO 7116 Parameter 2 byte
cmd_cosntr : defining how to generate binary APDU command data
cmd_data : command data passed to cmd_constr
resp_cosntr : defining how to decode binary APDU response data
Returns:
Tuple of (decoded_data, sw)
"""
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
p3 = i2h([len(cmd)])
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
(data, sw) = self.send_apdu(pdu)
if data:
# filter the resulting dict to avoid '_io' members inside
rsp = filter_dict(resp_constr.parse(h2b(data)))
else:
rsp = None
return (rsp, sw)
def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
sw_exp: SwMatchstr="9000") -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
cla : string (in hex) ISO 7816 class byte
ins : string (in hex) ISO 7816 instruction byte
p1 : string (in hex) ISO 7116 Parameter 1 byte
p2 : string (in hex) ISO 7116 Parameter 2 byte
cmd_cosntr : defining how to generate binary APDU command data
cmd_data : command data passed to cmd_constr
resp_cosntr : defining how to decode binary APDU response data
exp_sw : string (in hex) of status word (ex. "9000")
Returns:
Tuple of (decoded_data, sw)
"""
(rsp, sw) = self.send_apdu_constr(cla, ins,
p1, p2, cmd_constr, cmd_data, resp_constr)
if not sw_match(sw, sw_exp):
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
return (rsp, sw)
# Extract a single FCP item from TLV
def __parse_fcp(self, fcp):
def __parse_fcp(self, fcp: Hexstr):
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
# DF or ADF
from pytlv.TLV import TLV
@@ -88,11 +246,11 @@ class SimCardCommands:
else:
return int(r[-1][4:8], 16)
def get_atr(self) -> str:
def get_atr(self) -> Hexstr:
"""Return the ATR of the currently inserted card."""
return self._tp.get_atr()
def try_select_path(self, dir_list):
def try_select_path(self, dir_list: List[Hexstr]) -> List[ResTuple]:
""" Try to select a specified path
Args:
@@ -100,17 +258,16 @@ class SimCardCommands:
"""
rv = []
if type(dir_list) is not list:
if not isinstance(dir_list, list):
dir_list = [dir_list]
for i in dir_list:
data, sw = self._tp.send_apdu(
self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
rv.append((data, sw))
if sw != '9000':
return rv
return rv
def select_path(self, dir_list):
def select_path(self, dir_list: Path) -> List[Hexstr]:
"""Execute SELECT for an entire list/path of FIDs.
Args:
@@ -120,27 +277,27 @@ class SimCardCommands:
list of return values (FCP in hex encoding) for each element of the path
"""
rv = []
if type(dir_list) is not list:
if not isinstance(dir_list, list):
dir_list = [dir_list]
for i in dir_list:
data, sw = self.select_file(i)
data, _sw = self.select_file(i)
rv.append(data)
return rv
def select_file(self, fid: str):
def select_file(self, fid: Hexstr) -> ResTuple:
"""Execute SELECT a given file by FID.
Args:
fid : file identifier as hex string
"""
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
def select_parent_df(self):
def select_parent_df(self) -> ResTuple:
"""Execute SELECT to switch to the parent DF """
return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
return self.send_apdu_checksw(self.cla_byte + "a4030400")
def select_adf(self, aid: str):
def select_adf(self, aid: Hexstr) -> ResTuple:
"""Execute SELECT a given Applicaiton ADF.
Args:
@@ -148,9 +305,9 @@ class SimCardCommands:
"""
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
def read_binary(self, ef, length: int = None, offset: int = 0):
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
"""Execute READD BINARY.
Args:
@@ -169,19 +326,33 @@ class SimCardCommands:
total_data = ''
chunk_offset = 0
while chunk_offset < length:
chunk_len = min(255, length-chunk_offset)
chunk_len = min(self.max_cmd_len, length-chunk_offset)
pdu = self.cla_byte + \
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
try:
data, sw = self._tp.send_apdu_checksw(pdu)
data, sw = self.send_apdu_checksw(pdu)
except Exception as e:
raise ValueError('%s, failed to read (offset %d)' %
(str_sanitize(str(e)), offset))
(str_sanitize(str(e)), offset)) from e
total_data += data
chunk_offset += chunk_len
return total_data, sw
def update_binary(self, ef, data: str, offset: int = 0, verify: bool = False, conserve: bool = False):
def __verify_binary(self, ef, data: str, offset: int = 0):
"""Verify contents of transparent EF.
Args:
ef : string or list of strings indicating name or path of transparent EF
data : hex string of expected data
offset : byte offset in file from which to start verifying
"""
res = self.read_binary(ef, len(data) // 2, offset)
if res[0].lower() != data.lower():
raise ValueError('Binary verification failed (expected %s, got %s)' % (
data.lower(), res[0].lower()))
def update_binary(self, ef: Path, data: Hexstr, offset: int = 0, verify: bool = False,
conserve: bool = False) -> ResTuple:
"""Execute UPDATE BINARY.
Args:
@@ -198,44 +369,38 @@ class SimCardCommands:
# Save write cycles by reading+comparing before write
if conserve:
data_current, sw = self.read_binary(ef, data_length, offset)
if data_current == data:
return None, sw
try:
data_current, sw = self.read_binary(ef, data_length, offset)
if data_current == data:
return None, sw
except Exception:
# cannot read data. This is not a fatal error, as reading is just done to
# conserve the amount of smart card writes. The access conditions of the file
# may well permit us to UPDATE but not permit us to READ. So let's ignore
# any such exception during READ.
pass
self.select_path(ef)
total_data = ''
chunk_offset = 0
while chunk_offset < data_length:
chunk_len = min(255, data_length - chunk_offset)
chunk_len = min(self.max_cmd_len, data_length - chunk_offset)
# chunk_offset is bytes, but data slicing is hex chars, so we need to multiply by 2
pdu = self.cla_byte + \
'd6%04x%02x' % (offset + chunk_offset, chunk_len) + \
data[chunk_offset*2: (chunk_offset+chunk_len)*2]
try:
chunk_data, chunk_sw = self._tp.send_apdu_checksw(pdu)
chunk_data, chunk_sw = self.send_apdu_checksw(pdu)
except Exception as e:
raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' %
(str_sanitize(str(e)), chunk_offset, chunk_len))
(str_sanitize(str(e)), chunk_offset, chunk_len)) from e
total_data += data
chunk_offset += chunk_len
if verify:
self.verify_binary(ef, data, offset)
self.__verify_binary(ef, data, offset)
return total_data, chunk_sw
def verify_binary(self, ef, data: str, offset: int = 0):
"""Verify contents of transparent EF.
Args:
ef : string or list of strings indicating name or path of transparent EF
data : hex string of expected data
offset : byte offset in file from which to start verifying
"""
res = self.read_binary(ef, len(data) // 2, offset)
if res[0].lower() != data.lower():
raise ValueError('Binary verification failed (expected %s, got %s)' % (
data.lower(), res[0].lower()))
def read_record(self, ef, rec_no: int):
def read_record(self, ef: Path, rec_no: int) -> ResTuple:
"""Execute READ RECORD.
Args:
@@ -245,51 +410,9 @@ class SimCardCommands:
r = self.select_path(ef)
rec_length = self.__record_len(r)
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
return self._tp.send_apdu_checksw(pdu)
return self.send_apdu_checksw(pdu)
def update_record(self, ef, rec_no: int, data: str, force_len: bool = False, verify: bool = False,
conserve: bool = False):
"""Execute UPDATE RECORD.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
rec_no : record number to read
data : hex string of data to be written
force_len : enforce record length by using the actual data length
verify : verify data by re-reading the record
conserve : read record and compare it with data, skip write on match
"""
res = self.select_path(ef)
rec_length = self.__record_len(res)
data = expand_hex(data, rec_length)
if force_len:
# enforce the record length by the actual length of the given data input
rec_length = len(data) // 2
else:
# make sure the input data is padded to the record length using 0xFF.
# In cases where the input data exceed we throw an exception.
if (len(data) // 2 > rec_length):
raise ValueError('Data length exceeds record length (expected max %d, got %d)' % (
rec_length, len(data) // 2))
elif (len(data) // 2 < rec_length):
data = rpad(data, rec_length * 2)
# Save write cycles by reading+comparing before write
if conserve:
data_current, sw = self.read_record(ef, rec_no)
data_current = data_current[0:rec_length*2]
if data_current == data:
return None, sw
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
res = self._tp.send_apdu_checksw(pdu)
if verify:
self.verify_record(ef, rec_no, data)
return res
def verify_record(self, ef, rec_no: int, data: str):
def __verify_record(self, ef: Path, rec_no: int, data: str):
"""Verify record against given data
Args:
@@ -302,7 +425,60 @@ class SimCardCommands:
raise ValueError('Record verification failed (expected %s, got %s)' % (
data.lower(), res[0].lower()))
def record_size(self, ef):
def update_record(self, ef: Path, rec_no: int, data: Hexstr, force_len: bool = False,
verify: bool = False, conserve: bool = False, leftpad: bool = False) -> ResTuple:
"""Execute UPDATE RECORD.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
rec_no : record number to read
data : hex string of data to be written
force_len : enforce record length by using the actual data length
verify : verify data by re-reading the record
conserve : read record and compare it with data, skip write on match
leftpad : apply 0xff padding from the left instead from the right side.
"""
res = self.select_path(ef)
rec_length = self.__record_len(res)
data = expand_hex(data, rec_length)
if force_len:
# enforce the record length by the actual length of the given data input
rec_length = len(data) // 2
else:
# make sure the input data is padded to the record length using 0xFF.
# In cases where the input data exceed we throw an exception.
if len(data) // 2 > rec_length:
raise ValueError('Data length exceeds record length (expected max %d, got %d)' % (
rec_length, len(data) // 2))
elif len(data) // 2 < rec_length:
if leftpad:
data = lpad(data, rec_length * 2)
else:
data = rpad(data, rec_length * 2)
# Save write cycles by reading+comparing before write
if conserve:
try:
data_current, sw = self.read_record(ef, rec_no)
data_current = data_current[0:rec_length*2]
if data_current == data:
return None, sw
except Exception:
# cannot read data. This is not a fatal error, as reading is just done to
# conserve the amount of smart card writes. The access conditions of the file
# may well permit us to UPDATE but not permit us to READ. So let's ignore
# any such exception during READ.
pass
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
res = self.send_apdu_checksw(pdu)
if verify:
self.__verify_record(ef, rec_no, data)
return res
def record_size(self, ef: Path) -> int:
"""Determine the record size of given file.
Args:
@@ -311,7 +487,7 @@ class SimCardCommands:
r = self.select_path(ef)
return self.__record_len(r)
def record_count(self, ef):
def record_count(self, ef: Path) -> int:
"""Determine the number of records in given file.
Args:
@@ -320,7 +496,7 @@ class SimCardCommands:
r = self.select_path(ef)
return self.__len(r) // self.__record_len(r)
def binary_size(self, ef):
def binary_size(self, ef: Path) -> int:
"""Determine the size of given transparent file.
Args:
@@ -330,14 +506,14 @@ class SimCardCommands:
return self.__len(r)
# TS 102 221 Section 11.3.1 low-level helper
def _retrieve_data(self, tag: int, first: bool = True):
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
if first:
pdu = '80cb008001%02x' % (tag)
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
else:
pdu = '80cb000000'
return self._tp.send_apdu_checksw(pdu)
pdu = self.cla4lchan('80') + 'cb000000'
return self.send_apdu_checksw(pdu)
def retrieve_data(self, ef, tag: int):
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
Args
@@ -351,23 +527,23 @@ class SimCardCommands:
# retrieve first block
data, sw = self._retrieve_data(tag, first=True)
total_data += data
while sw == '62f1' or sw == '62f2':
while sw in ['62f1', '62f2']:
data, sw = self._retrieve_data(tag, first=False)
total_data += data
return total_data, sw
# TS 102 221 Section 11.3.2 low-level helper
def _set_data(self, data: str, first: bool = True):
def _set_data(self, data: Hexstr, first: bool = True) -> ResTuple:
if first:
p1 = 0x80
else:
p1 = 0x00
if isinstance(data, bytes) or isinstance(data, bytearray):
if isinstance(data, (bytes, bytearray)):
data = b2h(data)
pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data)
return self._tp.send_apdu_checksw(pdu)
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
return self.send_apdu_checksw(pdu)
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False):
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
"""Execute SET DATA.
Args
@@ -392,13 +568,13 @@ class SimCardCommands:
total_len = len(tlv_bin)
remaining = tlv_bin
while len(remaining) > 0:
fragment = remaining[:255]
fragment = remaining[:self.max_cmd_len]
rdata, sw = self._set_data(fragment, first=first)
first = False
remaining = remaining[255:]
remaining = remaining[self.max_cmd_len:]
return rdata, sw
def run_gsm(self, rand: str):
def run_gsm(self, rand: Hexstr) -> ResTuple:
"""Execute RUN GSM ALGORITHM.
Args:
@@ -407,9 +583,9 @@ class SimCardCommands:
if len(rand) != 32:
raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20'])
return self._tp.send_apdu(self.cla_byte + '88000010' + rand)
return self.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
def authenticate(self, rand: str, autn: str, context='3g'):
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
"""Execute AUTHENTICATE (USIM/ISIM).
Args:
@@ -418,10 +594,9 @@ class SimCardCommands:
context : 16 byte random data ('3g' or 'gsm')
"""
# 3GPP TS 31.102 Section 7.1.2.1
AuthCmd3G = Struct('rand'/LV, 'autn'/Optional(LV))
AuthCmd3G = Struct('rand'/LV, 'autn'/COptional(LV))
AuthResp3GSyncFail = Struct(Const(b'\xDC'), 'auts'/LV)
AuthResp3GSuccess = Struct(
Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/Optional(LV))
AuthResp3GSuccess = Struct(Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/COptional(LV))
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
# build parameters
cmd_data = {'rand': rand, 'autn': autn}
@@ -429,7 +604,7 @@ class SimCardCommands:
p2 = '81'
elif context == 'gsm':
p2 = '80'
(data, sw) = self._tp.send_apdu_constr_checksw(
(data, sw) = self.send_apdu_constr_checksw(
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
if 'auts' in data:
ret = {'synchronisation_failure': data}
@@ -437,43 +612,47 @@ class SimCardCommands:
ret = {'successful_3g_authentication': data}
return (ret, sw)
def status(self):
def status(self) -> ResTuple:
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
return self._tp.send_apdu_checksw('80F20000ff')
return self.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
def deactivate_file(self):
def deactivate_file(self) -> ResTuple:
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
return self._tp.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
return self.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
def activate_file(self, fid):
def activate_file(self, fid: Hexstr) -> ResTuple:
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
Args:
fid : file identifier as hex string
"""
return self._tp.send_apdu_checksw(self.cla_byte + '44000002' + fid)
return self.send_apdu_checksw(self.cla_byte + '44000002' + fid)
def create_file(self, payload: Hexstr):
def create_file(self, payload: Hexstr) -> ResTuple:
"""Execute CREEATE FILE command as per TS 102 222 Section 6.3"""
return self._tp.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
def delete_file(self, fid):
def resize_file(self, payload: Hexstr) -> ResTuple:
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
return self.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
def delete_file(self, fid: Hexstr) -> ResTuple:
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
return self._tp.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
return self.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
def terminate_df(self, fid):
def terminate_df(self, fid: Hexstr) -> ResTuple:
"""Execute TERMINATE DF command as per TS 102 222 Section 6.7"""
return self._tp.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
return self.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
def terminate_ef(self, fid):
def terminate_ef(self, fid: Hexstr) -> ResTuple:
"""Execute TERMINATE EF command as per TS 102 222 Section 6.8"""
return self._tp.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
return self.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
def terminate_card_usage(self):
def terminate_card_usage(self) -> ResTuple:
"""Execute TERMINATE CARD USAGE command as per TS 102 222 Section 6.9"""
return self._tp.send_apdu_checksw(self.cla_byte + 'fe000000')
return self.send_apdu_checksw(self.cla_byte + 'fe000000')
def manage_channel(self, mode='open', lchan_nr=0):
def manage_channel(self, mode: str = 'open', lchan_nr: int =0) -> ResTuple:
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
Args:
@@ -485,20 +664,20 @@ class SimCardCommands:
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
return self._tp.send_apdu_checksw(pdu)
return self.send_apdu_checksw(pdu)
def reset_card(self):
def reset_card(self) -> Hexstr:
"""Physically reset the card"""
return self._tp.reset_card()
def _chv_process_sw(self, op_name, chv_no, pin_code, sw):
def _chv_process_sw(self, op_name: str, chv_no: int, pin_code: Hexstr, sw: SwHexstr):
if sw_match(sw, '63cx'):
raise RuntimeError('Failed to %s chv_no 0x%02X with code 0x%s, %i tries left.' %
(op_name, chv_no, b2h(pin_code).upper(), int(sw[3])))
elif (sw != '9000'):
if sw != '9000':
raise SwMatchError(sw, '9000')
def verify_chv(self, chv_no: int, code: str):
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
"""Verify a given CHV (Card Holder Verification == PIN)
Args:
@@ -506,8 +685,7 @@ class SimCardCommands:
code : chv code as hex string
"""
fc = rpad(b2h(code), 16)
data, sw = self._tp.send_apdu(
self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
data, sw = self.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('verify', chv_no, code, sw)
return (data, sw)
@@ -520,12 +698,11 @@ class SimCardCommands:
pin_code : new chv code as hex string
"""
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(
self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
data, sw = self.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('unblock', chv_no, pin_code, sw)
return (data, sw)
def change_chv(self, chv_no: int, pin_code: str, new_pin_code: str):
def change_chv(self, chv_no: int, pin_code: Hexstr, new_pin_code: Hexstr) -> ResTuple:
"""Change a given CHV (Card Holder Verification == PIN)
Args:
@@ -534,12 +711,11 @@ class SimCardCommands:
new_pin_code : new chv code as hex string
"""
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
data, sw = self._tp.send_apdu(
self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
data, sw = self.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('change', chv_no, pin_code, sw)
return (data, sw)
def disable_chv(self, chv_no: int, pin_code: str):
def disable_chv(self, chv_no: int, pin_code: Hexstr) -> ResTuple:
"""Disable a given CHV (Card Holder Verification == PIN)
Args:
@@ -548,12 +724,11 @@ class SimCardCommands:
new_pin_code : new chv code as hex string
"""
fc = rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(
self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
data, sw = self.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('disable', chv_no, pin_code, sw)
return (data, sw)
def enable_chv(self, chv_no: int, pin_code: str):
def enable_chv(self, chv_no: int, pin_code: Hexstr) -> ResTuple:
"""Enable a given CHV (Card Holder Verification == PIN)
Args:
@@ -561,31 +736,30 @@ class SimCardCommands:
pin_code : chv code as hex string
"""
fc = rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(
self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
data, sw = self.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('enable', chv_no, pin_code, sw)
return (data, sw)
def envelope(self, payload: str):
def envelope(self, payload: Hexstr) -> ResTuple:
"""Send one ENVELOPE command to the SIM
Args:
payload : payload as hex string
"""
return self._tp.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
def terminal_profile(self, payload: str):
def terminal_profile(self, payload: Hexstr) -> ResTuple:
"""Send TERMINAL PROFILE to card
Args:
payload : payload as hex string
"""
data_length = len(payload) // 2
data, sw = self._tp.send_apdu(('80100000%02x' % data_length) + payload)
data, sw = self.send_apdu(('80100000%02x' % data_length) + payload)
return (data, sw)
# ETSI TS 102 221 11.1.22
def suspend_uicc(self, min_len_secs: int = 60, max_len_secs: int = 43200):
def suspend_uicc(self, min_len_secs: int = 60, max_len_secs: int = 43200) -> Tuple[int, Hexstr, SwHexstr]:
"""Send SUSPEND UICC to the card.
Args:
@@ -595,38 +769,48 @@ class SimCardCommands:
def encode_duration(secs: int) -> Hexstr:
if secs >= 10*24*60*60:
return '04%02x' % (secs // (10*24*60*60))
elif secs >= 24*60*60:
if secs >= 24*60*60:
return '03%02x' % (secs // (24*60*60))
elif secs >= 60*60:
if secs >= 60*60:
return '02%02x' % (secs // (60*60))
elif secs >= 60:
if secs >= 60:
return '01%02x' % (secs // 60)
else:
return '00%02x' % secs
return '00%02x' % secs
def decode_duration(enc: Hexstr) -> int:
time_unit = enc[:2]
length = h2i(enc[2:4])
length = h2i(enc[2:4])[0]
if time_unit == '04':
return length * 10*24*60*60
elif time_unit == '03':
if time_unit == '03':
return length * 24*60*60
elif time_unit == '02':
if time_unit == '02':
return length * 60*60
elif time_unit == '01':
if time_unit == '01':
return length * 60
elif time_unit == '00':
if time_unit == '00':
return length
else:
raise ValueError('Time unit must be 0x00..0x04')
raise ValueError('Time unit must be 0x00..0x04')
min_dur_enc = encode_duration(min_len_secs)
max_dur_enc = encode_duration(max_len_secs)
data, sw = self._tp.send_apdu_checksw(
'8076000004' + min_dur_enc + max_dur_enc)
data, sw = self.send_apdu_checksw('8076000004' + min_dur_enc + max_dur_enc)
negotiated_duration_secs = decode_duration(data[:4])
resume_token = data[4:]
return (negotiated_duration_secs, resume_token, sw)
def get_data(self, tag: int, cla: int = 0x00):
data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
# ETSI TS 102 221 11.1.22
def resume_uicc(self, token: Hexstr) -> ResTuple:
"""Send SUSPEND UICC (resume) to the card."""
if len(h2b(token)) != 8:
raise ValueError("Token must be 8 bytes long")
data, sw = self.send_apdu_checksw('8076010008' + token)
return (data, sw)
def get_data(self, tag: int, cla: int = 0x00):
data, sw = self.send_apdu('%02xca%04x00' % (cla, tag))
return (data, sw)
# TS 31.102 Section 7.5.2
def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
data, sw = self.send_apdu_checksw('807800%02x00' % (context))
return (data, sw)

View File

@@ -1,13 +1,20 @@
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
"""Utility code related to the integration of the 'construct' declarative parser."""
import typing
from construct import *
from construct.core import evaluate, BitwisableString
from construct.lib import integertypes
from pySim.utils import b2h, h2b, swap_nibbles
import codecs
import ipaddress
import gsm0338
"""Utility code related to the integration of the 'construct' declarative parser."""
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
from construct import Adapter, Prefixed, Int8ub, GreedyBytes, Default, Flag, Byte, Construct, Enum
from construct import BitsInteger, BitStruct, Bytes, StreamError, stream_read_entire, stream_write
from construct import SizeofError, IntegerError, swapbytes
from construct.core import evaluate
from construct.lib import integertypes
from pySim.utils import b2h, h2b, swap_nibbles
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
#
@@ -34,6 +41,199 @@ class HexAdapter(Adapter):
def _encode(self, obj, context, path):
return h2b(obj)
class Utf8Adapter(Adapter):
"""convert a bytes() type that contains utf8 encoded text to human readable text."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
return codecs.decode(obj, "utf-8")
def _encode(self, obj, context, path):
return codecs.encode(obj, "utf-8")
class GsmOrUcs2Adapter(Adapter):
"""Try to encode into a GSM 03.38 string; if that fails, fall back to UCS-2 as described
in TS 102 221 Annex A."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
# one of the magic bytes of TS 102 221 Annex A
if obj[0] in [0x80, 0x81, 0x82]:
ad = Ucs2Adapter(GreedyBytes)
else:
ad = GsmString(GreedyBytes)
return ad._decode(obj, context, path)
def _encode(self, obj, context, path):
# first try GSM 03.38; then fall back to TS 102 221 Annex A UCS-2
try:
ad = GsmString(GreedyBytes)
return ad._encode(obj, context, path)
except:
ad = Ucs2Adapter(GreedyBytes)
return ad._encode(obj, context, path)
class Ucs2Adapter(Adapter):
"""convert a bytes() type that contains UCS2 encoded characters encoded as defined in TS 102 221
Annex A to normal python string representation (and back)."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
if obj[0] == 0x80:
# TS 102 221 Annex A Variant 1
return codecs.decode(obj[1:], 'utf_16_be')
elif obj[0] == 0x81:
# TS 102 221 Annex A Variant 2
out = ""
# second byte contains a value indicating the number of characters
num_of_chars = obj[1]
# the third byte contains an 8 bit number which defines bits 15 to 8 of a 16 bit base
# pointer, where bit 16 is set to zero, and bits 7 to 1 are also set to zero. These
# sixteen bits constitute a base pointer to a "half-page" in the UCS2 code space
base_ptr = obj[2] << 7
for ch in obj[3:3+num_of_chars]:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
# GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, then
# the remaining seven bits are an offset value added to the 16 bit base pointer
# defined earlier, and the resultant 16 bit value is a UCS2 code point
if ch & 0x80:
codepoint = (ch & 0x7f) + base_ptr
out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
else:
out += codecs.decode(bytes([ch]), 'gsm03.38')
return out
elif obj[0] == 0x82:
# TS 102 221 Annex A Variant 3
out = ""
# second byte contains a value indicating the number of characters
num_of_chars = obj[1]
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space, for use with some or all of the
# remaining bytes in the string
base_ptr = obj[2] << 8 | obj[3]
for ch in obj[4:4+num_of_chars]:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
# GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, the
# remaining seven bits are an offset value added to the base pointer defined in
# bytes three and four, and the resultant 16 bit value is a UCS2 code point, else: #
# GSM default alphabet
if ch & 0x80:
codepoint = (ch & 0x7f) + base_ptr
out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
else:
out += codecs.decode(bytes([ch]), 'gsm03.38')
return out
else:
raise ValueError('First byte of TS 102 221 UCS-2 must be 0x80, 0x81 or 0x82')
def _encode(self, obj, context, path):
def encodable_in_gsm338(instr: str) -> bool:
"""Determine if given input string is encode-ale in gsm03.38."""
try:
# TODO: figure out if/how we can constrain to default alphabet. The gsm0338
# library seems to include the spanish lock/shift table
codecs.encode(instr, 'gsm03.38')
except ValueError:
return False
return True
def codepoints_not_in_gsm338(instr: str) -> typing.List[int]:
"""Return an integer list of UCS2 codepoints for all characters of 'inster'
which are not representable in the GSM 03.38 default alphabet."""
codepoint_list = []
for c in instr:
if encodable_in_gsm338(c):
continue
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
codepoint_list.append(c_codepoint)
return codepoint_list
def diff_between_min_and_max_of_list(inlst: typing.List) -> int:
return max(inlst) - min(inlst)
def encodable_in_variant2(instr: str) -> bool:
codepoint_prefix = None
for c in instr:
if encodable_in_gsm338(c):
continue
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
if c_codepoint >= 0x8000:
return False
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
else:
if c_prefix != codepoint_prefix:
return False
return True
def encodable_in_variant3(instr: str) -> bool:
codepoint_list = codepoints_not_in_gsm338(instr)
# compute delta between max and min; check if it's encodable in 7 bits
if diff_between_min_and_max_of_list(codepoint_list) >= 0x80:
return False
return True
def _encode_variant1(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 1"""
return b'\x80' + codecs.encode(instr, 'utf_16_be')
def _encode_variant2(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 2"""
codepoint_prefix = None
# second byte contains a value indicating the number of characters
hdr = b'\x81' + len(instr).to_bytes(1, byteorder='big')
chars = b''
for c in instr:
try:
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
assert codepoint_prefix == c_prefix
enc = (0x80 + (c_codepoint & 0x7f)).to_bytes(1, byteorder='big')
chars += enc
if codepoint_prefix is None:
codepoint_prefix = 0
return hdr + codepoint_prefix.to_bytes(1, byteorder='big') + chars
def _encode_variant3(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 3"""
# second byte contains a value indicating the number of characters
hdr = b'\x82' + len(instr).to_bytes(1, byteorder='big')
chars = b''
codepoint_list = codepoints_not_in_gsm338(instr)
codepoint_base = min(codepoint_list)
for c in instr:
try:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a GSM
# Default # Alphabet character
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
# if bit 8 of the byte is set to one, the remaining seven bits are an offset
# value added to the base pointer defined in bytes three and four, and the
# resultant 16 bit value is a UCS2 code point
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_codepoint_delta = c_codepoint - codepoint_base
assert c_codepoint_delta < 0x80
enc = (0x80 + c_codepoint_delta).to_bytes(1, byteorder='big')
chars += enc
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space
return hdr + codepoint_base.to_bytes(2, byteorder='big') + chars
if encodable_in_variant2(obj):
return _encode_variant2(obj)
elif encodable_in_variant3(obj):
return _encode_variant3(obj)
else:
return _encode_variant1(obj)
class BcdAdapter(Adapter):
"""convert a bytes() type to a string of BCD nibbles."""
@@ -44,6 +244,23 @@ class BcdAdapter(Adapter):
def _encode(self, obj, context, path):
return h2b(swap_nibbles(obj))
class PlmnAdapter(BcdAdapter):
"""convert a bytes(3) type to BCD string like 262-02 or 262-002."""
def _decode(self, obj, context, path):
bcd = super()._decode(obj, context, path)
if bcd[3] == 'f':
return '-'.join([bcd[:3], bcd[4:]])
else:
return '-'.join([bcd[:3], bcd[3:]])
def _encode(self, obj, context, path):
l = obj.split('-')
if len(l[1]) == 2:
bcd = l[0] + 'f' + l[1]
else:
bcd = l[0] + l[1]
return super()._encode(bcd, context, path)
class InvertAdapter(Adapter):
"""inverse logic (false->true, true->false)."""
@staticmethod
@@ -52,9 +269,9 @@ class InvertAdapter(Adapter):
# skip all private entries
if k.startswith('_'):
continue
if v == False:
if v is False:
obj[k] = True
elif v == True:
elif v is True:
obj[k] = False
return obj
@@ -66,26 +283,49 @@ class InvertAdapter(Adapter):
class Rpad(Adapter):
"""
Encoder appends padding bytes (b'\\xff') up to target size.
Decoder removes trailing padding bytes.
Encoder appends padding bytes (b'\\xff') or characters up to target size.
Decoder removes trailing padding bytes/characters.
Parameters:
subcon: Subconstruct as defined by construct library
pattern: set padding pattern (default: b'\\xff')
num_per_byte: number of 'elements' per byte. E.g. for hex nibbles: 2
"""
def __init__(self, subcon, pattern=b'\xff'):
def __init__(self, subcon, pattern=b'\xff', num_per_byte=1):
super().__init__(subcon)
self.pattern = pattern
self.num_per_byte = num_per_byte
def _decode(self, obj, context, path):
return obj.rstrip(self.pattern)
def _encode(self, obj, context, path):
if len(obj) > self.sizeof():
target_size = self.sizeof() * self.num_per_byte
if len(obj) > target_size:
raise SizeofError("Input ({}) exceeds target size ({})".format(
len(obj), self.sizeof()))
return obj + self.pattern * (self.sizeof() - len(obj))
len(obj), target_size))
return obj + self.pattern * (target_size - len(obj))
class MultiplyAdapter(Adapter):
"""
Decoder multiplies by multiplicator
Encoder divides by multiplicator
Parameters:
subcon: Subconstruct as defined by construct library
multiplier: Multiplier to apply to raw encoded value
"""
def __init__(self, subcon, multiplicator):
super().__init__(subcon)
self.multiplicator = multiplicator
def _decode(self, obj, context, path):
return obj * 8
def _encode(self, obj, context, path):
return obj // 8
class GsmStringAdapter(Adapter):
@@ -102,6 +342,63 @@ class GsmStringAdapter(Adapter):
def _encode(self, obj, context, path):
return obj.encode(self.codec, self.err)
class Ipv4Adapter(Adapter):
"""
Encoder converts from 4 bytes to string representation (A.B.C.D).
Decoder converts from string representation (A.B.C.D) to four bytes.
"""
def _decode(self, obj, context, path):
ia = ipaddress.IPv4Address(obj)
return ia.compressed
def _encode(self, obj, context, path):
ia = ipaddress.IPv4Address(obj)
return ia.packed
class Ipv6Adapter(Adapter):
"""
Encoder converts from 16 bytes to string representation.
Decoder converts from string representation to 16 bytes.
"""
def _decode(self, obj, context, path):
ia = ipaddress.IPv6Address(obj)
return ia.compressed
def _encode(self, obj, context, path):
ia = ipaddress.IPv6Address(obj)
return ia.packed
class StripTrailerAdapter(Adapter):
"""
Encoder removes all trailing bytes matching the default_value
Decoder pads input data up to total_length with default_value
This is used in constellations like "FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), ..."
where you have a bit-mask that may have 1, 2 or 3 bytes, depending on whether or not any
of the LSBs are actually set.
"""
def __init__(self, subcon, total_length:int, default_value=b'\x00', min_len=1):
super().__init__(subcon)
assert len(default_value) == 1
self.total_length = total_length
self.default_value = default_value
self.min_len = min_len
def _decode(self, obj, context, path):
assert isinstance(obj, bytes)
# pad with suppressed/missing bytes
if len(obj) < self.total_length:
obj += self.default_value * (self.total_length - len(obj))
return int.from_bytes(obj, 'big')
def _encode(self, obj, context, path):
assert isinstance(obj, int)
obj = obj.to_bytes(self.total_length, 'big')
# remove trailing bytes if they are zero
while len(obj) > self.min_len and obj[-1] == self.default_value[0]:
obj = obj[:-1]
return obj
def filter_dict(d, exclude_prefix='_'):
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
@@ -111,20 +408,20 @@ def filter_dict(d, exclude_prefix='_'):
for (key, value) in d.items():
if key.startswith(exclude_prefix):
continue
if type(value) is dict:
if isinstance(value, dict):
res[key] = filter_dict(value)
else:
res[key] = value
return res
def normalize_construct(c):
def normalize_construct(c, exclude_prefix: str = '_'):
"""Convert a construct specific type to a related base type, mostly useful
so we can serialize it."""
# we need to include the filter_dict as we otherwise get elements like this
# in the dict: '_io': <_io.BytesIO object at 0x7fdb64e05860> which we cannot json-serialize
c = filter_dict(c)
if isinstance(c, Container) or isinstance(c, dict):
c = filter_dict(c, exclude_prefix)
if isinstance(c, (Container, dict)):
r = {k: normalize_construct(v) for (k, v) in c.items()}
elif isinstance(c, ListContainer):
r = [normalize_construct(x) for x in c]
@@ -137,13 +434,25 @@ def normalize_construct(c):
return r
def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_'):
def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_', context: dict = {}):
"""Helper function to wrap around normalize_construct() and filter_dict()."""
if not length:
length = len(raw_bin_data)
parsed = c.parse(raw_bin_data, total_len=length)
return normalize_construct(parsed)
try:
parsed = c.parse(raw_bin_data, total_len=length, **context)
except StreamError as e:
# if the input is all-ff, this means the content is undefined. Let's avoid passing StreamError
# exceptions in those situations (which might occur if a length field 0xff is 255 but then there's
# actually less bytes in the remainder of the file.
if all(v == 0xff for v in raw_bin_data):
return None
else:
raise e
return normalize_construct(parsed, exclude_prefix)
def build_construct(c, decoded_data, context: dict = {}):
"""Helper function to handle total_len."""
return c.build(decoded_data, total_len=None, **context)
# here we collect some shared / common definitions of data types
LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
@@ -206,6 +515,20 @@ def GsmString(n):
'''
return GsmStringAdapter(Rpad(Bytes(n), pattern=b'\xff'), codec='gsm03.38')
def GsmOrUcs2String(n):
'''
GSM 03.38 or UCS-2 (TS 102 221 Annex A) encoded byte string of fixed length n.
Encoder appends padding bytes (b'\\xff') to maintain
length. Decoder removes those trailing bytes.
Exceptions are raised for invalid characters
and length excess.
Parameters:
n (Integer): Fixed length of the encoded byte string
'''
return GsmOrUcs2Adapter(Rpad(Bytes(n), pattern=b'\xff'))
class GreedyInteger(Construct):
"""A variable-length integer implementation, think of combining GrredyBytes with BytesInteger."""
def __init__(self, signed=False, swapped=False, minlen=0):
@@ -238,8 +561,7 @@ class GreedyInteger(Construct):
# round up to the minimum number
# of bytes we anticipate
if nbytes < minlen:
nbytes = minlen
nbytes = max(nbytes, minlen)
return nbytes
@@ -250,7 +572,7 @@ class GreedyInteger(Construct):
try:
data = obj.to_bytes(length, byteorder='big', signed=self.signed)
except ValueError as e:
raise IntegerError(str(e), path=path)
raise IntegerError(str(e), path=path) from e
if evaluate(self.swapped, context):
data = swapbytes(data)
stream_write(stream, data, length, path)

93
pySim/esim/__init__.py Normal file
View File

@@ -0,0 +1,93 @@
import sys
from typing import Optional
from importlib import resources
def compile_asn1_subdir(subdir_name:str):
"""Helper function that compiles ASN.1 syntax from all files within given subdir"""
import asn1tools
asn_txt = ''
__ver = sys.version_info
if (__ver.major, __ver.minor) >= (3, 9):
for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir():
asn_txt += i.read_text()
asn_txt += "\n"
#else:
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
return asn1tools.compile_string(asn_txt, codec='der')
# SGP.22 section 4.1 Activation Code
class ActivationCode:
def __init__(self, hostname:str, token:str, oid: Optional[str] = None, cc_required: Optional[bool] = False):
if '$' in hostname:
raise ValueError('$ sign not permitted in hostname')
self.hostname = hostname
if '$' in token:
raise ValueError('$ sign not permitted in token')
self.token = token
# TODO: validate OID
self.oid = oid
self.cc_required = cc_required
# only format 1 is specified and supported here
self.format = 1
@staticmethod
def decode_str(ac: str) -> dict:
if ac[0] != '1':
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
ac_elements = ac.split('$')
d = {
'oid': None,
'cc_required': False,
}
d['format'] = ac_elements.pop(0)
d['hostname'] = ac_elements.pop(0)
d['token'] = ac_elements.pop(0)
if len(ac_elements):
oid = ac_elements.pop(0)
if oid != '':
d['oid'] = oid
if len(ac_elements):
ccr = ac_elements.pop(0)
if ccr == '1':
d['cc_required'] = True
return d
@classmethod
def from_string(cls, ac: str) -> 'ActivationCode':
"""Create new instance from SGP.22 section 4.1 string representation."""
d = cls.decode_str(ac)
return cls(d['hostname'], d['token'], d['oid'], d['cc_required'])
def to_string(self, for_qrcode:bool = False) -> str:
"""Convert from internal representation to SGP.22 section 4.1 string representation."""
if for_qrcode:
ret = 'LPA:'
else:
ret = ''
ret += '%d$%s$%s' % (self.format, self.hostname, self.token)
if self.oid:
ret += '$%s' % (self.oid)
elif self.cc_required:
ret += '$'
if self.cc_required:
ret += '$1'
return ret
def __str__(self):
return self.to_string()
def to_qrcode(self):
"""Encode internal representation to QR code."""
import qrcode
qr = qrcode.QRCode()
qr.add_data(self.to_string(for_qrcode=True))
return qr.make_image()
def __repr__(self):
return "ActivationCode(format=%u, hostname='%s', token='%s', oid=%s, cc_required=%s)" % (self.format,
self.hostname,
self.token,
self.oid,
self.cc_required)

View File

@@ -0,0 +1,657 @@
PKIX1Explicit88 { iso(1) identified-organization(3) dod(6) internet(1)
security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-explicit(18) }
DEFINITIONS EXPLICIT TAGS ::=
BEGIN
-- EXPORTS ALL --
-- IMPORTS NONE --
-- UNIVERSAL Types defined in 1993 and 1998 ASN.1
-- and required by this specification
-- pycrate: UniversalString, BMPString and UTF8String already in the builtin types
--UniversalString ::= [UNIVERSAL 28] IMPLICIT OCTET STRING
-- UniversalString is defined in ASN.1:1993
--BMPString ::= [UNIVERSAL 30] IMPLICIT OCTET STRING
-- BMPString is the subtype of UniversalString and models
-- the Basic Multilingual Plane of ISO/IEC 10646
--UTF8String ::= [UNIVERSAL 12] IMPLICIT OCTET STRING
-- The content of this type conforms to RFC 3629.
-- PKIX specific OIDs
id-pkix OBJECT IDENTIFIER ::=
{ iso(1) identified-organization(3) dod(6) internet(1)
security(5) mechanisms(5) pkix(7) }
-- PKIX arcs
id-pe OBJECT IDENTIFIER ::= { id-pkix 1 }
-- arc for private certificate extensions
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
-- arc for policy qualifier types
id-kp OBJECT IDENTIFIER ::= { id-pkix 3 }
-- arc for extended key purpose OIDS
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
-- arc for access descriptors
-- policyQualifierIds for Internet policy qualifiers
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
-- OID for CPS qualifier
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
-- OID for user notice qualifier
-- access descriptor definitions
id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
id-ad-timeStamping OBJECT IDENTIFIER ::= { id-ad 3 }
id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
-- attribute data types
Attribute ::= SEQUENCE {
type AttributeType,
values SET OF AttributeValue }
-- at least one value is required
AttributeType ::= OBJECT IDENTIFIER
AttributeValue ::= ANY -- DEFINED BY AttributeType
AttributeTypeAndValue ::= SEQUENCE {
type AttributeType,
value AttributeValue }
-- suggested naming attributes: Definition of the following
-- information object set may be augmented to meet local
-- requirements. Note that deleting members of the set may
-- prevent interoperability with conforming implementations.
-- presented in pairs: the AttributeType followed by the
-- type definition for the corresponding AttributeValue
-- Arc for standard naming attributes
id-at OBJECT IDENTIFIER ::= { joint-iso-ccitt(2) ds(5) 4 }
-- Naming attributes of type X520name
id-at-name AttributeType ::= { id-at 41 }
id-at-surname AttributeType ::= { id-at 4 }
id-at-givenName AttributeType ::= { id-at 42 }
id-at-initials AttributeType ::= { id-at 43 }
id-at-generationQualifier AttributeType ::= { id-at 44 }
-- Naming attributes of type X520Name:
-- X520name ::= DirectoryString (SIZE (1..ub-name))
--
-- Expanded to avoid parameterized type:
X520name ::= CHOICE {
teletexString TeletexString (SIZE (1..ub-name)),
printableString PrintableString (SIZE (1..ub-name)),
universalString UniversalString (SIZE (1..ub-name)),
utf8String UTF8String (SIZE (1..ub-name)),
bmpString BMPString (SIZE (1..ub-name)) }
-- Naming attributes of type X520CommonName
id-at-commonName AttributeType ::= { id-at 3 }
-- Naming attributes of type X520CommonName:
-- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name))
--
-- Expanded to avoid parameterized type:
X520CommonName ::= CHOICE {
teletexString TeletexString (SIZE (1..ub-common-name)),
printableString PrintableString (SIZE (1..ub-common-name)),
universalString UniversalString (SIZE (1..ub-common-name)),
utf8String UTF8String (SIZE (1..ub-common-name)),
bmpString BMPString (SIZE (1..ub-common-name)) }
-- Naming attributes of type X520LocalityName
id-at-localityName AttributeType ::= { id-at 7 }
-- Naming attributes of type X520LocalityName:
-- X520LocalityName ::= DirectoryName (SIZE (1..ub-locality-name))
--
-- Expanded to avoid parameterized type:
X520LocalityName ::= CHOICE {
teletexString TeletexString (SIZE (1..ub-locality-name)),
printableString PrintableString (SIZE (1..ub-locality-name)),
universalString UniversalString (SIZE (1..ub-locality-name)),
utf8String UTF8String (SIZE (1..ub-locality-name)),
bmpString BMPString (SIZE (1..ub-locality-name)) }
-- Naming attributes of type X520StateOrProvinceName
id-at-stateOrProvinceName AttributeType ::= { id-at 8 }
-- Naming attributes of type X520StateOrProvinceName:
-- X520StateOrProvinceName ::= DirectoryName (SIZE (1..ub-state-name))
--
-- Expanded to avoid parameterized type:
X520StateOrProvinceName ::= CHOICE {
teletexString TeletexString (SIZE (1..ub-state-name)),
printableString PrintableString (SIZE (1..ub-state-name)),
universalString UniversalString (SIZE (1..ub-state-name)),
utf8String UTF8String (SIZE (1..ub-state-name)),
bmpString BMPString (SIZE (1..ub-state-name)) }
-- Naming attributes of type X520OrganizationName
id-at-organizationName AttributeType ::= { id-at 10 }
-- Naming attributes of type X520OrganizationName:
-- X520OrganizationName ::=
-- DirectoryName (SIZE (1..ub-organization-name))
--
-- Expanded to avoid parameterized type:
X520OrganizationName ::= CHOICE {
teletexString TeletexString
(SIZE (1..ub-organization-name)),
printableString PrintableString
(SIZE (1..ub-organization-name)),
universalString UniversalString
(SIZE (1..ub-organization-name)),
utf8String UTF8String
(SIZE (1..ub-organization-name)),
bmpString BMPString
(SIZE (1..ub-organization-name)) }
-- Naming attributes of type X520OrganizationalUnitName
id-at-organizationalUnitName AttributeType ::= { id-at 11 }
-- Naming attributes of type X520OrganizationalUnitName:
-- X520OrganizationalUnitName ::=
-- DirectoryName (SIZE (1..ub-organizational-unit-name))
--
-- Expanded to avoid parameterized type:
X520OrganizationalUnitName ::= CHOICE {
teletexString TeletexString
(SIZE (1..ub-organizational-unit-name)),
printableString PrintableString
(SIZE (1..ub-organizational-unit-name)),
universalString UniversalString
(SIZE (1..ub-organizational-unit-name)),
utf8String UTF8String
(SIZE (1..ub-organizational-unit-name)),
bmpString BMPString
(SIZE (1..ub-organizational-unit-name)) }
-- Naming attributes of type X520Title
id-at-title AttributeType ::= { id-at 12 }
-- Naming attributes of type X520Title:
-- X520Title ::= DirectoryName (SIZE (1..ub-title))
--
-- Expanded to avoid parameterized type:
X520Title ::= CHOICE {
teletexString TeletexString (SIZE (1..ub-title)),
printableString PrintableString (SIZE (1..ub-title)),
universalString UniversalString (SIZE (1..ub-title)),
utf8String UTF8String (SIZE (1..ub-title)),
bmpString BMPString (SIZE (1..ub-title)) }
-- Naming attributes of type X520dnQualifier
id-at-dnQualifier AttributeType ::= { id-at 46 }
X520dnQualifier ::= PrintableString
-- Naming attributes of type X520countryName (digraph from IS 3166)
id-at-countryName AttributeType ::= { id-at 6 }
X520countryName ::= PrintableString (SIZE (2))
-- Naming attributes of type X520SerialNumber
id-at-serialNumber AttributeType ::= { id-at 5 }
X520SerialNumber ::= PrintableString (SIZE (1..ub-serial-number))
-- Naming attributes of type X520Pseudonym
id-at-pseudonym AttributeType ::= { id-at 65 }
-- Naming attributes of type X520Pseudonym:
-- X520Pseudonym ::= DirectoryName (SIZE (1..ub-pseudonym))
--
-- Expanded to avoid parameterized type:
X520Pseudonym ::= CHOICE {
teletexString TeletexString (SIZE (1..ub-pseudonym)),
printableString PrintableString (SIZE (1..ub-pseudonym)),
universalString UniversalString (SIZE (1..ub-pseudonym)),
utf8String UTF8String (SIZE (1..ub-pseudonym)),
bmpString BMPString (SIZE (1..ub-pseudonym)) }
-- Naming attributes of type DomainComponent (from RFC 4519)
id-domainComponent AttributeType ::= { 0 9 2342 19200300 100 1 25 }
DomainComponent ::= IA5String
-- Legacy attributes
pkcs-9 OBJECT IDENTIFIER ::=
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 9 }
id-emailAddress AttributeType ::= { pkcs-9 1 }
EmailAddress ::= IA5String (SIZE (1..ub-emailaddress-length))
-- naming data types --
Name ::= CHOICE { -- only one possibility for now --
rdnSequence RDNSequence }
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
DistinguishedName ::= RDNSequence
RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
-- Directory string type --
DirectoryString ::= CHOICE {
teletexString TeletexString (SIZE (1..MAX)),
printableString PrintableString (SIZE (1..MAX)),
universalString UniversalString (SIZE (1..MAX)),
utf8String UTF8String (SIZE (1..MAX)),
bmpString BMPString (SIZE (1..MAX)) }
-- certificate and CRL specific structures begin here
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING }
TBSCertificate ::= SEQUENCE {
version [0] Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] Extensions OPTIONAL
-- If present, version MUST be v3 -- }
Version ::= INTEGER { v1(0), v2(1), v3(2) }
CertificateSerialNumber ::= INTEGER
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time }
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime }
UniqueIdentifier ::= BIT STRING
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
-- CRL structures
CertificateList ::= SEQUENCE {
tbsCertList TBSCertList,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING }
TBSCertList ::= SEQUENCE {
version Version OPTIONAL,
-- if present, MUST be v2
signature AlgorithmIdentifier,
issuer Name,
thisUpdate Time,
nextUpdate Time OPTIONAL,
revokedCertificates SEQUENCE OF SEQUENCE {
userCertificate CertificateSerialNumber,
revocationDate Time,
crlEntryExtensions Extensions OPTIONAL
-- if present, version MUST be v2
} OPTIONAL,
crlExtensions [0] Extensions OPTIONAL }
-- if present, version MUST be v2
-- Version, Time, CertificateSerialNumber, and Extensions were
-- defined earlier for use in the certificate structure
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
-- contains a value of the type
-- registered for use with the
-- algorithm object identifier value
-- X.400 address syntax starts here
ORAddress ::= SEQUENCE {
built-in-standard-attributes BuiltInStandardAttributes,
built-in-domain-defined-attributes
BuiltInDomainDefinedAttributes OPTIONAL,
-- see also teletex-domain-defined-attributes
extension-attributes ExtensionAttributes OPTIONAL }
-- Built-in Standard Attributes
BuiltInStandardAttributes ::= SEQUENCE {
country-name CountryName OPTIONAL,
administration-domain-name AdministrationDomainName OPTIONAL,
network-address [0] IMPLICIT NetworkAddress OPTIONAL,
-- see also extended-network-address
terminal-identifier [1] IMPLICIT TerminalIdentifier OPTIONAL,
private-domain-name [2] PrivateDomainName OPTIONAL,
organization-name [3] IMPLICIT OrganizationName OPTIONAL,
-- see also teletex-organization-name
numeric-user-identifier [4] IMPLICIT NumericUserIdentifier
OPTIONAL,
personal-name [5] IMPLICIT PersonalName OPTIONAL,
-- see also teletex-personal-name
organizational-unit-names [6] IMPLICIT OrganizationalUnitNames
OPTIONAL }
-- see also teletex-organizational-unit-names
CountryName ::= [APPLICATION 1] CHOICE {
x121-dcc-code NumericString
(SIZE (ub-country-name-numeric-length)),
iso-3166-alpha2-code PrintableString
(SIZE (ub-country-name-alpha-length)) }
AdministrationDomainName ::= [APPLICATION 2] CHOICE {
numeric NumericString (SIZE (0..ub-domain-name-length)),
printable PrintableString (SIZE (0..ub-domain-name-length)) }
NetworkAddress ::= X121Address -- see also extended-network-address
X121Address ::= NumericString (SIZE (1..ub-x121-address-length))
TerminalIdentifier ::= PrintableString (SIZE (1..ub-terminal-id-length))
PrivateDomainName ::= CHOICE {
numeric NumericString (SIZE (1..ub-domain-name-length)),
printable PrintableString (SIZE (1..ub-domain-name-length)) }
OrganizationName ::= PrintableString
(SIZE (1..ub-organization-name-length))
-- see also teletex-organization-name
NumericUserIdentifier ::= NumericString
(SIZE (1..ub-numeric-user-id-length))
PersonalName ::= SET {
surname [0] IMPLICIT PrintableString
(SIZE (1..ub-surname-length)),
given-name [1] IMPLICIT PrintableString
(SIZE (1..ub-given-name-length)) OPTIONAL,
initials [2] IMPLICIT PrintableString
(SIZE (1..ub-initials-length)) OPTIONAL,
generation-qualifier [3] IMPLICIT PrintableString
(SIZE (1..ub-generation-qualifier-length))
OPTIONAL }
-- see also teletex-personal-name
OrganizationalUnitNames ::= SEQUENCE SIZE (1..ub-organizational-units)
OF OrganizationalUnitName
-- see also teletex-organizational-unit-names
OrganizationalUnitName ::= PrintableString (SIZE
(1..ub-organizational-unit-name-length))
-- Built-in Domain-defined Attributes
BuiltInDomainDefinedAttributes ::= SEQUENCE SIZE
(1..ub-domain-defined-attributes) OF
BuiltInDomainDefinedAttribute
BuiltInDomainDefinedAttribute ::= SEQUENCE {
type PrintableString (SIZE
(1..ub-domain-defined-attribute-type-length)),
value PrintableString (SIZE
(1..ub-domain-defined-attribute-value-length)) }
-- Extension Attributes
ExtensionAttributes ::= SET SIZE (1..ub-extension-attributes) OF
ExtensionAttribute
ExtensionAttribute ::= SEQUENCE {
extension-attribute-type [0] IMPLICIT INTEGER
(0..ub-extension-attributes),
extension-attribute-value [1]
ANY DEFINED BY extension-attribute-type }
-- Extension types and attribute values
common-name INTEGER ::= 1
CommonName ::= PrintableString (SIZE (1..ub-common-name-length))
teletex-common-name INTEGER ::= 2
TeletexCommonName ::= TeletexString (SIZE (1..ub-common-name-length))
teletex-organization-name INTEGER ::= 3
TeletexOrganizationName ::=
TeletexString (SIZE (1..ub-organization-name-length))
teletex-personal-name INTEGER ::= 4
TeletexPersonalName ::= SET {
surname [0] IMPLICIT TeletexString
(SIZE (1..ub-surname-length)),
given-name [1] IMPLICIT TeletexString
(SIZE (1..ub-given-name-length)) OPTIONAL,
initials [2] IMPLICIT TeletexString
(SIZE (1..ub-initials-length)) OPTIONAL,
generation-qualifier [3] IMPLICIT TeletexString
(SIZE (1..ub-generation-qualifier-length))
OPTIONAL }
teletex-organizational-unit-names INTEGER ::= 5
TeletexOrganizationalUnitNames ::= SEQUENCE SIZE
(1..ub-organizational-units) OF TeletexOrganizationalUnitName
TeletexOrganizationalUnitName ::= TeletexString
(SIZE (1..ub-organizational-unit-name-length))
pds-name INTEGER ::= 7
PDSName ::= PrintableString (SIZE (1..ub-pds-name-length))
physical-delivery-country-name INTEGER ::= 8
PhysicalDeliveryCountryName ::= CHOICE {
x121-dcc-code NumericString (SIZE (ub-country-name-numeric-length)),
iso-3166-alpha2-code PrintableString
(SIZE (ub-country-name-alpha-length)) }
postal-code INTEGER ::= 9
PostalCode ::= CHOICE {
numeric-code NumericString (SIZE (1..ub-postal-code-length)),
printable-code PrintableString (SIZE (1..ub-postal-code-length)) }
physical-delivery-office-name INTEGER ::= 10
PhysicalDeliveryOfficeName ::= PDSParameter
physical-delivery-office-number INTEGER ::= 11
PhysicalDeliveryOfficeNumber ::= PDSParameter
extension-OR-address-components INTEGER ::= 12
ExtensionORAddressComponents ::= PDSParameter
physical-delivery-personal-name INTEGER ::= 13
PhysicalDeliveryPersonalName ::= PDSParameter
physical-delivery-organization-name INTEGER ::= 14
PhysicalDeliveryOrganizationName ::= PDSParameter
extension-physical-delivery-address-components INTEGER ::= 15
ExtensionPhysicalDeliveryAddressComponents ::= PDSParameter
unformatted-postal-address INTEGER ::= 16
UnformattedPostalAddress ::= SET {
printable-address SEQUENCE SIZE (1..ub-pds-physical-address-lines)
OF PrintableString (SIZE (1..ub-pds-parameter-length)) OPTIONAL,
teletex-string TeletexString
(SIZE (1..ub-unformatted-address-length)) OPTIONAL }
street-address INTEGER ::= 17
StreetAddress ::= PDSParameter
post-office-box-address INTEGER ::= 18
PostOfficeBoxAddress ::= PDSParameter
poste-restante-address INTEGER ::= 19
PosteRestanteAddress ::= PDSParameter
unique-postal-name INTEGER ::= 20
UniquePostalName ::= PDSParameter
local-postal-attributes INTEGER ::= 21
LocalPostalAttributes ::= PDSParameter
PDSParameter ::= SET {
printable-string PrintableString
(SIZE(1..ub-pds-parameter-length)) OPTIONAL,
teletex-string TeletexString
(SIZE(1..ub-pds-parameter-length)) OPTIONAL }
extended-network-address INTEGER ::= 22
ExtendedNetworkAddress ::= CHOICE {
e163-4-address SEQUENCE {
number [0] IMPLICIT NumericString
(SIZE (1..ub-e163-4-number-length)),
sub-address [1] IMPLICIT NumericString
(SIZE (1..ub-e163-4-sub-address-length))
OPTIONAL },
psap-address [0] IMPLICIT PresentationAddress }
PresentationAddress ::= SEQUENCE {
pSelector [0] EXPLICIT OCTET STRING OPTIONAL,
sSelector [1] EXPLICIT OCTET STRING OPTIONAL,
tSelector [2] EXPLICIT OCTET STRING OPTIONAL,
nAddresses [3] EXPLICIT SET SIZE (1..MAX) OF OCTET STRING }
terminal-type INTEGER ::= 23
TerminalType ::= INTEGER {
telex (3),
teletex (4),
g3-facsimile (5),
g4-facsimile (6),
ia5-terminal (7),
videotex (8) } (0..ub-integer-options)
-- Extension Domain-defined Attributes
teletex-domain-defined-attributes INTEGER ::= 6
TeletexDomainDefinedAttributes ::= SEQUENCE SIZE
(1..ub-domain-defined-attributes) OF TeletexDomainDefinedAttribute
TeletexDomainDefinedAttribute ::= SEQUENCE {
type TeletexString
(SIZE (1..ub-domain-defined-attribute-type-length)),
value TeletexString
(SIZE (1..ub-domain-defined-attribute-value-length)) }
-- specifications of Upper Bounds MUST be regarded as mandatory
-- from Annex B of ITU-T X.411 Reference Definition of MTS Parameter
-- Upper Bounds
-- Upper Bounds
ub-name INTEGER ::= 32768
ub-common-name INTEGER ::= 64
ub-locality-name INTEGER ::= 128
ub-state-name INTEGER ::= 128
ub-organization-name INTEGER ::= 64
ub-organizational-unit-name INTEGER ::= 64
ub-title INTEGER ::= 64
ub-serial-number INTEGER ::= 64
ub-match INTEGER ::= 128
ub-emailaddress-length INTEGER ::= 255
ub-common-name-length INTEGER ::= 64
ub-country-name-alpha-length INTEGER ::= 2
ub-country-name-numeric-length INTEGER ::= 3
ub-domain-defined-attributes INTEGER ::= 4
ub-domain-defined-attribute-type-length INTEGER ::= 8
ub-domain-defined-attribute-value-length INTEGER ::= 128
ub-domain-name-length INTEGER ::= 16
ub-extension-attributes INTEGER ::= 256
ub-e163-4-number-length INTEGER ::= 15
ub-e163-4-sub-address-length INTEGER ::= 40
ub-generation-qualifier-length INTEGER ::= 3
ub-given-name-length INTEGER ::= 16
ub-initials-length INTEGER ::= 5
ub-integer-options INTEGER ::= 256
ub-numeric-user-id-length INTEGER ::= 32
ub-organization-name-length INTEGER ::= 64
ub-organizational-unit-name-length INTEGER ::= 32
ub-organizational-units INTEGER ::= 4
ub-pds-name-length INTEGER ::= 16
ub-pds-parameter-length INTEGER ::= 30
ub-pds-physical-address-lines INTEGER ::= 6
ub-postal-code-length INTEGER ::= 16
ub-pseudonym INTEGER ::= 128
ub-surname-length INTEGER ::= 40
ub-terminal-id-length INTEGER ::= 24
ub-unformatted-address-length INTEGER ::= 180
ub-x121-address-length INTEGER ::= 16
-- Note - upper bounds on string types, such as TeletexString, are
-- measured in characters. Excepting PrintableString or IA5String, a
-- significantly greater number of octets will be required to hold
-- such a value. As a minimum, 16 octets, or twice the specified
-- upper bound, whichever is the larger, should be allowed for
-- TeletexString. For UTF8String or UniversalString at least four
-- times the upper bound should be allowed.
END

View File

@@ -0,0 +1,343 @@
PKIX1Implicit88 { iso(1) identified-organization(3) dod(6) internet(1)
security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-implicit(19) }
DEFINITIONS IMPLICIT TAGS ::=
BEGIN
-- EXPORTS ALL --
IMPORTS
id-pe, id-kp, id-qt-unotice, id-qt-cps,
ORAddress, Name, RelativeDistinguishedName,
CertificateSerialNumber, Attribute, DirectoryString
FROM PKIX1Explicit88 { iso(1) identified-organization(3)
dod(6) internet(1) security(5) mechanisms(5) pkix(7)
id-mod(0) id-pkix1-explicit(18) };
-- ISO arc for standard certificate and CRL extensions
id-ce OBJECT IDENTIFIER ::= {joint-iso-ccitt(2) ds(5) 29}
-- authority key identifier OID and syntax
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
AuthorityKeyIdentifier ::= SEQUENCE {
keyIdentifier [0] KeyIdentifier OPTIONAL,
authorityCertIssuer [1] GeneralNames OPTIONAL,
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
-- authorityCertIssuer and authorityCertSerialNumber MUST both
-- be present or both be absent
KeyIdentifier ::= OCTET STRING
-- subject key identifier OID and syntax
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
SubjectKeyIdentifier ::= KeyIdentifier
-- key usage extension OID and syntax
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
KeyUsage ::= BIT STRING {
digitalSignature (0),
nonRepudiation (1), -- recent editions of X.509 have
-- renamed this bit to contentCommitment
keyEncipherment (2),
dataEncipherment (3),
keyAgreement (4),
keyCertSign (5),
cRLSign (6),
encipherOnly (7),
decipherOnly (8) }
-- private key usage period extension OID and syntax
id-ce-privateKeyUsagePeriod OBJECT IDENTIFIER ::= { id-ce 16 }
PrivateKeyUsagePeriod ::= SEQUENCE {
notBefore [0] GeneralizedTime OPTIONAL,
notAfter [1] GeneralizedTime OPTIONAL }
-- either notBefore or notAfter MUST be present
-- certificate policies extension OID and syntax
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
CertificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
PolicyInformation ::= SEQUENCE {
policyIdentifier CertPolicyId,
policyQualifiers SEQUENCE SIZE (1..MAX) OF
PolicyQualifierInfo OPTIONAL }
CertPolicyId ::= OBJECT IDENTIFIER
PolicyQualifierInfo ::= SEQUENCE {
policyQualifierId PolicyQualifierId,
qualifier ANY DEFINED BY policyQualifierId }
-- Implementations that recognize additional policy qualifiers MUST
-- augment the following definition for PolicyQualifierId
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
-- CPS pointer qualifier
CPSuri ::= IA5String
-- user notice qualifier
UserNotice ::= SEQUENCE {
noticeRef NoticeReference OPTIONAL,
explicitText DisplayText OPTIONAL }
NoticeReference ::= SEQUENCE {
organization DisplayText,
noticeNumbers SEQUENCE OF INTEGER }
DisplayText ::= CHOICE {
ia5String IA5String (SIZE (1..200)),
visibleString VisibleString (SIZE (1..200)),
bmpString BMPString (SIZE (1..200)),
utf8String UTF8String (SIZE (1..200)) }
-- policy mapping extension OID and syntax
id-ce-policyMappings OBJECT IDENTIFIER ::= { id-ce 33 }
PolicyMappings ::= SEQUENCE SIZE (1..MAX) OF SEQUENCE {
issuerDomainPolicy CertPolicyId,
subjectDomainPolicy CertPolicyId }
-- subject alternative name extension OID and syntax
id-ce-subjectAltName OBJECT IDENTIFIER ::= { id-ce 17 }
SubjectAltName ::= GeneralNames
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
GeneralName ::= CHOICE {
otherName [0] AnotherName,
rfc822Name [1] IA5String,
dNSName [2] IA5String,
x400Address [3] ORAddress,
directoryName [4] Name,
ediPartyName [5] EDIPartyName,
uniformResourceIdentifier [6] IA5String,
iPAddress [7] OCTET STRING,
registeredID [8] OBJECT IDENTIFIER }
-- AnotherName replaces OTHER-NAME ::= TYPE-IDENTIFIER, as
-- TYPE-IDENTIFIER is not supported in the '88 ASN.1 syntax
AnotherName ::= SEQUENCE {
type-id OBJECT IDENTIFIER,
value [0] EXPLICIT ANY DEFINED BY type-id }
EDIPartyName ::= SEQUENCE {
nameAssigner [0] DirectoryString OPTIONAL,
partyName [1] DirectoryString }
-- issuer alternative name extension OID and syntax
id-ce-issuerAltName OBJECT IDENTIFIER ::= { id-ce 18 }
IssuerAltName ::= GeneralNames
id-ce-subjectDirectoryAttributes OBJECT IDENTIFIER ::= { id-ce 9 }
SubjectDirectoryAttributes ::= SEQUENCE SIZE (1..MAX) OF Attribute
-- basic constraints extension OID and syntax
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
BasicConstraints ::= SEQUENCE {
cA BOOLEAN DEFAULT FALSE,
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
-- name constraints extension OID and syntax
id-ce-nameConstraints OBJECT IDENTIFIER ::= { id-ce 30 }
NameConstraints ::= SEQUENCE {
permittedSubtrees [0] GeneralSubtrees OPTIONAL,
excludedSubtrees [1] GeneralSubtrees OPTIONAL }
GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
GeneralSubtree ::= SEQUENCE {
base GeneralName,
minimum [0] BaseDistance DEFAULT 0,
maximum [1] BaseDistance OPTIONAL }
BaseDistance ::= INTEGER (0..MAX)
-- policy constraints extension OID and syntax
id-ce-policyConstraints OBJECT IDENTIFIER ::= { id-ce 36 }
PolicyConstraints ::= SEQUENCE {
requireExplicitPolicy [0] SkipCerts OPTIONAL,
inhibitPolicyMapping [1] SkipCerts OPTIONAL }
SkipCerts ::= INTEGER (0..MAX)
-- CRL distribution points extension OID and syntax
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= {id-ce 31}
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
DistributionPoint ::= SEQUENCE {
distributionPoint [0] DistributionPointName OPTIONAL,
reasons [1] ReasonFlags OPTIONAL,
cRLIssuer [2] GeneralNames OPTIONAL }
DistributionPointName ::= CHOICE {
fullName [0] GeneralNames,
nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
ReasonFlags ::= BIT STRING {
unused (0),
keyCompromise (1),
cACompromise (2),
affiliationChanged (3),
superseded (4),
cessationOfOperation (5),
certificateHold (6),
privilegeWithdrawn (7),
aACompromise (8) }
-- extended key usage extension OID and syntax
id-ce-extKeyUsage OBJECT IDENTIFIER ::= {id-ce 37}
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
KeyPurposeId ::= OBJECT IDENTIFIER
-- permit unspecified key uses
anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 }
-- extended key purpose OIDs
id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 }
id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 }
id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 }
id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 }
id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 }
id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 }
-- inhibit any policy OID and syntax
id-ce-inhibitAnyPolicy OBJECT IDENTIFIER ::= { id-ce 54 }
InhibitAnyPolicy ::= SkipCerts
-- freshest (delta)CRL extension OID and syntax
id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 }
FreshestCRL ::= CRLDistributionPoints
-- authority info access
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
AuthorityInfoAccessSyntax ::=
SEQUENCE SIZE (1..MAX) OF AccessDescription
AccessDescription ::= SEQUENCE {
accessMethod OBJECT IDENTIFIER,
accessLocation GeneralName }
-- subject info access
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
SubjectInfoAccessSyntax ::=
SEQUENCE SIZE (1..MAX) OF AccessDescription
-- CRL number extension OID and syntax
id-ce-cRLNumber OBJECT IDENTIFIER ::= { id-ce 20 }
CRLNumber ::= INTEGER (0..MAX)
-- issuing distribution point extension OID and syntax
id-ce-issuingDistributionPoint OBJECT IDENTIFIER ::= { id-ce 28 }
IssuingDistributionPoint ::= SEQUENCE {
distributionPoint [0] DistributionPointName OPTIONAL,
onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE,
onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE,
onlySomeReasons [3] ReasonFlags OPTIONAL,
indirectCRL [4] BOOLEAN DEFAULT FALSE,
onlyContainsAttributeCerts [5] BOOLEAN DEFAULT FALSE }
-- at most one of onlyContainsUserCerts, onlyContainsCACerts,
-- and onlyContainsAttributeCerts may be set to TRUE.
id-ce-deltaCRLIndicator OBJECT IDENTIFIER ::= { id-ce 27 }
BaseCRLNumber ::= CRLNumber
-- reason code extension OID and syntax
id-ce-cRLReasons OBJECT IDENTIFIER ::= { id-ce 21 }
CRLReason ::= ENUMERATED {
unspecified (0),
keyCompromise (1),
cACompromise (2),
affiliationChanged (3),
superseded (4),
cessationOfOperation (5),
certificateHold (6),
removeFromCRL (8),
privilegeWithdrawn (9),
aACompromise (10) }
-- certificate issuer CRL entry extension OID and syntax
id-ce-certificateIssuer OBJECT IDENTIFIER ::= { id-ce 29 }
CertificateIssuer ::= GeneralNames
-- hold instruction extension OID and syntax
id-ce-holdInstructionCode OBJECT IDENTIFIER ::= { id-ce 23 }
HoldInstructionCode ::= OBJECT IDENTIFIER
-- ANSI x9 arc holdinstruction arc
holdInstruction OBJECT IDENTIFIER ::=
{joint-iso-itu-t(2) member-body(2) us(840) x9cm(10040) 2}
-- ANSI X9 holdinstructions
id-holdinstruction-none OBJECT IDENTIFIER ::=
{holdInstruction 1} -- deprecated
id-holdinstruction-callissuer OBJECT IDENTIFIER ::= {holdInstruction 2}
id-holdinstruction-reject OBJECT IDENTIFIER ::= {holdInstruction 3}
-- invalidity date CRL entry extension OID and syntax
id-ce-invalidityDate OBJECT IDENTIFIER ::= { id-ce 24 }
InvalidityDate ::= GeneralizedTime
END

785
pySim/esim/asn1/rsp/rsp.asn Normal file
View File

@@ -0,0 +1,785 @@
RSPDefinitions {joint-iso-itu-t(2) international-organizations(23) gsma(146) rsp(1) spec-version(1) version-two(2)}
DEFINITIONS
AUTOMATIC TAGS
EXTENSIBILITY IMPLIED ::=
BEGIN
IMPORTS Certificate, CertificateList, Time FROM PKIX1Explicit88 {iso(1) identified-organization(3) dod(6) internet(1) security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-explicit(18)}
SubjectKeyIdentifier FROM PKIX1Implicit88 {iso(1) identified-organization(3) dod(6) internet(1) security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-implicit(19)};
id-rsp OBJECT IDENTIFIER ::= {joint-iso-itu-t(2) international-organizations(23) gsma(146) rsp(1)}
-- Basic types, for size constraints
Octet8 ::= OCTET STRING (SIZE(8))
Octet16 ::= OCTET STRING (SIZE(16))
OctetTo16 ::= OCTET STRING (SIZE(1..16))
Octet32 ::= OCTET STRING (SIZE(32))
Octet1 ::= OCTET STRING(SIZE(1))
Octet2 ::= OCTET STRING (SIZE(2))
VersionType ::= OCTET STRING(SIZE(3)) -- major/minor/revision version are coded as binary value on byte 1/2/3, e.g. '02 00 0C' for v2.0.12.
Iccid ::= [APPLICATION 26] OCTET STRING (SIZE(10)) -- ICCID as coded in EFiccid, corresponding tag is '5A'
RemoteOpId ::= [2] INTEGER {installBoundProfilePackage(1)}
TransactionId ::= OCTET STRING (SIZE(1..16))
-- Definition of EUICCInfo1 --------------------------
GetEuiccInfo1Request ::= [32] SEQUENCE { -- Tag 'BF20'
}
EUICCInfo1 ::= [32] SEQUENCE { -- Tag 'BF20'
svn [2] VersionType, -- GSMA SGP.22 version supported (SVN)
euiccCiPKIdListForVerification [9] SEQUENCE OF SubjectKeyIdentifier, -- List of CI Public Key Identifiers supported on the eUICC for signature verification
euiccCiPKIdListForSigning [10] SEQUENCE OF SubjectKeyIdentifier -- List of CI Public Key Identifier supported on the eUICC for signature creation
}
-- Definition of EUICCInfo2 --------------------------
GetEuiccInfo2Request ::= [34] SEQUENCE { -- Tag 'BF22'
}
EUICCInfo2 ::= [34] SEQUENCE { -- Tag 'BF22'
profileVersion [1] VersionType, -- SIMAlliance Profile package version supported
svn [2] VersionType, -- GSMA SGP.22 version supported (SVN)
euiccFirmwareVer [3] VersionType, -- eUICC Firmware version
extCardResource [4] OCTET STRING, -- Extended Card Resource Information according to ETSI TS 102 226
uiccCapability [5] UICCCapability,
javacardVersion [6] VersionType OPTIONAL,
globalplatformVersion [7] VersionType OPTIONAL,
rspCapability [8] RspCapability,
euiccCiPKIdListForVerification [9] SEQUENCE OF SubjectKeyIdentifier, -- List of CI Public Key Identifiers supported on the eUICC for signature verification
euiccCiPKIdListForSigning [10] SEQUENCE OF SubjectKeyIdentifier, -- List of CI Public Key Identifier supported on the eUICC for signature creation
euiccCategory [11] INTEGER {
other(0),
basicEuicc(1),
mediumEuicc(2),
contactlessEuicc(3)
} OPTIONAL,
forbiddenProfilePolicyRules [25] PprIds OPTIONAL, -- Tag '99'
ppVersion VersionType, -- Protection Profile version
sasAcreditationNumber UTF8String (SIZE(0..64)),
certificationDataObject [12] CertificationDataObject OPTIONAL
}
-- Definition of RspCapability
RspCapability ::= BIT STRING {
additionalProfile(0), -- at least one more Profile can be installed
crlSupport(1), -- CRL
rpmSupport(2), -- Remote Profile Management
testProfileSupport (3) -- support for test profile
}
-- Definition of CertificationDataObject
CertificationDataObject ::= SEQUENCE {
platformLabel UTF8String, -- Platform_Label as defined in GlobalPlatform DLOA specification [57]
discoveryBaseURL UTF8String -- Discovery Base URL of the SE default DLOA Registrar as defined in GlobalPlatform DLOA specification [57]
}
CertificateInfo ::= BIT STRING {
reserved(0), -- eUICC has a CERT.EUICC.ECDSA in GlobalPlatform format. The use of this bit is deprecated.
certSigningX509(1), -- eUICC has a CERT.EUICC.ECDSA in X.509 format
rfu2(2),
rfu3(3),
reserved2(4), -- Handling of Certificate in GlobalPlatform format. The use of this bit is deprecated.
certVerificationX509(5)-- Handling of Certificate in X.509 format
}
-- Definition of UICCCapability
UICCCapability ::= BIT STRING {
/* Sequence is derived from ServicesList[] defined in SIMalliance PEDefinitions*/
contactlessSupport(0), -- Contactless (SWP, HCI and associated APIs)
usimSupport(1), -- USIM as defined by 3GPP
isimSupport(2), -- ISIM as defined by 3GPP
csimSupport(3), -- CSIM as defined by 3GPP2
akaMilenage(4), -- Milenage as AKA algorithm
akaCave(5), -- CAVE as authentication algorithm
akaTuak128(6), -- TUAK as AKA algorithm with 128 bit key length
akaTuak256(7), -- TUAK as AKA algorithm with 256 bit key length
rfu1(8), -- reserved for further algorithms
rfu2(9), -- reserved for further algorithms
gbaAuthenUsim(10), -- GBA authentication in the context of USIM
gbaAuthenISim(11), -- GBA authentication in the context of ISIM
mbmsAuthenUsim(12), -- MBMS authentication in the context of USIM
eapClient(13), -- EAP client
javacard(14), -- Javacard support
multos(15), -- Multos support
multipleUsimSupport(16), -- Multiple USIM applications are supported within the same Profile
multipleIsimSupport(17), -- Multiple ISIM applications are supported within the same Profile
multipleCsimSupport(18) -- Multiple CSIM applications are supported within the same Profile
}
-- Definition of DeviceInfo
DeviceInfo ::= SEQUENCE {
tac Octet8,
deviceCapabilities DeviceCapabilities,
imei Octet8 OPTIONAL
}
DeviceCapabilities ::= SEQUENCE { -- Highest fully supported release for each definition
-- The device SHALL set all the capabilities it supports
gsmSupportedRelease VersionType OPTIONAL,
utranSupportedRelease VersionType OPTIONAL,
cdma2000onexSupportedRelease VersionType OPTIONAL,
cdma2000hrpdSupportedRelease VersionType OPTIONAL,
cdma2000ehrpdSupportedRelease VersionType OPTIONAL,
eutranSupportedRelease VersionType OPTIONAL,
contactlessSupportedRelease VersionType OPTIONAL,
rspCrlSupportedVersion VersionType OPTIONAL,
rspRpmSupportedVersion VersionType OPTIONAL
}
ProfileInfoListRequest ::= [45] SEQUENCE { -- Tag 'BF2D'
searchCriteria [0] CHOICE {
isdpAid [APPLICATION 15] OctetTo16, -- AID of the ISD-P, tag '4F'
iccid Iccid, -- ICCID, tag '5A'
profileClass [21] ProfileClass -- Tag '95'
} OPTIONAL,
tagList [APPLICATION 28] OCTET STRING OPTIONAL -- tag '5C'
}
-- Definition of ProfileInfoList
ProfileInfoListResponse ::= [45] CHOICE { -- Tag 'BF2D'
profileInfoListOk SEQUENCE OF ProfileInfo,
profileInfoListError ProfileInfoListError
}
ProfileInfo ::= [PRIVATE 3] SEQUENCE { -- Tag 'E3'
iccid Iccid OPTIONAL,
isdpAid [APPLICATION 15] OctetTo16 OPTIONAL, -- AID of the ISD-P containing the Profile, tag '4F'
profileState [112] ProfileState OPTIONAL, -- Tag '9F70'
profileNickname [16] UTF8String (SIZE(0..64)) OPTIONAL, -- Tag '90'
serviceProviderName [17] UTF8String (SIZE(0..32)) OPTIONAL, -- Tag '91'
profileName [18] UTF8String (SIZE(0..64)) OPTIONAL, -- Tag '92'
iconType [19] IconType OPTIONAL, -- Tag '93'
icon [20] OCTET STRING (SIZE(0..1024)) OPTIONAL, -- Tag '94', see condition in ES10c:GetProfilesInfo
profileClass [21] ProfileClass DEFAULT operational, -- Tag '95'
notificationConfigurationInfo [22] SEQUENCE OF NotificationConfigurationInformation OPTIONAL, -- Tag 'B6'
profileOwner [23] OperatorID OPTIONAL, -- Tag 'B7'
dpProprietaryData [24] DpProprietaryData OPTIONAL, -- Tag 'B8'
profilePolicyRules [25] PprIds OPTIONAL -- Tag '99'
}
PprIds ::= BIT STRING {-- Definition of Profile Policy Rules identifiers
pprUpdateControl(0), -- defines how to update PPRs via ES6
ppr1(1), -- Indicator for PPR1 'Disabling of this Profile is not allowed'
ppr2(2), -- Indicator for PPR2 'Deletion of this Profile is not allowed'
ppr3(3) -- Indicator for PPR3 'Deletion of this Profile is required upon its successful disabling'
}
OperatorID ::= SEQUENCE {
mccMnc OCTET STRING (SIZE(3)), -- MCC and MNC coded as defined in 3GPP TS 24.008 [32]
gid1 OCTET STRING OPTIONAL, -- referring to content of EF GID1 (file identifier '6F3E') as defined in 3GPP TS 31.102 [54]
gid2 OCTET STRING OPTIONAL -- referring to content of EF GID2 (file identifier '6F3F') as defined in 3GPP TS 31.102 [54]
}
ProfileInfoListError ::= INTEGER {incorrectInputValues(1), undefinedError(127)}
-- Definition of StoreMetadata request
StoreMetadataRequest ::= [37] SEQUENCE { -- Tag 'BF25'
iccid Iccid,
serviceProviderName [17] UTF8String (SIZE(0..32)), -- Tag '91'
profileName [18] UTF8String (SIZE(0..64)), -- Tag '92' (corresponds to 'Short Description' defined in SGP.21 [2])
iconType [19] IconType OPTIONAL, -- Tag '93' (JPG or PNG)
icon [20] OCTET STRING (SIZE(0..1024)) OPTIONAL, -- Tag '94'(Data of the icon. Size 64 x 64 pixel. This field SHALL only be present if iconType is present)
profileClass [21] ProfileClass OPTIONAL, -- Tag '95' (default if absent: 'operational')
notificationConfigurationInfo [22] SEQUENCE OF NotificationConfigurationInformation OPTIONAL,
profileOwner [23] OperatorID OPTIONAL, -- Tag 'B7'
profilePolicyRules [25] PprIds OPTIONAL -- Tag '99'
}
NotificationEvent ::= BIT STRING {
notificationInstall (0),
notificationEnable(1),
notificationDisable(2),
notificationDelete(3)
}
NotificationConfigurationInformation ::= SEQUENCE {
profileManagementOperation NotificationEvent,
notificationAddress UTF8String -- FQDN to forward the notification
}
IconType ::= INTEGER {jpg(0), png(1)}
ProfileState ::= INTEGER {disabled(0), enabled(1)}
ProfileClass ::= INTEGER {test(0), provisioning(1), operational(2)}
-- Definition of UpdateMetadata request
UpdateMetadataRequest ::= [42] SEQUENCE { -- Tag 'BF2A'
serviceProviderName [17] UTF8String (SIZE(0..32)) OPTIONAL, -- Tag '91'
profileName [18] UTF8String (SIZE(0..64)) OPTIONAL, -- Tag '92'
iconType [19] IconType OPTIONAL, -- Tag '93'
icon [20] OCTET STRING (SIZE(0..1024)) OPTIONAL, -- Tag '94'
profilePolicyRules [25] PprIds OPTIONAL -- Tag '99'
}
-- Definition of data objects for command PrepareDownload -------------------------
PrepareDownloadRequest ::= [33] SEQUENCE { -- Tag 'BF21'
smdpSigned2 SmdpSigned2, -- Signed information
smdpSignature2 [APPLICATION 55] OCTET STRING, -- DP_Sign1, tag '5F37'
hashCc Octet32 OPTIONAL, -- Hash of confirmation code
smdpCertificate Certificate -- CERT.DPpb.ECDSA
}
SmdpSigned2 ::= SEQUENCE {
transactionId [0] TransactionId, -- The TransactionID generated by the SM DP+
ccRequiredFlag BOOLEAN, --Indicates if the Confirmation Code is required
bppEuiccOtpk [APPLICATION 73] OCTET STRING OPTIONAL -- otPK.EUICC.ECKA already used for binding the BPP, tag '5F49'
}
PrepareDownloadResponse ::= [33] CHOICE { -- Tag 'BF21'
downloadResponseOk PrepareDownloadResponseOk,
downloadResponseError PrepareDownloadResponseError
}
PrepareDownloadResponseOk ::= SEQUENCE {
euiccSigned2 EUICCSigned2, -- Signed information
euiccSignature2 [APPLICATION 55] OCTET STRING -- tag '5F37'
}
EUICCSigned2 ::= SEQUENCE {
transactionId [0] TransactionId,
euiccOtpk [APPLICATION 73] OCTET STRING, -- otPK.EUICC.ECKA, tag '5F49'
hashCc Octet32 OPTIONAL -- Hash of confirmation code
}
PrepareDownloadResponseError ::= SEQUENCE {
transactionId [0] TransactionId,
downloadErrorCode DownloadErrorCode
}
DownloadErrorCode ::= INTEGER {invalidCertificate(1), invalidSignature(2), unsupportedCurve(3), noSessionContext(4), invalidTransactionId(5), undefinedError(127)}
-- Definition of data objects for command AuthenticateServer--------------------
AuthenticateServerRequest ::= [56] SEQUENCE { -- Tag 'BF38'
serverSigned1 ServerSigned1, -- Signed information
serverSignature1 [APPLICATION 55] OCTET STRING, -- tag ?5F37?
euiccCiPKIdToBeUsed SubjectKeyIdentifier, -- CI Public Key Identifier to be used
serverCertificate Certificate, -- RSP Server Certificate CERT.XXauth.ECDSA
ctxParams1 CtxParams1
}
ServerSigned1 ::= SEQUENCE {
transactionId [0] TransactionId, -- The Transaction ID generated by the RSP Server
euiccChallenge [1] Octet16, -- The eUICC Challenge
serverAddress [3] UTF8String, -- The RSP Server address
serverChallenge [4] Octet16 -- The RSP Server Challenge
}
CtxParams1 ::= CHOICE {
ctxParamsForCommonAuthentication CtxParamsForCommonAuthentication -- New contextual data objects may be defined for extensibility
}
CtxParamsForCommonAuthentication ::= SEQUENCE {
matchingId UTF8String OPTIONAL,-- The MatchingId could be the Activation code token or EventID or empty
deviceInfo DeviceInfo -- The Device information
}
AuthenticateServerResponse ::= [56] CHOICE { -- Tag 'BF38'
authenticateResponseOk AuthenticateResponseOk,
authenticateResponseError AuthenticateResponseError
}
AuthenticateResponseOk ::= SEQUENCE {
euiccSigned1 EuiccSigned1, -- Signed information
euiccSignature1 [APPLICATION 55] OCTET STRING, --EUICC_Sign1, tag 5F37
euiccCertificate Certificate, -- eUICC Certificate (CERT.EUICC.ECDSA) signed by the EUM
eumCertificate Certificate -- EUM Certificate (CERT.EUM.ECDSA) signed by the requested CI
}
EuiccSigned1 ::= SEQUENCE {
transactionId [0] TransactionId,
serverAddress [3] UTF8String,
serverChallenge [4] Octet16, -- The RSP Server Challenge
euiccInfo2 [34] EUICCInfo2,
ctxParams1 CtxParams1
}
AuthenticateResponseError ::= SEQUENCE {
transactionId [0] TransactionId,
authenticateErrorCode AuthenticateErrorCode
}
AuthenticateErrorCode ::= INTEGER {invalidCertificate(1), invalidSignature(2), unsupportedCurve(3), noSessionContext(4), invalidOid(5), euiccChallengeMismatch(6), ciPKUnknown(7), undefinedError(127)}
-- Definition of Cancel Session------------------------------
CancelSessionRequest ::= [65] SEQUENCE { -- Tag 'BF41'
transactionId TransactionId, -- The TransactionID generated by the RSP Server
reason CancelSessionReason
}
CancelSessionReason ::= INTEGER {endUserRejection(0), postponed(1), timeout(2), pprNotAllowed(3)}
CancelSessionResponse ::= [65] CHOICE { -- Tag 'BF41'
cancelSessionResponseOk CancelSessionResponseOk,
cancelSessionResponseError INTEGER {invalidTransactionId(5), undefinedError(127)}
}
CancelSessionResponseOk ::= SEQUENCE {
euiccCancelSessionSigned EuiccCancelSessionSigned, -- Signed information
euiccCancelSessionSignature [APPLICATION 55] OCTET STRING -- tag '5F37
}
EuiccCancelSessionSigned ::= SEQUENCE {
transactionId TransactionId,
smdpOid OBJECT IDENTIFIER, -- SM-DP+ OID as contained in CERT.DPauth.ECDSA
reason CancelSessionReason
}
-- Definition of Bound Profile Package --------------------------
BoundProfilePackage ::= [54] SEQUENCE { -- Tag 'BF36'
initialiseSecureChannelRequest [35] InitialiseSecureChannelRequest, -- Tag 'BF23'
firstSequenceOf87 [0] SEQUENCE OF [7] OCTET STRING, -- sequence of '87' TLVs
sequenceOf88 [1] SEQUENCE OF [8] OCTET STRING, -- sequence of '88' TLVs
secondSequenceOf87 [2] SEQUENCE OF [7] OCTET STRING OPTIONAL, -- sequence of '87' TLVs
sequenceOf86 [3] SEQUENCE OF [6] OCTET STRING -- sequence of '86' TLVs
}
-- Definition of Get eUICC Challenge --------------------------
GetEuiccChallengeRequest ::= [46] SEQUENCE { -- Tag 'BF2E'
}
GetEuiccChallengeResponse ::= [46] SEQUENCE { -- Tag 'BF2E'
euiccChallenge Octet16 -- random eUICC challenge
}
-- Definition of Profile Installation Resulceipt
ProfileInstallationResult ::= [55] SEQUENCE { -- Tag 'BF37'
profileInstallationResultData [39] ProfileInstallationResultData,
euiccSignPIR EuiccSignPIR
}
ProfileInstallationResultData ::= [39] SEQUENCE { -- Tag 'BF27'
transactionId[0] TransactionId, -- The TransactionID generated by the SM-DP+
notificationMetadata[47] NotificationMetadata,
smdpOid OBJECT IDENTIFIER OPTIONAL, -- SM-DP+ OID (same value as in CERT.DPpb.ECDSA)
finalResult [2] CHOICE {
successResult SuccessResult,
errorResult ErrorResult
}
}
EuiccSignPIR ::= [APPLICATION 55] OCTET STRING -- Tag '5F37', eUICC?s signature
SuccessResult ::= SEQUENCE {
aid [APPLICATION 15] OCTET STRING (SIZE (5..16)), -- AID of ISD-P
simaResponse OCTET STRING -- contains (multiple) 'EUICCResponse' as defined in [5]
}
ErrorResult ::= SEQUENCE {
bppCommandId BppCommandId,
errorReason ErrorReason,
simaResponse OCTET STRING OPTIONAL -- contains (multiple) 'EUICCResponse' as defined in [5]
}
BppCommandId ::= INTEGER {initialiseSecureChannel(0), configureISDP(1), storeMetadata(2), storeMetadata2(3), replaceSessionKeys(4), loadProfileElements(5)}
ErrorReason ::= INTEGER {
incorrectInputValues(1),
invalidSignature(2),
invalidTransactionId(3),
unsupportedCrtValues(4),
unsupportedRemoteOperationType(5),
unsupportedProfileClass(6),
scp03tStructureError(7),
scp03tSecurityError(8),
installFailedDueToIccidAlreadyExistsOnEuicc(9), installFailedDueToInsufficientMemoryForProfile(10),
installFailedDueToInterruption(11),
installFailedDueToPEProcessingError (12),
installFailedDueToIccidMismatch(13),
testProfileInstallFailedDueToInvalidNaaKey(14),
pprNotAllowed(15),
installFailedDueToUnknownError(127)
}
ListNotificationRequest ::= [40] SEQUENCE { -- Tag 'BF28'
profileManagementOperation [1] NotificationEvent OPTIONAL
}
ListNotificationResponse ::= [40] CHOICE { -- Tag 'BF28'
notificationMetadataList SEQUENCE OF NotificationMetadata,
listNotificationsResultError INTEGER {undefinedError(127)}
}
NotificationMetadata ::= [47] SEQUENCE { -- Tag 'BF2F'
seqNumber [0] INTEGER,
profileManagementOperation [1] NotificationEvent, --Only one bit set to 1
notificationAddress UTF8String, -- FQDN to forward the notification
iccid Iccid OPTIONAL
}
-- Definition of Profile Nickname Information
SetNicknameRequest ::= [41] SEQUENCE { -- Tag 'BF29'
iccid Iccid,
profileNickname [16] UTF8String (SIZE(0..64))
}
SetNicknameResponse ::= [41] SEQUENCE { -- Tag 'BF29'
setNicknameResult INTEGER {ok(0), iccidNotFound (1), undefinedError(127)}
}
id-rsp-cert-objects OBJECT IDENTIFIER ::= { id-rsp cert-objects(2)}
id-rspExt OBJECT IDENTIFIER ::= {id-rsp-cert-objects 0}
id-rspRole OBJECT IDENTIFIER ::= {id-rsp-cert-objects 1}
-- Definition of OIDs for role identification
id-rspRole-ci OBJECT IDENTIFIER ::= {id-rspRole 0}
id-rspRole-euicc OBJECT IDENTIFIER ::= {id-rspRole 1}
id-rspRole-eum OBJECT IDENTIFIER ::= {id-rspRole 2}
id-rspRole-dp-tls OBJECT IDENTIFIER ::= {id-rspRole 3}
id-rspRole-dp-auth OBJECT IDENTIFIER ::= {id-rspRole 4}
id-rspRole-dp-pb OBJECT IDENTIFIER ::= {id-rspRole 5}
id-rspRole-ds-tls OBJECT IDENTIFIER ::= {id-rspRole 6}
id-rspRole-ds-auth OBJECT IDENTIFIER ::= {id-rspRole 7}
--Definition of data objects for InitialiseSecureChannel Request
InitialiseSecureChannelRequest ::= [35] SEQUENCE { -- Tag 'BF23'
remoteOpId RemoteOpId, -- Remote Operation Type Identifier (value SHALL be set to installBoundProfilePackage)
transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+
controlRefTemplate[6] IMPLICIT ControlRefTemplate, -- Control Reference Template (Key Agreement). Current specification considers a subset of CRT specified in GlobalPlatform Card Specification [8], section 6.4.2.3 for the Mutual Authentication Data Field
smdpOtpk [APPLICATION 73] OCTET STRING, ---otPK.DP.ECKA as specified in GlobalPlatform Card Specification [8] section 6.4.2.3 for ePK.OCE.ECKA, tag '5F49'
smdpSign [APPLICATION 55] OCTET STRING -- SM-DP's signature, tag '5F37'
}
ControlRefTemplate ::= SEQUENCE {
keyType[0] Octet1, -- Key type according to GlobalPlatform Card Specification [8] Table 11-16, AES= '88', Tag '80'
keyLen[1] Octet1, --Key length in number of bytes. For current specification key length SHALL by 0x10 bytes, Tag '81'
hostId[4] OctetTo16 -- Host ID value , Tag '84'
}
--Definition of data objects for ConfigureISDPRequest
ConfigureISDPRequest ::= [36] SEQUENCE { -- Tag 'BF24'
dpProprietaryData [24] DpProprietaryData OPTIONAL -- Tag 'B8'
}
DpProprietaryData ::= SEQUENCE { -- maximum size including tag and length field: 128 bytes
dpOid OBJECT IDENTIFIER -- OID in the tree of the SM-DP+ that created the Profile
-- additional data objects defined by the SM-DP+ MAY follow
}
-- Definition of request message for command ReplaceSessionKeys
ReplaceSessionKeysRequest ::= [38] SEQUENCE { -- tag 'BF26'
/*The new initial MAC chaining value*/
initialMacChainingValue OCTET STRING,
/*New session key value for encryption/decryption (PPK-ENC)*/
ppkEnc OCTET STRING,
/*New session key value of the session key C-MAC computation/verification (PPK-MAC)*/
ppkCmac OCTET STRING
}
-- Definition of data objects for RetrieveNotificationsList
RetrieveNotificationsListRequest ::= [43] SEQUENCE { -- Tag 'BF2B'
searchCriteria CHOICE {
seqNumber [0] INTEGER,
profileManagementOperation [1] NotificationEvent
} OPTIONAL
}
RetrieveNotificationsListResponse ::= [43] CHOICE { -- Tag 'BF2B'
notificationList SEQUENCE OF PendingNotification,
notificationsListResultError INTEGER {noResultAvailable(1), undefinedError(127)}
}
PendingNotification ::= CHOICE {
profileInstallationResult [55] ProfileInstallationResult, -- tag 'BF37'
otherSignedNotification OtherSignedNotification
}
OtherSignedNotification ::= SEQUENCE {
tbsOtherNotification NotificationMetadata,
euiccNotificationSignature [APPLICATION 55] OCTET STRING, -- eUICC signature of tbsOtherNotification, Tag '5F37'
euiccCertificate Certificate, -- eUICC Certificate (CERT.EUICC.ECDSA) signed by the EUM
eumCertificate Certificate -- EUM Certificate (CERT.EUM.ECDSA) signed by the requested CI
}
-- Definition of notificationSent
NotificationSentRequest ::= [48] SEQUENCE { -- Tag 'BF30'
seqNumber [0] INTEGER
}
NotificationSentResponse ::= [48] SEQUENCE { -- Tag 'BF30'
deleteNotificationStatus INTEGER {ok(0), nothingToDelete(1), undefinedError(127)}
}
-- Definition of Enable Profile --------------------------
EnableProfileRequest ::= [49] SEQUENCE { -- Tag 'BF31'
profileIdentifier CHOICE {
isdpAid [APPLICATION 15] OctetTo16, -- AID, tag '4F'
iccid Iccid -- ICCID, tag '5A'
},
refreshFlag BOOLEAN -- indicating whether REFRESH is required
}
EnableProfileResponse ::= [49] SEQUENCE { -- Tag 'BF31'
enableResult INTEGER {ok(0), iccidOrAidNotFound (1), profileNotInDisabledState(2), disallowedByPolicy(3), wrongProfileReenabling(4), undefinedError(127)}
}
-- Definition of Disable Profile --------------------------
DisableProfileRequest ::= [50] SEQUENCE { -- Tag 'BF32'
profileIdentifier CHOICE {
isdpAid [APPLICATION 15] OctetTo16, -- AID, tag '4F'
iccid Iccid -- ICCID, tag '5A'
},
refreshFlag BOOLEAN -- indicating whether REFRESH is required
}
DisableProfileResponse ::= [50] SEQUENCE { -- Tag 'BF32'
disableResult INTEGER {ok(0), iccidOrAidNotFound (1), profileNotInEnabledState(2), disallowedByPolicy(3), undefinedError(127)}
}
-- Definition of Delete Profile --------------------------
DeleteProfileRequest ::= [51] CHOICE { -- Tag 'BF33'
isdpAid [APPLICATION 15] OctetTo16, -- AID, tag '4F'
iccid Iccid -- ICCID, tag '5A'
}
DeleteProfileResponse ::= [51] SEQUENCE { -- Tag 'BF33'
deleteResult INTEGER {ok(0), iccidOrAidNotFound (1), profileNotInDisabledState(2), disallowedByPolicy(3), undefinedError(127)}
}
-- Definition of Memory Reset --------------------------
EuiccMemoryResetRequest ::= [52] SEQUENCE { -- Tag 'BF34'
resetOptions [2] BIT STRING {
deleteOperationalProfiles(0),
deleteFieldLoadedTestProfiles(1),
resetDefaultSmdpAddress(2)}
}
EuiccMemoryResetResponse ::= [52] SEQUENCE { -- Tag 'BF34'
resetResult INTEGER {ok(0), nothingToDelete(1), undefinedError(127)}
}
-- Definition of Get EID --------------------------
GetEuiccDataRequest ::= [62] SEQUENCE { -- Tag 'BF3E'
tagList [APPLICATION 28] Octet1 -- tag '5C', the value SHALL be set to '5A'
}
GetEuiccDataResponse ::= [62] SEQUENCE { -- Tag 'BF3E'
eidValue [APPLICATION 26] Octet16 -- tag '5A'
}
-- Definition of Get Rat
GetRatRequest ::= [67] SEQUENCE { -- Tag ' BF43'
-- No input data
}
GetRatResponse ::= [67] SEQUENCE { -- Tag 'BF43'
rat RulesAuthorisationTable
}
RulesAuthorisationTable ::= SEQUENCE OF ProfilePolicyAuthorisationRule
ProfilePolicyAuthorisationRule ::= SEQUENCE {
pprIds PprIds,
allowedOperators SEQUENCE OF OperatorID,
pprFlags BIT STRING {consentRequired(0)}
}
-- Definition of data structure command for loading a CRL
LoadCRLRequest ::= [53] SEQUENCE { -- Tag 'BF35'
-- A CRL-A
crl CertificateList
}
-- Definition of data structure response for loading a CRL
LoadCRLResponse ::= [53] CHOICE { -- Tag 'BF35'
loadCRLResponseOk LoadCRLResponseOk,
loadCRLResponseError LoadCRLResponseError
}
LoadCRLResponseOk ::= SEQUENCE {
missingParts SEQUENCE OF SEQUENCE {
number INTEGER (0..MAX)
} OPTIONAL
}
LoadCRLResponseError ::= INTEGER {invalidSignature(1), invalidCRLFormat(2), notEnoughMemorySpace(3), verificationKeyNotFound(4), undefinedError(127)}
-- Definition of the extension for Certificate Expiration Date
id-rsp-expDate OBJECT IDENTIFIER ::= {id-rspExt 1}
ExpirationDate ::= Time
-- Definition of the extension id for total partial-CRL number
id-rsp-totalPartialCrlNumber OBJECT IDENTIFIER ::= {id-rspExt 2}
TotalPartialCrlNumber ::= INTEGER
-- Definition of the extension id for the partial-CRL number
id-rsp-partialCrlNumber OBJECT IDENTIFIER ::= {id-rspExt 3}
PartialCrlNumber ::= INTEGER
-- Definition for ES9+ ASN.1 Binding --------------------------
RemoteProfileProvisioningRequest ::= [2] CHOICE { -- Tag 'A2'
initiateAuthenticationRequest [57] InitiateAuthenticationRequest, -- Tag 'BF39'
authenticateClientRequest [59] AuthenticateClientRequest, -- Tag 'BF3B'
getBoundProfilePackageRequest [58] GetBoundProfilePackageRequest, -- Tag 'BF3A'
cancelSessionRequestEs9 [65] CancelSessionRequestEs9, -- Tag 'BF41'
handleNotification [61] HandleNotification -- tag 'BF3D'
}
RemoteProfileProvisioningResponse ::= [2] CHOICE { -- Tag 'A2'
initiateAuthenticationResponse [57] InitiateAuthenticationResponse, -- Tag 'BF39'
authenticateClientResponseEs9 [59] AuthenticateClientResponseEs9, -- Tag 'BF3B'
getBoundProfilePackageResponse [58] GetBoundProfilePackageResponse, -- Tag 'BF3A'
cancelSessionResponseEs9 [65] CancelSessionResponseEs9, -- Tag 'BF41'
authenticateClientResponseEs11 [64] AuthenticateClientResponseEs11 -- Tag 'BF40'
}
InitiateAuthenticationRequest ::= [57] SEQUENCE { -- Tag 'BF39'
euiccChallenge [1] Octet16, -- random eUICC challenge
smdpAddress [3] UTF8String,
euiccInfo1 EUICCInfo1
}
InitiateAuthenticationResponse ::= [57] CHOICE { -- Tag 'BF39'
initiateAuthenticationOk InitiateAuthenticationOkEs9,
initiateAuthenticationError INTEGER {
invalidDpAddress(1),
euiccVersionNotSupportedByDp(2),
ciPKNotSupported(3)
}
}
InitiateAuthenticationOkEs9 ::= SEQUENCE {
transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+
serverSigned1 ServerSigned1, -- Signed information
serverSignature1 [APPLICATION 55] OCTET STRING, -- Server_Sign1, tag '5F37'
euiccCiPKIdToBeUsed SubjectKeyIdentifier, -- The curve CI Public Key to be used as required by ES10b.AuthenticateServer
serverCertificate Certificate
}
AuthenticateClientRequest ::= [59] SEQUENCE { -- Tag 'BF3B'
transactionId [0] TransactionId,
authenticateServerResponse [56] AuthenticateServerResponse -- This is the response from ES10b.AuthenticateServer
}
AuthenticateClientResponseEs9 ::= [59] CHOICE { -- Tag 'BF3B'
authenticateClientOk AuthenticateClientOk,
authenticateClientError INTEGER {
eumCertificateInvalid(1),
eumCertificateExpired(2),
euiccCertificateInvalid(3),
euiccCertificateExpired(4),
euiccSignatureInvalid(5),
matchingIdRefused(6),
eidMismatch(7),
noEligibleProfile(8),
ciPKUnknown(9),
invalidTransactionId(10),
undefinedError(127)
}
}
AuthenticateClientOk ::= SEQUENCE {
transactionId [0] TransactionId,
profileMetaData [37] StoreMetadataRequest,
prepareDownloadRequest [33] PrepareDownloadRequest
}
GetBoundProfilePackageRequest ::= [58] SEQUENCE { -- Tag 'BF3A'
transactionId [0] TransactionId,
prepareDownloadResponse [33] PrepareDownloadResponse
}
GetBoundProfilePackageResponse ::= [58] CHOICE { -- Tag 'BF3A'
getBoundProfilePackageOk GetBoundProfilePackageOk,
getBoundProfilePackageError INTEGER {
euiccSignatureInvalid(1),
confirmationCodeMissing(2),
confirmationCodeRefused(3),
confirmationCodeRetriesExceeded(4),
invalidTransactionId(95),
undefinedError(127)
}
}
GetBoundProfilePackageOk ::= SEQUENCE {
transactionId [0] TransactionId,
boundProfilePackage [54] BoundProfilePackage
}
HandleNotification ::= [61] SEQUENCE { -- Tag 'BF3D'
pendingNotification PendingNotification
}
CancelSessionRequestEs9 ::= [65] SEQUENCE { -- Tag 'BF41'
transactionId TransactionId,
cancelSessionResponse CancelSessionResponse -- data structure defined for ES10b.CancelSession function
}
CancelSessionResponseEs9 ::= [65] CHOICE { -- Tag 'BF41'
cancelSessionOk CancelSessionOk,
cancelSessionError INTEGER {
invalidTransactionId(1),
euiccSignatureInvalid(2),
undefinedError(127)
}
}
CancelSessionOk ::= SEQUENCE { -- This function has no output data
}
EuiccConfiguredAddressesRequest ::= [60] SEQUENCE { -- Tag 'BF3C'
}
EuiccConfiguredAddressesResponse ::= [60] SEQUENCE { -- Tag 'BF3C'
defaultDpAddress UTF8String OPTIONAL, -- Default SM-DP+ address as an FQDN
rootDsAddress UTF8String -- Root SM-DS address as an FQDN
}
ISDRProprietaryApplicationTemplate ::= [PRIVATE 0] SEQUENCE { -- Tag 'E0'
svn [2] VersionType, -- GSMA SGP.22 version supported (SVN)
lpaeSupport BIT STRING {
lpaeUsingCat(0), -- LPA in the eUICC using Card Application Toolkit
lpaeUsingScws(1) -- LPA in the eUICC using Smartcard Web Server
} OPTIONAL
}
LpaeActivationRequest ::= [66] SEQUENCE { -- Tag 'BF42'
lpaeOption BIT STRING {
activateCatBasedLpae(0), -- LPAe with LUIe based on CAT
activateScwsBasedLpae(1) -- LPAe with LUIe based on SCWS
}
}
LpaeActivationResponse ::= [66] SEQUENCE { -- Tag 'BF42'
lpaeActivationResult INTEGER {ok(0), notSupported(1)}
}
SetDefaultDpAddressRequest ::= [63] SEQUENCE { -- Tag 'BF3F'
defaultDpAddress UTF8String -- Default SM-DP+ address as an FQDN
}
SetDefaultDpAddressResponse ::= [63] SEQUENCE { -- Tag 'BF3F'
setDefaultDpAddressResult INTEGER { ok (0), undefinedError (127)}
}
AuthenticateClientResponseEs11 ::= [64] CHOICE { -- Tag 'BF40'
authenticateClientOk AuthenticateClientOkEs11,
authenticateClientError INTEGER {
eumCertificateInvalid(1),
eumCertificateExpired(2),
euiccCertificateInvalid(3),
euiccCertificateExpired(4),
euiccSignatureInvalid(5),
eventIdUnknown(6),
invalidTransactionId(7),
undefinedError(127)
}
}
AuthenticateClientOkEs11 ::= SEQUENCE {
transactionId TransactionId,
eventEntries SEQUENCE OF EventEntries
}
EventEntries ::= SEQUENCE {
eventId UTF8String,
rspServerAddress UTF8String
}
END

File diff suppressed because it is too large Load Diff

294
pySim/esim/bsp.py Normal file
View File

@@ -0,0 +1,294 @@
# Early proof-of-concept implementation of
# GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
# where BPP is the Bound Profile Package. So the full expansion is the
# "GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
#
# Originally (SGP.22 v2.x) this was called SCP03t, but it has since been
# renamed to BSP.
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# SGP.22 v3.0 Section 2.5.3:
# That block of data is split into segments of a maximum size of 1020 bytes (including the tag, length field and MAC).
import abc
from typing import List
import logging
# for BSP key derivation
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
from Cryptodome.Cipher import AES
from Cryptodome.Hash import CMAC
from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
# don't log by default
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
MAX_SEGMENT_SIZE = 1020
class BspAlgo(abc.ABC):
blocksize: int
def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
"""Return padding bytes towards multiple of N."""
if in_len % multiple == 0:
return b''
pad_cnt = multiple - (in_len % multiple)
return bytes([padding]) * pad_cnt
def _pad_to_multiple(self, indat: bytes, multiple: int, padding: int = 0) -> bytes:
"""Pad the input data to multiples of 'multiple'."""
return indat + self._get_padding(len(indat), multiple, padding)
def __str__(self):
return self.__class__.__name__
class BspAlgoCrypt(BspAlgo, abc.ABC):
def __init__(self, s_enc: bytes):
self.s_enc = s_enc
self.block_nr = 1
def encrypt(self, data:bytes) -> bytes:
"""Encrypt given input bytes using the key material given in constructor."""
padded_data = self._pad_to_multiple(data, self.blocksize)
block_nr = self.block_nr
ciphertext = self._encrypt(padded_data)
logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
block_nr, b2h(self.s_enc), b2h(data), b2h(padded_data), b2h(ciphertext))
return ciphertext
def decrypt(self, data:bytes) -> bytes:
"""Decrypt given input bytes using the key material given in constructor."""
return self._unpad(self._decrypt(data))
@abc.abstractmethod
def _unpad(self, padded: bytes) -> bytes:
"""Remove the padding from padded data."""
@abc.abstractmethod
def _encrypt(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
@abc.abstractmethod
def _decrypt(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
class BspAlgoCryptAES128(BspAlgoCrypt):
name = 'AES-CBC-128'
blocksize = 16
def _get_padding(self, in_len: int, multiple: int, padding: int = 0):
# SGP.22 section 2.6.4.4
# Append a byte with value '80' to the right of the data block;
# Append 0 to 15 bytes with value '00' so that the length of the padded data block
# is a multiple of 16 bytes.
return b'\x80' + super()._get_padding(in_len + 1, multiple, padding)
def _unpad(self, padded: bytes) -> bytes:
"""Remove the customary 80 00 00 ... padding used for AES."""
# first remove any trailing zero bytes
stripped = padded.rstrip(b'\0')
# then remove the final 80
assert stripped[-1] == 0x80
return stripped[:-1]
def _get_icv(self):
# The binary value of this number SHALL be left padded with zeroes to form a full block.
data = self.block_nr.to_bytes(self.blocksize, "big")
#iv = bytes([0] * (self.blocksize-1)) + b'\x01'
iv = bytes([0] * self.blocksize)
# This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
icv = cipher.encrypt(data)
logger.debug("_get_icv(block_nr=%u, data=%s) -> icv=%s", self.block_nr, b2h(data), b2h(icv))
self.block_nr = self.block_nr + 1
return icv
def _encrypt(self, data: bytes) -> bytes:
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv())
return cipher.encrypt(data)
def _decrypt(self, data: bytes) -> bytes:
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv())
return cipher.decrypt(data)
class BspAlgoMac(BspAlgo, abc.ABC):
l_mac = 0 # must be overridden by derived class
def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
self.s_mac = s_mac
self.mac_chain = initial_mac_chaining_value
def auth(self, tag: int, data: bytes) -> bytes:
assert tag in range (256)
# The input data used for C-MAC computation comprises the MAC Chaining value, the tag, the final length and the result of step 2
lcc = len(data) + self.l_mac
tag_and_length = bytes([tag]) + bertlv_encode_len(lcc)
temp_data = self.mac_chain + tag_and_length + data
old_mcv = self.mac_chain
c_mac = self._auth(temp_data)
# The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value.
ret = tag_and_length + data + c_mac
logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret))
return ret
def verify(self, ciphertext: bytes) -> bool:
mac_stripped = ciphertext[0:-self.l_mac]
mac_received = ciphertext[-self.l_mac:]
temp_data = self.mac_chain + mac_stripped
mac_computed = self._auth(temp_data)
if mac_received != mac_computed:
raise ValueError("MAC value not matching: received: %s, computed: %s" % (mac_received, mac_computed))
return mac_stripped
@abc.abstractmethod
def _auth(self, temp_data: bytes) -> bytes:
"""To be implemented by algorithm specific derived class."""
class BspAlgoMacAES128(BspAlgoMac):
name = 'AES-CMAC-128'
l_mac = 8
def _auth(self, temp_data: bytes) -> bytes:
# The full MAC value is computed using the MACing algorithm as defined in table 4c.
cmac = CMAC.new(self.s_mac, ciphermod=AES)
cmac.update(temp_data)
full_c_mac = cmac.digest()
# Subsequent MAC chaining values are the full result of step 4 of the previous data block
self.mac_chain = full_c_mac
# If the algorithm is AES-CBC-128 or SM4-CBC, the C-MAC value is the 8 most significant bytes of the result of step 4
return full_c_mac[0:8]
def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, host_id: bytes, eid, l : int = 16):
"""BSP protocol key derivation as per SGP.22 v3.0 Section 2.6.4.2"""
assert key_type <= 255
assert key_length <= 255
host_id_lv = bertlv_encode_len(len(host_id)) + host_id
eid_lv = bertlv_encode_len(len(eid)) + eid
shared_info = bytes([key_type, key_length]) + host_id_lv + eid_lv
logger.debug("kdf_shared_info: %s", b2h(shared_info))
# X9.63 Key Derivation Function with SHA256
xkdf = X963KDF(algorithm=hashes.SHA256(), length=l*3, sharedinfo=shared_info)
out = xkdf.derive(shared_secret)
logger.debug("kdf_out: %s", b2h(out))
initial_mac_chaining_value = out[0:l]
s_enc = out[l:2*l]
s_mac = out[l*2:3*l]
return s_enc, s_mac, initial_mac_chaining_value
class BspInstance:
"""An instance of the BSP crypto. Initialized once with the key material via constructor,
then the user can call any number of encrypt_and_mac cycles to protect plaintext and
generate the respective ciphertext."""
def __init__(self, s_enc: bytes, s_mac: bytes, initial_mcv: bytes):
logger.debug("%s(s_enc=%s, s_mac=%s, initial_mcv=%s)", self.__class__.__name__, b2h(s_enc), b2h(s_mac), b2h(initial_mcv))
self.c_algo = BspAlgoCryptAES128(s_enc)
self.m_algo = BspAlgoMacAES128(s_mac, initial_mcv)
TAG_LEN = 1
length_len = len(bertlv_encode_len(MAX_SEGMENT_SIZE))
self.max_payload_size = MAX_SEGMENT_SIZE - TAG_LEN - length_len - self.m_algo.l_mac
@classmethod
def from_kdf(cls, shared_secret: bytes, key_type: int, key_length: int, host_id: bytes, eid: bytes):
"""Convenience constructor for constructing an instance with keys from KDF."""
s_enc, s_mac, initial_mcv = bsp_key_derivation(shared_secret, key_type, key_length, host_id, eid)
return cls(s_enc, s_mac, initial_mcv)
def encrypt_and_mac_one(self, tag: int, plaintext:bytes) -> bytes:
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertex."""
assert tag <= 255
assert len(plaintext) <= self.max_payload_size
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
ciphered = self.c_algo.encrypt(plaintext)
maced = self.m_algo.auth(tag, ciphered)
return maced
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
remainder = plaintext
result = []
while len(remainder):
remaining_len = len(remainder)
if remaining_len < self.max_payload_size:
segment_len = remaining_len
segment = remainder
remainder = b''
else:
segment_len = self.max_payload_size
segment = remainder[0:segment_len]
remainder = remainder[segment_len:]
result.append(self.encrypt_and_mac_one(tag, segment))
return result
def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
"""MAC a single plaintext TLV. Returns the protected ciphertex."""
assert tag <= 255
assert len(plaintext) < self.max_payload_size
maced = self.m_algo.auth(tag, plaintext)
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
self.c_algo.block_nr += 1
return maced
def mac_only(self, tag: int, plaintext:bytes) -> List[bytes]:
remainder = plaintext
result = []
while len(remainder):
remaining_len = len(remainder)
if remaining_len < self.max_payload_size:
segment_len = remaining_len
segment = remainder
remainder = b''
else:
segment_len = self.max_payload_size
segment = remainder[0:segment_len]
remainder = remainder[segment_len:]
result.append(self.mac_only_one(tag, segment))
return result
def demac_and_decrypt_one(self, ciphertext: bytes) -> bytes:
payload = self.m_algo.verify(ciphertext)
tdict, l, val, remain = bertlv_parse_one(payload)
logger.debug("tag=%s, l=%u, val=%s, remain=%s", tdict, l, b2h(val), b2h(remain))
plaintext = self.c_algo.decrypt(val)
return plaintext
def demac_and_decrypt(self, ciphertext_list: List[bytes]) -> bytes:
plaintext_list = [self.demac_and_decrypt_one(x) for x in ciphertext_list]
return b''.join(plaintext_list)
def demac_only_one(self, ciphertext: bytes) -> bytes:
payload = self.m_algo.verify(ciphertext)
_tdict, _l, val, _remain = bertlv_parse_one(payload)
return val
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
plaintext_list = [self.demac_only_one(x) for x in ciphertext_list]
return b''.join(plaintext_list)

458
pySim/esim/es2p.py Normal file
View File

@@ -0,0 +1,458 @@
"""GSMA eSIM RSP ES2+ interface according to SGP.22 v2.5"""
# (C) 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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import requests
import logging
import json
from datetime import datetime
import time
import base64
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class ApiParam(abc.ABC):
"""A class reprsenting a single parameter in the ES2+ API."""
@classmethod
def verify_decoded(cls, data):
"""Verify the decoded reprsentation of a value. Should raise an exception if somthing is odd."""
pass
@classmethod
def verify_encoded(cls, data):
"""Verify the encoded reprsentation of a value. Should raise an exception if somthing is odd."""
pass
@classmethod
def encode(cls, data):
"""[Validate and] Encode the given value."""
cls.verify_decoded(data)
encoded = cls._encode(data)
cls.verify_decoded(encoded)
return encoded
@classmethod
def _encode(cls, data):
"""encoder function, typically [but not always] overridden by derived class."""
return data
@classmethod
def decode(cls, data):
"""[Validate and] Decode the given value."""
cls.verify_encoded(data)
decoded = cls._decode(data)
cls.verify_decoded(decoded)
return decoded
@classmethod
def _decode(cls, data):
"""decoder function, typically [but not always] overridden by derived class."""
return data
class ApiParamString(ApiParam):
"""Base class representing an API parameter of 'string' type."""
pass
class ApiParamInteger(ApiParam):
"""Base class representing an API parameter of 'integer' type."""
@classmethod
def _decode(cls, data):
return int(data)
@classmethod
def _encode(cls, data):
return str(data)
@classmethod
def verify_decoded(cls, data):
if not isinstance(data, int):
raise TypeError('Expected an integer input data type')
@classmethod
def verify_encoded(cls, data):
if not data.isdecimal():
raise ValueError('integer (%s) contains non-decimal characters' % data)
assert str(int(data)) == data
class ApiParamBoolean(ApiParam):
"""Base class representing an API parameter of 'boolean' type."""
@classmethod
def _encode(cls, data):
return bool(data)
class ApiParamFqdn(ApiParam):
"""String, as a list of domain labels concatenated using the full stop (dot, period) character as
separator between labels. Labels are restricted to the Alphanumeric mode character set defined in table 5
of ISO/IEC 18004"""
@classmethod
def verify_encoded(cls, data):
# FIXME
pass
class param:
class Iccid(ApiParamString):
"""String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
character F."""
@classmethod
def _encode(cls, data):
data = str(data)
# SGP.22 version prior to 2.2 do not require support for 19-digit ICCIDs, so let's always
# encode it with padding F at the end.
if len(data) == 19:
data += 'F'
return data
@classmethod
def verify_encoded(cls, data):
if len(data) not in [19, 20]:
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
@classmethod
def _decode(cls, data):
# strip trailing padding (if it's 20 digits)
if len(data) == 20 and data[-1] in ['F', 'f']:
data = data[:-1]
return data
@classmethod
def verify_decoded(cls, data):
data = str(data)
if len(data) not in [19, 20]:
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
if len(data) == 19:
decimal_part = data
else:
decimal_part = data[:-1]
final_part = data[-1:]
if final_part not in ['F', 'f'] and not final_part.isdecimal():
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
if not decimal_part.isdecimal():
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
class Eid(ApiParamString):
"""String of 32 decimal characters"""
@classmethod
def verify_encoded(cls, data):
if len(data) != 32:
raise ValueError('EID length invalid: "%s" (%u)' % (data, len(data)))
@classmethod
def verify_decoded(cls, data):
if not data.isdecimal():
raise ValueError('EID (%s) contains non-decimal characters' % data)
class ProfileType(ApiParamString):
pass
class MatchingId(ApiParamString):
pass
class ConfirmationCode(ApiParamString):
pass
class SmdsAddress(ApiParamFqdn):
pass
class SmdpAddress(ApiParamFqdn):
pass
class ReleaseFlag(ApiParamBoolean):
pass
class FinalProfileStatusIndicator(ApiParamString):
pass
class Timestamp(ApiParamString):
"""String format as specified by W3C: YYYY-MM-DDThh:mm:ssTZD"""
@classmethod
def _decode(cls, data):
return datetime.fromisoformat(data)
@classmethod
def _encode(cls, data):
return datetime.toisoformat(data)
class NotificationPointId(ApiParamInteger):
pass
class NotificationPointStatus(ApiParam):
pass
class ResultData(ApiParam):
@classmethod
def _decode(cls, data):
return base64.b64decode(data)
@classmethod
def _encode(cls, data):
return base64.b64encode(data)
class JsonResponseHeader(ApiParam):
"""SGP.22 section 6.5.1.4."""
@classmethod
def verify_decoded(cls, data):
fe_status = data.get('functionExecutionStatus')
if not fe_status:
raise ValueError('Missing mandatory functionExecutionStatus in header')
status = fe_status.get('status')
if not status:
raise ValueError('Missing mandatory status in header functionExecutionStatus')
if status not in ['Executed-Success', 'Executed-WithWarning', 'Failed', 'Expired']:
raise ValueError('Unknown/unspecified status "%s"' % status)
class HttpStatusError(Exception):
pass
class HttpHeaderError(Exception):
pass
class Es2PlusApiError(Exception):
"""Exception representing an error at the ES2+ API level (status != Executed)."""
def __init__(self, func_ex_status: dict):
self.status = func_ex_status['status']
sec = {
'subjectCode': None,
'reasonCode': None,
'subjectIdentifier': None,
'message': None,
}
actual_sec = func_ex_status.get('statusCodeData', None)
sec.update(actual_sec)
self.subject_code = sec['subjectCode']
self.reason_code = sec['reasonCode']
self.subject_id = sec['subjectIdentifier']
self.message = sec['message']
def __str__(self):
return f'{self.status}("{self.subject_code}","{self.reason_code}","{self.subject_id}","{self.message}")'
class Es2PlusApiFunction(abc.ABC):
"""Base classs for representing an ES2+ API Function."""
# the below class variables are expected to be overridden in derived classes
path = None
# dictionary of input parameters. key is parameter name, value is ApiParam class
input_params = {}
# list of mandatory input parameters
input_mandatory = []
# dictionary of output parameters. key is parameter name, value is ApiParam class
output_params = {}
# list of mandatory output parameters (for successful response)
output_mandatory = []
# expected HTTP status code of the response
expected_http_status = 200
def __init__(self, url_prefix: str, func_req_id: str, session):
self.url_prefix = url_prefix
self.func_req_id = func_req_id
self.session = session
def encode(self, data: dict, func_call_id: str) -> dict:
"""Validate an encode input dict into JSON-serializable dict for request body."""
output = {
'header': {
'functionRequesterIdentifier': self.func_req_id,
'functionCallIdentifier': func_call_id
}
}
for p in self.input_mandatory:
if not p in data:
raise ValueError('Mandatory input parameter %s missing' % p)
for p, v in data.items():
p_class = self.input_params.get(p)
if not p_class:
logger.warning('Unexpected/unsupported input parameter %s=%s', p, v)
output[p] = v
else:
output[p] = p_class.encode(v)
return output
def decode(self, data: dict) -> dict:
"""[further] Decode and validate the JSON-Dict of the respnse body."""
output = {}
# let's first do the header, it's special
if not 'header' in data:
raise ValueError('Mandatory output parameter "header" missing')
hdr_class = self.output_params.get('header')
output['header'] = hdr_class.decode(data['header'])
if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
raise Es2PlusApiError(output['header']['functionExecutionStatus'])
# we can only expect mandatory parameters to be present in case of successful execution
for p in self.output_mandatory:
if p == 'header':
continue
if not p in data:
raise ValueError('Mandatory output parameter "%s" missing' % p)
for p, v in data.items():
p_class = self.output_params.get(p)
if not p_class:
logger.warning('Unexpected/unsupported output parameter "%s"="%s"', p, v)
output[p] = v
else:
output[p] = p_class.decode(v)
return output
def call(self, data: dict, func_call_id:str, timeout=10) -> dict:
"""Make an API call to the ES2+ API endpoint represented by this object.
Input data is passed in `data` as json-serializable dict. Output data
is returned as json-deserialized dict."""
url = self.url_prefix + self.path
encoded = json.dumps(self.encode(data, func_call_id))
headers = {
'Content-Type': 'application/json',
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
}
logger.debug("HTTP REQ %s - '%s'" % (url, encoded))
response = self.session.post(url, data=encoded, headers=headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
logger.debug("HTTP RSP: %s" % (response.content))
if response.status_code != self.expected_http_status:
raise HttpStatusError(response)
if not response.headers.get('Content-Type').startswith(headers['Content-Type']):
raise HttpHeaderError(response)
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
raise HttpHeaderError(response)
return self.decode(response.json())
# ES2+ DownloadOrder function (SGP.22 section 5.3.1)
class DownloadOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/downloadOrder'
input_params = {
'eid': param.Eid,
'iccid': param.Iccid,
'profileType': param.ProfileType
}
output_params = {
'header': param.JsonResponseHeader,
'iccid': param.Iccid,
}
output_mandatory = ['header', 'iccid']
# ES2+ ConfirmOrder function (SGP.22 section 5.3.2)
class ConfirmOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/confirmOrder'
input_params = {
'iccid': param.Iccid,
'eid': param.Eid,
'matchingId': param.MatchingId,
'confirmationCode': param.ConfirmationCode,
'smdsAddress': param.SmdsAddress,
'releaseFlag': param.ReleaseFlag,
}
input_mandatory = ['iccid', 'releaseFlag']
output_params = {
'header': param.JsonResponseHeader,
'eid': param.Eid,
'matchingId': param.MatchingId,
'smdpAddress': param.SmdpAddress,
}
output_mandatory = ['header', 'matchingId']
# ES2+ CancelOrder function (SGP.22 section 5.3.3)
class CancelOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/cancelOrder'
input_params = {
'iccid': param.Iccid,
'eid': param.Eid,
'matchingId': param.MatchingId,
'finalProfileStatusIndicator': param.FinalProfileStatusIndicator,
}
input_mandatory = ['finalProfileStatusIndicator', 'iccid']
output_params = {
'header': param.JsonResponseHeader,
}
output_mandatory = ['header']
# ES2+ ReleaseProfile function (SGP.22 section 5.3.4)
class ReleaseProfile(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/releaseProfile'
input_params = {
'iccid': param.Iccid,
}
input_mandatory = ['iccid']
output_params = {
'header': param.JsonResponseHeader,
}
output_mandatory = ['header']
# ES2+ HandleDownloadProgress function (SGP.22 section 5.3.5)
class HandleDownloadProgressInfo(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/handleDownloadProgressInfo'
input_params = {
'eid': param.Eid,
'iccid': param.Iccid,
'profileType': param.ProfileType,
'timestamp': param.Timestamp,
'notificationPointId': param.NotificationPointId,
'notificationPointStatus': param.NotificationPointStatus,
'resultData': param.ResultData,
}
input_mandatory = ['iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
expected_http_status = 204
class Es2pApiClient:
"""Main class representing a full ES2+ API client. Has one method for each API function."""
def __init__(self, url_prefix:str, func_req_id:str, server_cert_verify: str = None, client_cert: str = None):
self.func_id = 0
self.session = requests.Session()
if server_cert_verify:
self.session.verify = server_cert_verify
if client_cert:
self.session.cert = client_cert
self.downloadOrder = DownloadOrder(url_prefix, func_req_id, self.session)
self.confirmOrder = ConfirmOrder(url_prefix, func_req_id, self.session)
self.cancelOrder = CancelOrder(url_prefix, func_req_id, self.session)
self.releaseProfile = ReleaseProfile(url_prefix, func_req_id, self.session)
self.handleDownloadProgressInfo = HandleDownloadProgressInfo(url_prefix, func_req_id, self.session)
def _gen_func_id(self) -> str:
"""Generate the next function call id."""
self.func_id += 1
return 'FCI-%u-%u' % (time.time(), self.func_id)
def call_downloadOrder(self, data: dict) -> dict:
"""Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
return self.downloadOrder.call(data, self._gen_func_id())
def call_confirmOrder(self, data: dict) -> dict:
"""Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
return self.confirmOrder.call(data, self._gen_func_id())
def call_cancelOrder(self, data: dict) -> dict:
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
return self.cancelOrder.call(data, self._gen_func_id())
def call_releaseProfile(self, data: dict) -> dict:
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
return self.releaseProfile.call(data, self._gen_func_id())
def call_handleDownloadProgressInfo(self, data: dict) -> dict:
"""Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
return self.handleDownloadProgressInfo.call(data, self._gen_func_id())

185
pySim/esim/es8p.py Normal file
View File

@@ -0,0 +1,185 @@
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+
# as per SGP22 v3.0 Section 5.5
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Dict, List, Optional
from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len
import pySim.esim.rsp as rsp
from pySim.esim.bsp import BspInstance
# Given that GSMA RSP uses ASN.1 in a very weird way, we actually cannot encode the full data type before
# signing, but we have to build parts of it separately first, then sign that, so we can put the signature
# into the same sequence as the signed data. We use the existing pySim TLV code for this.
def wrap_as_der_tlv(tag: int, val: bytes) -> bytes:
"""Wrap the 'value' into a DER-encoded TLV."""
return bertlv_encode_tag(tag) + bertlv_encode_len(len(val)) + val
def gen_init_sec_chan_signed_part(iscsp: Dict) -> bytes:
"""Generate the concatenated remoteOpId, transactionId, controlRefTemplate and smdpOtpk data objects
without the outer SEQUENCE tag / length or the remainder of initialiseSecureChannel, as is required
for signing purpose."""
out = b''
out += wrap_as_der_tlv(0x82, bytes([iscsp['remoteOpId']]))
out += wrap_as_der_tlv(0x80, iscsp['transactionId'])
crt = iscsp['controlRefTemplate']
out_crt = wrap_as_der_tlv(0x80, crt['keyType'])
out_crt += wrap_as_der_tlv(0x81, crt['keyLen'])
out_crt += wrap_as_der_tlv(0x84, crt['hostId'])
out += wrap_as_der_tlv(0xA6, out_crt)
out += wrap_as_der_tlv(0x5F49, iscsp['smdpOtpk'])
return out
# SGP.22 Section 5.5.1
def gen_initialiseSecureChannel(transactionId: str, host_id: bytes, smdp_otpk: bytes, euicc_otpk: bytes, dp_pb):
"""Generate decoded representation of (signed) initialiseSecureChannel (SGP.22 5.5.2)"""
init_scr = { 'remoteOpId': 1, # installBoundProfilePackage
'transactionId': h2b(transactionId),
# GlobalPlatform Card Specification Amendment F [13] section 6.5.2.3 for the Mutual Authentication Data Field
'controlRefTemplate': { 'keyType': bytes([0x88]), 'keyLen': bytes([16]), 'hostId': host_id },
'smdpOtpk': smdp_otpk, # otPK.DP.KA
}
to_sign = gen_init_sec_chan_signed_part(init_scr) + wrap_as_der_tlv(0x5f49, euicc_otpk)
init_scr['smdpSign'] = dp_pb.ecdsa_sign(to_sign)
return init_scr
def gen_replace_session_keys(ppk_enc: bytes, ppk_cmac: bytes, initial_mcv: bytes) -> bytes:
"""Generate encoded (but unsigned) ReplaceSessionKeysReqest DO (SGP.22 5.5.4)"""
rsk = { 'ppkEnc': ppk_enc, 'ppkCmac': ppk_cmac, 'initialMacChainingValue': initial_mcv }
return rsp.asn1.encode('ReplaceSessionKeysRequest', rsk)
class ProfileMetadata:
"""Representation of Profile metadata. Right now only the mandatory bits are
supported, but in general this should follow the StoreMetadataRequest of SGP.22 5.5.3"""
def __init__(self, iccid_bin: bytes, spn: str, profile_name: str):
self.iccid_bin = iccid_bin
self.spn = spn
self.profile_name = profile_name
def gen_store_metadata_request(self) -> bytes:
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
smr = {
'iccid': self.iccid_bin,
'serviceProviderName': self.spn,
'profileName': self.profile_name,
}
return rsp.asn1.encode('StoreMetadataRequest', smr)
class ProfilePackage:
def __init__(self, metadata: Optional[ProfileMetadata] = None):
self.metadata = metadata
class UnprotectedProfilePackage(ProfilePackage):
"""Representing an unprotected profile package (UPP) as defined in SGP.22 Section 2.5.2"""
@classmethod
def from_der(cls, der: bytes, metadata: Optional[ProfileMetadata] = None) -> 'UnprotectedProfilePackage':
"""Load an UPP from its DER representation."""
inst = cls(metadata=metadata)
cls.der = der
# TODO: we later certainly want to parse it so we can perform modification (IMSI, key material, ...)
# just like in the traditional SIM/USIM dynamic data phase at the end of personalization
return inst
def to_der(self):
"""Return the DER representation of the UPP."""
# TODO: once we work on decoded structures, we may want to re-encode here
return self.der
class ProtectedProfilePackage(ProfilePackage):
"""Representing a protected profile package (PPP) as defined in SGP.22 Section 2.5.3"""
@classmethod
def from_upp(cls, upp: UnprotectedProfilePackage, bsp: BspInstance) -> 'ProtectedProfilePackage':
"""Generate the PPP as a sequence of encrypted and MACed Command TLVs representing the UPP"""
inst = cls(metadata=upp.metadata)
inst.upp = upp
# store ppk-enc, ppc-mac
inst.ppk_enc = bsp.c_algo.s_enc
inst.ppk_mac = bsp.m_algo.s_mac
inst.initial_mcv = bsp.m_algo.mac_chain
inst.encoded = bsp.encrypt_and_mac(0x86, upp.to_der())
return inst
#def __val__(self):
#return self.encoded
class BoundProfilePackage(ProfilePackage):
"""Representing a bound profile package (BPP) as defined in SGP.22 Section 2.5.4"""
@classmethod
def from_ppp(cls, ppp: ProtectedProfilePackage):
inst = cls()
inst.upp = None
inst.ppp = ppp
return inst
@classmethod
def from_upp(cls, upp: UnprotectedProfilePackage):
inst = cls()
inst.upp = upp
inst.ppp = None
return inst
def encode(self, ss: 'RspSessionState', dp_pb: 'CertAndPrivkey') -> bytes:
"""Generate a bound profile package (SGP.22 2.5.4)."""
def encode_seq(tag: int, sequence: List[bytes]) -> bytes:
"""Encode a "sequenceOfXX" as specified in SGP.22 specifying the raw SEQUENCE OF tag,
and assuming the caller provides the fully-encoded (with TAG + LEN) member TLVs."""
payload = b''.join(sequence)
return bertlv_encode_tag(tag) + bertlv_encode_len(len(payload)) + payload
bsp = BspInstance.from_kdf(ss.shared_secret, 0x88, 16, ss.host_id, h2b(ss.eid))
iscr = gen_initialiseSecureChannel(ss.transactionId, ss.host_id, ss.smdp_otpk, ss.euicc_otpk, dp_pb)
# generate unprotected input data
conf_idsp_bin = rsp.asn1.encode('ConfigureISDPRequest', {})
if self.upp:
smr_bin = self.upp.metadata.gen_store_metadata_request()
else:
smr_bin = self.ppp.metadata.gen_store_metadata_request()
# we don't use rsp.asn1.encode('boundProfilePackage') here, as the BSP already provides
# fully encoded + MACed TLVs including their tag + length values. We cannot put those as
# 'value' input into an ASN.1 encoder, as that would double the TAG + LENGTH :(
# 'initialiseSecureChannelRequest'
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
# firstSequenceOf87
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
# sequenceOF88
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
if self.ppp: # we have to use session keys
rsk_bin = gen_replace_session_keys(self.ppp.ppk_enc, self.ppp.ppk_mac, self.ppp.initial_mcv)
# secondSequenceOf87
bpp_seq += encode_seq(0xa2, bsp.encrypt_and_mac(0x87, rsk_bin))
else:
self.ppp = ProtectedProfilePackage.from_upp(self.upp, bsp)
# 'sequenceOf86'
bpp_seq += encode_seq(0xa3, self.ppp.encoded)
# manual DER encode: wrap in outer SEQUENCE
return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq

98
pySim/esim/rsp.py Normal file
View File

@@ -0,0 +1,98 @@
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning)
# as per SGP22 v3.0
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional
import shelve
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography import x509
from pySim.esim import compile_asn1_subdir
asn1 = compile_asn1_subdir('rsp')
class RspSessionState:
"""Encapsulates the state of a RSP session. It is created during the initiateAuthentication
and subsequently used by further API calls using the same transactionId. The session state
is removed either after cancelSession or after notification.
TODO: add some kind of time based expiration / garbage collection."""
def __init__(self, transactionId: str, serverChallenge: bytes, ci_cert_id: bytes):
self.transactionId = transactionId
self.serverChallenge = serverChallenge
# used at a later point between API calsl
self.ci_cert_id = ci_cert_id
self.euicc_cert: Optional[x509.Certificate] = None
self.eum_cert: Optional[x509.Certificate] = None
self.eid: Optional[bytes] = None
self.profileMetadata: Optional['ProfileMetadata'] = None
self.smdpSignature2_do = None
# really only needed while processing getBoundProfilePackage request?
self.euicc_otpk: Optional[bytes] = None
self.smdp_ot: Optional[ec.EllipticCurvePrivateKey] = None
self.smdp_otpk: Optional[bytes] = None
self.host_id: Optional[bytes] = None
self.shared_secret: Optional[bytes] = None
def __getstate__(self):
"""helper function called when pickling the object to persistent storage. We must pickel all
members that are not pickle-able."""
state = self.__dict__.copy()
# serialize eUICC certificate as DER
if state.get('euicc_cert', None):
state['_euicc_cert'] = self.euicc_cert.public_bytes(Encoding.DER)
del state['euicc_cert']
# serialize EUM certificate as DER
if state.get('eum_cert', None):
state['_eum_cert'] = self.eum_cert.public_bytes(Encoding.DER)
del state['eum_cert']
# serialize one-time SMDP private key to integer + curve
if state.get('smdp_ot', None):
state['_smdp_otsk'] = self.smdp_ot.private_numbers().private_value
state['_smdp_ot_curve'] = self.smdp_ot.curve
del state['smdp_ot']
return state
def __setstate__(self, state):
"""helper function called when unpickling the object from persistent storage. We must recreate all
members from the state generated in __getstate__ above."""
# restore eUICC certificate from DER
if '_euicc_cert' in state:
self.euicc_cert = x509.load_der_x509_certificate(state['_euicc_cert'])
del state['_euicc_cert']
else:
self.euicc_cert = None
# restore EUM certificate from DER
if '_eum_cert' in state:
self.eum_cert = x509.load_der_x509_certificate(state['_eum_cert'])
del state['_eum_cert']
# restore one-time SMDP private key from integer + curve
if state.get('_smdp_otsk', None):
self.smdp_ot = ec.derive_private_key(state['_smdp_otsk'], state['_smdp_ot_curve'])
# FIXME: how to add the public key from smdp_otpk to an instance of EllipticCurvePrivateKey?
del state['_smdp_otsk']
del state['_smdp_ot_curve']
# automatically recover all the remainig state
self.__dict__.update(state)
class RspSessionStore(shelve.DbfilenameShelf):
"""A derived class as wrapper around the database-backed non-volatile storage 'shelve', in case we might
need to extend it in the future. We use it to store RspSessionState objects indexed by transactionId."""

308
pySim/esim/saip/__init__.py Normal file
View File

@@ -0,0 +1,308 @@
# Implementation of SimAlliance/TCA Interoperable Profile handling
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import io
from typing import Tuple, List, Optional, Dict, Union
import asn1tools
from pySim.utils import bertlv_parse_tag, bertlv_parse_len
from pySim.ts_102_221 import FileDescriptor
from pySim.construct import build_construct
from pySim.esim import compile_asn1_subdir
from pySim.esim.saip import templates
asn1 = compile_asn1_subdir('saip')
class File:
"""Internal representation of a file in a profile filesystem.
Parameters:
pename: Name string of the profile element
l: List of tuples [fileDescriptor, fillFileContent, fillFileOffset profile elements]
template: Applicable FileTemplate describing defaults as per SAIP spec
"""
def __init__(self, pename: str, l: Optional[List[Tuple]] = None, template: Optional[templates.FileTemplate] = None):
self.pe_name = pename
self.template = template
self.fileDescriptor = {}
self.stream = None
# apply some defaults from profile
if self.template:
self.from_template(self.template)
print("after template: %s" % repr(self))
if l:
self.from_tuples(l)
def from_template(self, template: templates.FileTemplate):
"""Determine defaults for file based on given FileTemplate."""
fdb_dec = {}
self.rec_len = None
if template.fid:
self.fileDescriptor['fileID'] = template.fid.to_bytes(2, 'big')
if template.sfi:
self.fileDescriptor['shortEFID'] = bytes([template.sfi])
if template.arr:
self.fileDescriptor['securityAttributesReferenced'] = bytes([template.arr])
# All the files defined in the templates shall have, by default, shareable/not-shareable bit in the file descriptor set to "shareable".
fdb_dec['shareable'] = True
if template.file_type in ['LF', 'CY']:
fdb_dec['file_type'] = 'working_ef'
if template.rec_len:
self.record_len = template.rec_len
if template.nb_rec and template.rec_len:
self.fileDescriptor['efFileSize'] = (template.nb_rec * template.rec_len).to_bytes(2, 'big') # FIXME
if template.file_type == 'LF':
fdb_dec['structure'] = 'linear_fixed'
elif template.file_type == 'CY':
fdb_dec['structure'] = 'cyclic'
elif template.file_type in ['TR', 'BT']:
fdb_dec['file_type'] = 'working_ef'
if template.file_size:
self.fileDescriptor['efFileSize'] = template.file_size.to_bytes(2, 'big') # FIXME
if template.file_type == 'BT':
fdb_dec['structure'] = 'ber_tlv'
elif template.file_type == 'TR':
fdb_dec['structure'] = 'transparent'
elif template.file_type in ['MF', 'DF', 'ADF']:
fdb_dec['file_type'] = 'df'
fdb_dec['structure'] = 'no_info_given'
# build file descriptor based on above input data
fd_dict = {'file_descriptor_byte': fdb_dec}
if self.rec_len:
fd_dict['record_len'] = self.rec_len
self.fileDescriptor['fileDescriptor'] = build_construct(FileDescriptor._construct, fd_dict)
# FIXME: default_val
# FIXME: high_update
# FIXME: params?
def from_tuples(self, l:List[Tuple]):
"""Parse a list of fileDescriptor, fillFileContent, fillFileOffset tuples into this instance."""
def get_fileDescriptor(l:List[Tuple]):
for k, v in l:
if k == 'fileDescriptor':
return v
fd = get_fileDescriptor(l)
if not fd:
raise ValueError("No fileDescriptor found")
self.fileDescriptor.update(dict(fd))
self.stream = self.linearize_file_content(l)
def to_tuples(self) -> List[Tuple]:
"""Generate a list of fileDescriptor, fillFileContent, fillFileOffset tuples into this instance."""
raise NotImplementedError
@staticmethod
def linearize_file_content(l: List[Tuple]) -> Optional[io.BytesIO]:
"""linearize a list of fillFileContent / fillFileOffset tuples into a stream of bytes."""
stream = io.BytesIO()
for k, v in l:
if k == 'doNotCreate':
return None
if k == 'fileDescriptor':
pass
elif k == 'fillFileOffset':
stream.write(b'\xff' * v)
elif k == 'fillFileContent':
stream.write(v)
else:
return ValueError("Unknown key '%s' in tuple list" % k)
return stream
def __str__(self) -> str:
return "File(%s)" % self.pe_name
def __repr__(self) -> str:
return "File(%s): %s" % (self.pe_name, self.fileDescriptor)
class ProfileElement:
"""Class representing a Profile Element (PE) within a SAIP Profile."""
FILE_BEARING = ['mf', 'cd', 'telecom', 'usim', 'opt-usim', 'isim', 'opt-isim', 'phonebook', 'gsm-access',
'csim', 'opt-csim', 'eap', 'df-5gs', 'df-saip', 'df-snpn', 'df-5gprose', 'iot', 'opt-iot']
def _fixup_sqnInit_dec(self) -> None:
"""asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
this."""
if self.type != 'akaParameter':
return
sqn_init = self.decoded.get('sqnInit', None)
if not sqn_init:
return
# this weird '0x' value in a string is what we get from our (slightly hacked) ASN.1 syntax
if sqn_init == '0x000000000000':
# SEQUENCE (SIZE (32)) OF OCTET STRING (SIZE (6))
self.decoded['sqnInit'] = [b'\x00'*6] * 32
def _fixup_sqnInit_enc(self) -> None:
"""asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
this."""
if self.type != 'akaParameter':
return
sqn_init = self.decoded.get('sqnInit', None)
if not sqn_init:
return
for s in sqn_init:
if any(s):
return
# none of the fields were initialized with a non-default (non-zero) value, so we can skip it
del self.decoded['sqnInit']
def parse_der(self, der: bytes) -> None:
"""Parse a sequence of PE and store the result in instance attributes."""
self.type, self.decoded = asn1.decode('ProfileElement', der)
# work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
self._fixup_sqnInit_dec()
@property
def header_name(self) -> str:
"""Return the name of the header field within the profile element."""
# unneccessarry compliaction by inconsistent naming :(
if self.type.startswith('opt-'):
return self.type.replace('-','') + '-header'
return self.type + '-header'
@property
def header(self):
"""Return the decoded ProfileHeader."""
return self.decoded.get(self.header_name, None)
@property
def templateID(self):
"""Return the decoded templateID used by this profile element (if any)."""
return self.decoded.get('templateID', None)
@property
def files(self):
"""Return dict of decoded 'File' ASN.1 items."""
if not self.type in self.FILE_BEARING:
return {}
return {k:v for (k,v) in self.decoded.items() if k not in ['templateID', self.header_name]}
@classmethod
def from_der(cls, der: bytes) -> 'ProfileElement':
"""Construct an instance from given raw, DER encoded bytes."""
inst = cls()
inst.parse_der(der)
return inst
def to_der(self) -> bytes:
"""Build an encoded DER representation of the instance."""
# work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
self._fixup_sqnInit_enc()
return asn1.encode('ProfileElement', (self.type, self.decoded))
def __str__(self) -> str:
return self.type
def bertlv_first_segment(binary: bytes) -> Tuple[bytes, bytes]:
"""obtain the first segment of a binary concatenation of BER-TLV objects.
Returns: tuple of first TLV and remainder."""
_tagdict, remainder = bertlv_parse_tag(binary)
length, remainder = bertlv_parse_len(remainder)
tl_length = len(binary) - len(remainder)
tlv_length = tl_length + length
return binary[:tlv_length], binary[tlv_length:]
class ProfileElementSequence:
"""A sequence of ProfileElement objects, which is the overall representation of an eSIM profile."""
def __init__(self):
self.pe_list: List[ProfileElement] = None
self.pe_by_type: Dict = {}
self.pes_by_naa: Dict = {}
def get_pes_for_type(self, tname: str) -> List[ProfileElement]:
"""Return list of profile elements present for given profile element type."""
return self.pe_by_type.get(tname, [])
def get_pe_for_type(self, tname: str) -> Optional[ProfileElement]:
"""Return a single profile element for given profile element type. Works only for
types of which there is only a signle instance in the PE Sequence!"""
l = self.get_pes_for_type(tname)
if len(l) == 0:
return None
assert len(l) == 1
return l[0]
def parse_der(self, der: bytes) -> None:
"""Parse a sequence of PE and store the result in self.pe_list."""
self.pe_list = []
remainder = der
while len(remainder):
first_tlv, remainder = bertlv_first_segment(remainder)
self.pe_list.append(ProfileElement.from_der(first_tlv))
self._process_pelist()
def _process_pelist(self) -> None:
self._rebuild_pe_by_type()
self._rebuild_pes_by_naa()
def _rebuild_pe_by_type(self) -> None:
self.pe_by_type = {}
# build a dict {pe_type: [pe, pe, pe]}
for pe in self.pe_list:
if pe.type in self.pe_by_type:
self.pe_by_type[pe.type].append(pe)
else:
self.pe_by_type[pe.type] = [pe]
def _rebuild_pes_by_naa(self) -> None:
"""rebuild the self.pes_by_naa dict {naa: [ [pe, pe, pe], [pe, pe] ]} form,
which basically means for every NAA there's a lsit of instances, and each consists
of a list of a list of PEs."""
self.pres_by_naa = {}
petype_not_naa_related = ['securityDomain', 'rfm', 'application', 'end']
naa = ['mf', 'usim', 'isim', 'csim']
cur_naa = None
cur_naa_list = []
for pe in self.pe_list:
# skip all PE that are not related to NAA
if pe.type in petype_not_naa_related:
continue
if pe.type in naa:
if cur_naa:
if not cur_naa in self.pes_by_naa:
self.pes_by_naa[cur_naa] = []
self.pes_by_naa[cur_naa].append(cur_naa_list)
cur_naa = pe.type
cur_naa_list = []
cur_naa_list.append(pe)
# append the final one
if cur_naa and len(cur_naa_list) > 0:
if not cur_naa in self.pes_by_naa:
self.pes_by_naa[cur_naa] = []
self.pes_by_naa[cur_naa].append(cur_naa_list)
@classmethod
def from_der(cls, der: bytes) -> 'ProfileElementSequence':
"""Construct an instance from given raw, DER encoded bytes."""
inst = cls()
inst.parse_der(der)
return inst
def to_der(self) -> bytes:
"""Build an encoded DER representation of the instance."""
out = b''
for pe in self.pe_list:
out += pe.to_der()
return out
def __repr__(self) -> str:
return "PESequence(%s)" % ', '.join([str(x) for x in self.pe_list])
def __iter__(self) -> str:
yield from self.pe_list

View File

@@ -0,0 +1,90 @@
# Data sources: Provding data for profile personalization
#
# (C) 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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import secrets
from Cryptodome.Random import get_random_bytes
class DataSource(abc.ABC):
"""Base class for something that can provide data during a personalization process."""
@abc.abstractmethod
def generate_one(self):
pass
class DataSourceFixed(DataSource):
"""A data source that provides a fixed value (of any type).
Parameters:
fixed_value: The fixed value that shall be used during each data generation
"""
def __init__(self, fixed_value, **kwargs):
self.fixed_value = fixed_value
super().__init__(**kwargs)
def generate_one(self):
return self.fixed_value
class DataSourceIncrementing(DataSource):
"""A data source that provides incrementing integer numbers.
Parameters:
base_value: The start value (value returned during first data generation)
step_size: Increment step size (Default: 1)
"""
def __init__(self, base_value: int, **kwargs):
self.base_value = int(base_value)
self.step_size = kwargs.pop('step_size', 1)
self.i = 0
super().__init__(**kwargs)
def generate_one(self):
val = self.base_value + self.i
self.i += self.step_size
return val
class DataSourceRandomBytes(DataSource):
"""A data source that provides a configurable number of random bytes.
Parameters:
size: Number of bytes to generate each turn
"""
def __init__(self, size: int, **kwargs):
self.size = size
super().__init__(**kwargs)
def generate_one(self):
return get_random_bytes(self.size)
class DataSourceRandomUInt(DataSource):
"""A data source that provides a configurable unsigned integer value.
Parameters:
below: Number one greater than the maximum permitted random unsigned integer
"""
def __init__(self, below: int, **kwargs):
self.below = below
super().__init__(**kwargs)
def generate_one(self):
return secrets.randbelow(self.below)

77
pySim/esim/saip/oid.py Normal file
View File

@@ -0,0 +1,77 @@
# Implementation of SimAlliance/TCA Interoperable Profile OIDs
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List, Union
class OID:
@staticmethod
def intlist_from_str(instr: str) -> List[int]:
return [int(x) for x in instr.split('.')]
@staticmethod
def str_from_intlist(intlist: List[int]) -> str:
return '.'.join([str(x) for x in intlist])
def __init__(self, initializer: Union[List[int], str]):
if isinstance(initializer, str):
self.intlist = self.intlist_from_str(initializer)
else:
self.intlist = initializer
def __str__(self) -> str:
return self.str_from_intlist(self.intlist)
def __repr__(self) -> str:
return 'OID(%s)' % (str(self))
class eOID(OID):
"""OID helper for TCA eUICC prefix"""
__prefix = [2,23,143,1]
def __init__(self, initializer):
if isinstance(initializer, str):
initializer = self.intlist_from_str(initializer)
super().__init__(self.__prefix + initializer)
MF = eOID("2.1")
DF_CD = eOID("2.2")
DF_TELECOM = eOID("2.3")
DF_TELECOM_v2 = eOID("2.3.2")
ADF_USIM_by_default = eOID("2.4")
ADF_USIM_by_default_v2 = eOID("2.4.2")
ADF_USIM_not_by_default = eOID("2.5")
ADF_USIM_not_by_default_v2 = eOID("2.5.2")
ADF_USIM_not_by_default_v3 = eOID("2.5.3")
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
ADF_ISIM_by_default = eOID("2.8")
ADF_ISIM_not_by_default = eOID("2.9")
ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
ADF_CSIM_by_default = eOID("2.10")
ADF_CSIM_by_default_v2 = eOID("2.10.2")
ADF_CSIM_not_by_default = eOID("2.11")
ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
DF_EAP = eOID("2.12")
DF_5GS = eOID("2.13")
DF_5GS_v2 = eOID("2.13.2")
DF_5GS_v3 = eOID("2.13.3")
DF_5GS_v4 = eOID("2.13.4")
DF_SAIP = eOID("2.14")
DF_SNPN = eOID("2.15")
DF_5GProSe = eOID("2.16")
IoT_default = eOID("2.17")
IoT_default = eOID("2.18")

View File

@@ -0,0 +1,324 @@
# Implementation of SimAlliance/TCA Interoperable Profile handling
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import io
from typing import List, Tuple
from pySim.tlv import camel_to_snake
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
from pySim.esim.saip import ProfileElement, ProfileElementSequence
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
return list(filter(lambda x: x[0] not in unwanted_keys, l))
def file_replace_content(file: List[Tuple], new_content: bytes):
"""Completely replace all fillFileContent of a decoded 'File' with the new_content."""
# use [:] to avoid making a copy, as we're doing in-place modification of the list here
file[:] = remove_unwanted_tuples_from_list(file, ['fillFileContent', 'fillFileOffset'])
file.append(('fillFileContent', new_content))
return file
class ClassVarMeta(abc.ABCMeta):
"""Metaclass that puts all additional keyword-args into the class. We use this to have one
class definition for something like a PIN, and then have derived classes for PIN1, PIN2, ..."""
def __new__(metacls, name, bases, namespace, **kwargs):
#print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
x = super().__new__(metacls, name, bases, namespace)
for k, v in kwargs.items():
setattr(x, k, v)
setattr(x, 'name', camel_to_snake(name))
return x
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
"""Base class representing a part of the eSIM profile that is configurable during the
personalization process (with dynamic data from elsewhere)."""
def __init__(self, input_value):
self.input_value = input_value # the raw input value as given by caller
self.value = None # the processed input value (e.g. with check digit) as produced by validate()
def validate(self):
"""Optional validation method. Can be used by derived classes to perform validation
of the input value (self.value). Will raise an exception if validation fails."""
# default implementation: simply copy input_value over to value
self.value = self.input_value
@abc.abstractmethod
def apply(self, pes: ProfileElementSequence):
pass
class Iccid(ConfigurableParameter):
"""Configurable ICCID. Expects the value to be a string of decimal digits.
If the string of digits is only 18 digits long, a Luhn check digit will be added."""
def validate(self):
# convert to string as it migt be an integer
iccid_str = str(self.input_value)
if len(iccid_str) < 18 or len(iccid_str) > 20:
raise ValueError('ICCID must be 18, 19 or 20 digits long')
if not iccid_str.isdecimal():
raise ValueError('ICCID must only contain decimal digits')
self.value = sanitize_iccid(iccid_str)
def apply(self, pes: ProfileElementSequence):
# patch the header
pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(self.value, 20))
# patch MF/EF.ICCID
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(self.value)))
class Imsi(ConfigurableParameter):
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
the last digit of the IMSI."""
def validate(self):
# convert to string as it migt be an integer
imsi_str = str(self.input_value)
if len(imsi_str) < 6 or len(imsi_str) > 15:
raise ValueError('IMSI must be 6..15 digits long')
if not imsi_str.isdecimal():
raise ValueError('IMSI must only contain decimal digits')
self.value = imsi_str
def apply(self, pes: ProfileElementSequence):
imsi_str = self.value
# we always use the least significant byte of the IMSI as ACC
acc = (1 << int(imsi_str[-1]))
# patch ADF.USIM/EF.IMSI
for pe in pes.get_pes_for_type('usim'):
file_replace_content(pe.decoded['ef-imsi'], h2b(enc_imsi(imsi_str)))
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
# TODO: DF.GSM_ACCESS if not linked?
class SdKey(ConfigurableParameter, metaclass=ClassVarMeta):
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
# these will be set by derived classes
key_type = None
key_id = None
kvn = None
key_usage_qual = None
permitted_len = None
def validate(self):
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
raise ValueError('Value must be of bytes-like type')
if self.permitted_len:
if len(self.input_value) not in self.permitted_len:
raise ValueError('Value length must be %s' % self.permitted_len)
self.value = self.input_value
def _apply_sd(self, pe: ProfileElement):
assert pe.type == 'securityDomain'
for key in pe.decoded['keyList']:
if key['keyIdentifier'][0] == self.key_id and key['keyVersionNumber'][0] == self.kvn:
assert len(key['keyComponents']) == 1
key['keyComponents'][0]['keyData'] = self.value
return
# Could not find matching key to patch, create a new one
key = {
'keyUsageQualifier': bytes([self.key_usage_qual]),
'keyIdentifier': bytes([self.key_id]),
'keyVersionNumber': bytes([self.kvn]),
'keyComponents': [
{ 'keyType': bytes([self.key_type]), 'keyData': self.value },
]
}
pe.decoded['keyList'].append(key)
def apply(self, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('securityDomain'):
self._apply_sd(pe)
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
class SdKeyScp80_01Kic(SdKeyScp80_01, key_id=0x01, key_usage_qual=0x18): # FIXME: ordering?
pass
class SdKeyScp80_01Kid(SdKeyScp80_01, key_id=0x02, key_usage_qual=0x14):
pass
class SdKeyScp80_01Kik(SdKeyScp80_01, key_id=0x03, key_usage_qual=0x48):
pass
class SdKeyScp81_01(SdKey, kvn=0x81): # FIXME
pass
class SdKeyScp81_01Psk(SdKeyScp81_01, key_id=0x01, key_type=0x85, key_usage_qual=0x3C):
pass
class SdKeyScp81_01Dek(SdKeyScp81_01, key_id=0x02, key_type=0x88, key_usage_qual=0x48):
pass
class SdKeyScp02_20(SdKey, kvn=0x20, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
class SdKeyScp02_20Enc(SdKeyScp02_20, key_id=0x01, key_usage_qual=0x18):
pass
class SdKeyScp02_20Mac(SdKeyScp02_20, key_id=0x02, key_usage_qual=0x14):
pass
class SdKeyScp02_20Dek(SdKeyScp02_20, key_id=0x03, key_usage_qual=0x48):
pass
class SdKeyScp03_30(SdKey, kvn=0x30, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
class SdKeyScp03_30Enc(SdKeyScp03_30, key_id=0x01, key_usage_qual=0x18):
pass
class SdKeyScp03_30Mac(SdKeyScp03_30, key_id=0x02, key_usage_qual=0x14):
pass
class SdKeyScp03_30Dek(SdKeyScp03_30, key_id=0x03, key_usage_qual=0x48):
pass
class SdKeyScp03_31(SdKey, kvn=0x31, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
class SdKeyScp03_31Enc(SdKeyScp03_31, key_id=0x01, key_usage_qual=0x18):
pass
class SdKeyScp03_31Mac(SdKeyScp03_31, key_id=0x02, key_usage_qual=0x14):
pass
class SdKeyScp03_31Dek(SdKeyScp03_31, key_id=0x03, key_usage_qual=0x48):
pass
class SdKeyScp03_32(SdKey, kvn=0x32, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
class SdKeyScp03_32Enc(SdKeyScp03_32, key_id=0x01, key_usage_qual=0x18):
pass
class SdKeyScp03_32Mac(SdKeyScp03_32, key_id=0x02, key_usage_qual=0x14):
pass
class SdKeyScp03_32Dek(SdKeyScp03_32, key_id=0x03, key_usage_qual=0x48):
pass
def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
filtered = list(filter(lambda x: x.type == wanted_type, l))
assert len(filtered) == 1
return filtered[0]
def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
filtered = list(filter(lambda x: x.type == wanted_type, l))
return filtered[0]
class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
"""Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
keyReference = None
def validate(self):
if isinstance(self.input_value, int):
self.value = '%08d' % self.input_value
else:
self.value = self.input_value
# FIXME: valid length?
if not self.value.isdecimal():
raise ValueError('PUK must only contain decimal digits')
def apply(self, pes: ProfileElementSequence):
puk = ''.join(['%02x' % (ord(x)) for x in self.value])
padded_puk = rpad(puk, 16)
mf_pes = pes.pes_by_naa['mf'][0]
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
for pukCode in pukCodes.decoded['pukCodes']:
if pukCode['keyReference'] == self.keyReference:
pukCode['pukValue'] = h2b(padded_puk)
return
raise ValueError('cannot find pukCode')
class Puk1(Puk, keyReference=0x01):
pass
class Puk2(Puk, keyReference=0x81):
pass
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
"""Configurable PIN (Personal Identification Number). String of digits."""
keyReference = None
def validate(self):
if isinstance(self.input_value, int):
self.value = '%04d' % self.input_value
else:
self.value = self.input_value
if len(self.value) < 4 or len(self.value) > 8:
raise ValueError('PIN mus be 4..8 digits long')
if not self.value.isdecimal():
raise ValueError('PIN must only contain decimal digits')
def apply(self, pes: ProfileElementSequence):
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
padded_pin = rpad(pin, 16)
mf_pes = pes.pes_by_naa['mf'][0]
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
return
for pinCode in pinCodes.decoded['pinCodes'][1]:
if pinCode['keyReference'] == self.keyReference:
pinCode['pinValue'] = h2b(padded_pin)
return
raise ValueError('cannot find pinCode')
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
"""Configurable PIN (Personal Identification Number). String of digits."""
keyReference = None
def validate(self):
if isinstance(self.input_value, int):
self.value = '%04d' % self.input_value
else:
self.value = self.input_value
if len(self.value) < 4 or len(self.value) > 8:
raise ValueError('PIN mus be 4..8 digits long')
if not self.value.isdecimal():
raise ValueError('PIN must only contain decimal digits')
def _apply_one(self, pe: ProfileElement):
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
padded_pin = rpad(pin, 16)
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
return
for pinCode in pinCodes.decoded['pinCodes'][1]:
if pinCode['keyReference'] == self.keyReference:
pinCode['pinValue'] = h2b(padded_pin)
return
raise ValueError('cannot find pinCode')
def apply(self, pes: ProfileElementSequence):
for naa in pes.pes_by_naa:
if naa not in ['usim','isim','csim','telecom']:
continue
for instance in pes.pes_by_naa[naa]:
self._apply_one(instance)
class Pin1(Pin, keyReference=0x01):
pass
# PIN2 is special: telecom + usim + isim + csim
class Pin2(AppPin, keyReference=0x81):
pass
class Adm1(Pin, keyReference=0x0A):
pass
class Adm2(Pin, keyReference=0x0B):
pass
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
"""Configurable Algorithm parameter. bytes."""
key = None
def validate(self):
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
raise ValueError('Value must be of bytes-like type')
self.value = self.input_value
def apply(self, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('akaParameter'):
algoConfiguration = pe.decoded['algoConfiguration']
if algoConfiguration[0] != 'algoParameter':
continue
algoConfiguration[1][self.key] = self.value
class K(AlgoConfig, key='key'):
pass
class Opc(AlgoConfig, key='opc'):
pass
class AlgorithmID(AlgoConfig, key='algorithmID'):
def validate(self):
if self.input_value not in [1, 2, 3]:
raise ValueError('Invalid algorithmID %s' % (self.input_value))
self.value = self.input_value

View File

@@ -0,0 +1,675 @@
# Implementation of SimAlliance/TCA Interoperable Profile Template handling
#
# (C) 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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import *
from copy import deepcopy
import pySim.esim.saip.oid as OID
class FileTemplate:
"""Representation of a single file in a SimAlliance/TCA Profile Template."""
def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
pe_name:Optional[str] = None):
# initialize from arguments
self.fid = fid
self.name = name
if pe_name:
self.pe_name = pe_name
else:
self.pe_name = self.name.replace('.','-').replace('_','-').lower()
self.file_type = ftype
if ftype in ['LF', 'CY']:
self.nb_rec = nb_rec
self.rec_len = size
elif ftype in ['TR']:
self.file_size = size
self.arr = arr
self.sfi = sfi
self.default_val = default_val
self.content_rqd = content_rqd
self.params = params
self.ass_serv = ass_serv
self.high_update = high_update
# initialize empty
self.parent = None
self.children = []
def __str__(self) -> str:
return "FileTemplate(%s)" % (self.name)
def __repr__(self) -> str:
s_fid = "%04x" % self.fid if self.fid is not None else 'None'
s_arr = self.arr if self.arr is not None else 'None'
s_sfi = "%02x" % self.sfi if self.sfi is not None else 'None'
return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s)" % (self.name, self.pe_name, s_fid,
self.file_type, s_arr, s_sfi)
class ProfileTemplate:
"""Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
consists of a number of file definitions. We implement each profile template as a class derived from this
base class. Each such derived class is a singleton and has no instances."""
created_by_default: bool = False
oid: Optional[OID.eOID] = None
files: List[FileTemplate] = []
files_by_pename: dict[str,FileTemplate] = {}
def __init_subclass__(cls, **kwargs):
"""This classmethod is called automatically after executing the subclass body. We use it to
initialize the cls.files_by_pename from the cls.files"""
super().__init_subclass__(**kwargs)
for f in cls.files:
cls.files_by_pename[f.pe_name] = f
ProfileTemplateRegistry.add(cls)
class ProfileTemplateRegistry:
"""A registry of profile templates. Exists as a singleton class with no instances and only
classmethods."""
by_oid = {}
@classmethod
def add(cls, tpl: ProfileTemplate):
"""Add a ProfileTemplate to the registry. There can only be one Template per OID."""
oid_str = str(tpl.oid)
if oid_str in cls.by_oid:
raise ValueError("We already have a template for OID %s" % oid_str)
cls.by_oid[oid_str] = tpl
@classmethod
def get_by_oid(cls, oid: Union[List[int], str]) -> Optional[ProfileTemplate]:
"""Look-up the ProfileTemplate based on its OID. The OID can be given either in dotted-string format,
or as a list of integers."""
if not isinstance(oid, str):
oid = OID.OID.str_from_intlist(oid)
return cls.by_oid.get(oid, None)
# below are transcribed template definitions from "ANNEX A (Normative): File Structure Templates Definition"
# of "Profile interoperability specification V3.1 Final" (unless other version explicitly specified).
# Section 9.2
class FilesAtMF(ProfileTemplate):
created_by_default = True
oid = OID.MF
files = [
FileTemplate(0x3f00, 'MF', 'MF', None, None, 14, None, None, None, params=['pinStatusTemplateDO']),
FileTemplate(0x2f05, 'EF.PL', 'TR', None, 2, 1, 0x05, 'FF...FF', None),
FileTemplate(0x2f02, 'EF.ICCID', 'TR', None, 10, 11, None, None, True),
FileTemplate(0x2f00, 'EF.DIR', 'LF', None, None, 10, 0x1e, None, True, params=['nb_rec', 'size']),
FileTemplate(0x2f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, params=['nb_rec', 'size']),
FileTemplate(0x2f08, 'EF.UMPC', 'TR', None, 5, 10, 0x08, None, False),
]
# Section 9.3
class FilesCD(ProfileTemplate):
created_by_default = False
oid = OID.DF_CD
files = [
FileTemplate(0x7f11, 'DF.CD', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f01, 'EF.LAUNCHPAD', 'TR', None, None, 2, None, None, True, params=['size']),
]
for i in range(0x40, 0x7f):
files.append(FileTemplate(0x6f00+i, 'EF.ICON', 'TR', None, None, 2, None, None, True, params=['size']))
# Section 9.4: Do this separately, so we can use them also from 9.5.3
df_pb_files = [
FileTemplate(0x5f3a, 'DF.PHONEBOOK', 'DF', None, None, 14, None, None, True, ['pinStatusTemplateDO']),
FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size']),
]
for i in range(0x38, 0x40):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi']))
for i in range(0x40, 0x48):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
for i in range(0x48, 0x50):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
df_pb_files += [
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi']),
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
]
for i in range(0x50, 0x58):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x58, 0x60):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x60, 0x68):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi']))
for i in range(0x68, 0x70):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x70, 0x78):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi']))
for i in range(0x78, 0x80):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x80, 0x88):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x88, 0x90):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi']))
for i in range(0x90, 0x98):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi']))
for i in range(0x98, 0xa0):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
# Section 9.4 v2.3.1
class FilesTelecom(ProfileTemplate):
created_by_default = False
oid = OID.DF_TELECOM
files = [
FileTemplate(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size']),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
files += [deepcopy(x) for x in df_pb_files]
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
]
# Section 9.4
class FilesTelecomV2(ProfileTemplate):
created_by_default = False
oid = OID.DF_TELECOM_v2
files = [
FileTemplate(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size']),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
files += [deepcopy(x) for x in df_pb_files]
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119]),
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119]),
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 2
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 3
]
# Section 9.5.1 v2.3.1
class FilesUsimMandatory(ProfileTemplate):
created_by_default = True
oid = OID.ADF_USIM_by_default
files = [
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name = 'ef-keysPS'),
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 14, 2, 0x04, None, True),
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
]
# Section 9.5.1
class FilesUsimMandatoryV2(ProfileTemplate):
created_by_default = True
oid = OID.ADF_USIM_by_default_v2
files = [
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name='ef-keysPS'),
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 17, 2, 0x04, None, True),
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
]
# Section 9.5.2 v2.3.1
class FilesUsimOptional(ProfileTemplate):
created_by_default = False
oid = OID.ADF_USIM_not_by_default
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
]
# Section 9.5.2
class FilesUsimOptionalV2(ProfileTemplate):
created_by_default = False
oid = OID.ADF_USIM_not_by_default_v2
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
FileTemplate(0x6ff3, 'EF.EPDGID', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
FileTemplate(0x6ff4, 'EF.EPDGSELECTION','TR',None,None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
FileTemplate(0x6ff5, 'EF.EPDGIDEM', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
FileTemplate(0x6ff6, 'EF.EPDGIDEMSEL','TR',None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
FileTemplate(0x6ff7, 'EF.FromPreferred','TR',None, 1, 2, None, '00', False, ass_serv=[114]),
FileTemplate(0x6ff8, 'EF.IMSConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[115]),
FileTemplate(0x6ff9, 'EF.3GPPPSDataOff','TR',None, 4, 2, None, None, True, ass_serv=[117]),
FileTemplate(0x6ffa, 'EF.3GPPPSDOSLIST','LF',None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[118]),
FileTemplate(0x6ffc, 'EF.XCAPConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[120]),
FileTemplate(0x6ffd, 'EF.EARFCNLIST','TR', None, None, 10, None, None, True, ['size'], ass_serv=[121]),
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
]
# Section 9.5.3
class FilesUsimDfPhonebook(ProfileTemplate):
created_by_default = False
oid = OID.DF_PHONEBOOK_ADF_USIM
files = df_pb_files
# Section 9.5.4
class FilesUsimDfGsmAccess(ProfileTemplate):
created_by_default = False
oid = OID.DF_GSM_ACCESS_ADF_USIM
files = [
FileTemplate(0x5f3b, 'DF.GSM-ACCESS','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[27]),
FileTemplate(0x4f20, 'EF.Kc', 'TR', None, 9, 5, 0x01, 'FF...FF07', False, ass_serv=[27], high_update=True),
FileTemplate(0x4f52, 'EF.KcGPRS', 'TR', None, 9, 5, 0x02, 'FF...FF07', False, ass_serv=[27], high_update=True),
FileTemplate(0x4f63, 'EF.CPBCCH', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[39], high_update=True),
FileTemplate(0x4f64, 'EF.InvScan', 'TR', None, 1, 2, None, '00', False, ass_serv=[40]),
]
# Section 9.5.11 v2.3.1
class FilesUsimDf5GS(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
]
# Section 9.5.11.2
class FilesUsimDf5GSv2(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS_v2
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
]
# Section 9.5.11.3
class FilesUsimDf5GSv3(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS_v3
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 2, 62, 5, 0x03, 'FF...FF', False, ass_serv=[122,136], high_update=True),
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 2, 62, 5, 0x04, 'FF...FF', False, ass_serv=[122,136], high_update=True),
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130], pe_name='ef-supinai'),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
]
# Section 9.5.12
class FilesUsimDfSaip(ProfileTemplate):
created_by_default = False
oid = OID.DF_SAIP
files = [
FileTemplate(0x6fd0, 'DF.SAIP', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[(124, 125)], pe_name='df-df-saip'),
FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF..FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
]
# Section 9.6.1
class FilesIsimMandatory(ProfileTemplate):
created_by_default = True
oid = OID.ADF_ISIM_by_default
files = [
FileTemplate( None, 'ADF.ISIM', 'ADF', None, None, 14, None, None, False, ['aid','temporary_fid','pinStatusTemplateDO']),
FileTemplate(0x6f02, 'EF.IMPI', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x6f04, 'EF.IMPU', 'LF', 1, None, 2, 0x04, None, True, ['size']),
FileTemplate(0x6f03, 'EF.Domain', 'TR', None, None, 2, 0x05, None, True, ['size']),
FileTemplate(0x6f07, 'EF.IST', 'TR', None, 14, 2, 0x07, None, True),
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 3, 10, 0x03, '000000', False),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x06, None, True, ['nb_rec','size']),
]
# Section 9.6.2 v2.3.1
class FilesIsimOptional(ProfileTemplate):
created_by_default = False
oid = OID.ADF_ISIM_not_by_default
files = [
FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
]
# Section 9.6.2
class FilesIsimOptionalv2(ProfileTemplate):
created_by_default = False
oid = OID.ADF_ISIM_not_by_default_v2
files = [
FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
FileTemplate(0x6ff7, 'EF.FromPreferred','TR', None, 1, 2, None, '00', False, ass_serv=[17]),
FileTemplate(0x6ff8, 'EF.ImsConfigData','BT', None,None, 2, None, None, True, ['size'], ass_serv=[18]),
FileTemplate(0x6ffc, 'EF.XcapconfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[19]),
FileTemplate(0x6ffa, 'EF.WebRTCURI', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ass_serv=[20]),
FileTemplate(0x6ffa, 'EF.MudMidCfgData','BT',None, None, 2, None, None, True, ['size'], ass_serv=[21]),
]
# TODO: CSIM
# Section 9.8
class FilesEap(ProfileTemplate):
created_by_default = False
oid = OID.DF_EAP
files = [
FileTemplate( None, 'DF.EAP', 'DF', None, None, 14, None, None, False, ['fid','pinStatusTemplateDO'], ass_serv=[(124, 125)]),
FileTemplate(0x4f01, 'EF.EAPKEYS', 'TR', None, None, 2, None, None, True, ['size'], high_update=True),
FileTemplate(0x4f02, 'EF.EAPSTATUS', 'TR', None, 1, 2, None, '00', False, high_update=True),
FileTemplate(0x4f03, 'EF.PUId', 'TR', None, None, 2, None, None, True, ['size']),
FileTemplate(0x4f04, 'EF.Ps', 'TR', None, None, 5, None, 'FF..FF', False, ['size'], high_update=True),
FileTemplate(0x4f20, 'EF.CurID', 'TR', None, None, 5, None, 'FF..FF', False, ['size'], high_update=True),
FileTemplate(0x4f21, 'EF.RelID', 'TR', None, None, 5, None, None, True, ['size']),
FileTemplate(0x4f22, 'EF.Realm', 'TR', None, None, 5, None, None, True, ['size']),
]
# Section 9.9 Access Rules Definition
ARR_DEFINITION = {
1: ['8001019000', '800102A406830101950108', '800158A40683010A950108'],
2: ['800101A406830101950108', '80015AA40683010A950108'],
3: ['80015BA40683010A950108'],
4: ['8001019000', '80011A9700', '800140A40683010A950108'],
5: ['800103A406830101950108', '800158A40683010A950108'],
6: ['800111A406830101950108', '80014AA40683010A950108'],
7: ['800103A406830101950108', '800158A40683010A950108', '840132A406830101950108'],
8: ['800101A406830101950108', '800102A406830181950108', '800158A40683010A950108'],
9: ['8001019000', '80011AA406830101950108', '800140A40683010A950108'],
10: ['8001019000', '80015AA40683010A950108'],
11: ['8001019000', '800118A40683010A950108', '8001429700'],
12: ['800101A406830101950108', '80015A9700'],
13: ['800113A406830101950108', '800148A40683010A950108'],
14: ['80015EA40683010A950108'],
}

View File

@@ -0,0 +1,128 @@
# Implementation of SimAlliance/TCA Interoperable Profile handling
#
# (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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pySim.esim.saip import *
class ProfileError(Exception):
pass
class ProfileConstraintChecker:
def check(self, pes: ProfileElementSequence):
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(pes)
class CheckBasicStructure(ProfileConstraintChecker):
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
opt_pe = pes.get_pe_for_type(opt)
if opt_pe:
after_pe = pes.get_pe_for_type(after)
if not after_pe:
raise ProfileError('PE-%s without PE-%s' % (opt.upper(), after.upper()))
# FIXME: check order
def check_start_and_end(self, pes: ProfileElementSequence):
if pes.pe_list[0].type != 'header':
raise ProfileError('first element is not header')
if pes.pe_list[1].type != 'mf':
# strictly speaking: permitted, but we don't support MF via GenericFileManagement
raise ProfileError('second element is not mf')
if pes.pe_list[-1].type != 'end':
raise ProfileError('last element is not end')
def check_number_of_occurrence(self, pes: ProfileElementSequence):
# check for invalid number of occurrences
if len(pes.get_pes_for_type('header')) != 1:
raise ProfileError('multiple ProfileHeader')
if len(pes.get_pes_for_type('mf')) != 1:
# strictly speaking: 0 permitted, but we don't support MF via GenericFileManagement
raise ProfileError('multiple PE-MF')
for tn in ['end', 'cd', 'telecom',
'usim', 'isim', 'csim', 'opt-usim','opt-isim','opt-csim',
'df-saip', 'df-5gs']:
if len(pes.get_pes_for_type(tn)) > 1:
raise ProfileError('multiple PE-%s' % tn.upper())
def check_optional_ordering(self, pes: ProfileElementSequence):
# ordering and required depenencies
self._is_after_if_exists(pes,'opt-usim', 'usim')
self._is_after_if_exists(pes,'opt-isim', 'isim')
self._is_after_if_exists(pes,'gsm-access', 'usim')
self._is_after_if_exists(pes,'phonebook', 'usim')
self._is_after_if_exists(pes,'df-5gs', 'usim')
self._is_after_if_exists(pes,'df-saip', 'usim')
self._is_after_if_exists(pes,'opt-csim', 'csim')
def check_mandatory_services(self, pes: ProfileElementSequence):
"""Ensure that the PE for the mandatory services exist."""
m_svcs = pes.get_pe_for_type('header').decoded['eUICC-Mandatory-services']
if 'usim' in m_svcs and not pes.get_pe_for_type('usim'):
raise ProfileError('no PE-USIM for mandatory usim service')
if 'isim' in m_svcs and not pes.get_pe_for_type('isim'):
raise ProfileError('no PE-ISIM for mandatory isim service')
if 'csim' in m_svcs and not pes.get_pe_for_type('csim'):
raise ProfileError('no PE-ISIM for mandatory csim service')
if 'gba-usim' in m_svcs and not 'usim' in m_svcs:
raise ProfileError('gba-usim mandatory, but no usim')
if 'gba-isim' in m_svcs and not 'isim' in m_svcs:
raise ProfileError('gba-isim mandatory, but no isim')
if 'multiple-usim' in m_svcs and not 'usim' in m_svcs:
raise ProfileError('multiple-usim mandatory, but no usim')
if 'multiple-isim' in m_svcs and not 'isim' in m_svcs:
raise ProfileError('multiple-isim mandatory, but no isim')
if 'multiple-csim' in m_svcs and not 'csim' in m_svcs:
raise ProfileError('multiple-csim mandatory, but no csim')
if 'get-identity' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
raise ProfileError('get-identity mandatory, but no usim or isim')
if 'profile-a-x25519' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
raise ProfileError('profile-a-x25519 mandatory, but no usim or isim')
if 'profile-a-p256' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
FileChoiceList = List[Tuple]
class FileError(ProfileError):
pass
class FileConstraintChecker:
def check(self, l: FileChoiceList):
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(l)
class FileCheckBasicStructure(FileConstraintChecker):
def check_seqence(self, l: FileChoiceList):
by_type = {}
for k, v in l:
if k in by_type:
by_type[k].append(v)
else:
by_type[k] = [v]
if 'doNotCreate' in by_type:
if len(l) != 1:
raise FileError("doNotCreate must be the only element")
if 'fileDescriptor' in by_type:
if len(by_type['fileDescriptor']) != 1:
raise FileError("fileDescriptor must be the only element")
if l[0][0] != 'fileDescriptor':
raise FileError("fileDescriptor must be the first element")
def check_forbidden(self, l: FileChoiceList):
"""Perform checks for forbidden parameters as described in Section 8.3.3."""

210
pySim/esim/x509_cert.py Normal file
View File

@@ -0,0 +1,210 @@
# Implementation of X.509 certificate handling in GSMA eSIM
# as per SGP22 v3.0
#
# (C) 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 Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
from typing import Optional, List
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography import x509
from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from pySim.utils import b2h
def check_signed(signed: x509.Certificate, signer: x509.Certificate) -> bool:
"""Verify if 'signed' certificate was signed using 'signer'."""
# this code only works for ECDSA, but this is all we need for GSMA eSIM
pkey = signer.public_key()
# this 'signed.signature_algorithm_parameters' below requires cryptopgraphy 41.0.0 :(
pkey.verify(signed.signature, signed.tbs_certificate_bytes, signed.signature_algorithm_parameters)
def cert_get_subject_key_id(cert: x509.Certificate) -> bytes:
"""Obtain the subject key identifier of the given cert object (as raw bytes)."""
ski_ext = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
return ski_ext.key_identifier
def cert_get_auth_key_id(cert: x509.Certificate) -> bytes:
"""Obtain the authority key identifier of the given cert object (as raw bytes)."""
aki_ext = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
return aki_ext.key_identifier
def cert_policy_has_oid(cert: x509.Certificate, match_oid: x509.ObjectIdentifier) -> bool:
"""Determine if given certificate has a certificatePolicy extension of matching OID."""
for policy_ext in filter(lambda x: isinstance(x.value, x509.CertificatePolicies), cert.extensions):
if any(policy.policy_identifier == match_oid for policy in policy_ext.value._policies):
return True
return False
ID_RSP = "2.23.146.1"
ID_RSP_CERT_OBJECTS = '.'.join([ID_RSP, '2'])
ID_RSP_ROLE = '.'.join([ID_RSP_CERT_OBJECTS, '1'])
class oid:
id_rspRole_ci = x509.ObjectIdentifier(ID_RSP_ROLE + '.0')
id_rspRole_euicc_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.1')
id_rspRole_eum_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.2')
id_rspRole_dp_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.3')
id_rspRole_dp_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.4')
id_rspRole_dp_pb_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.5')
id_rspRole_ds_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.6')
id_rspRole_ds_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.7')
class VerifyError(Exception):
"""An error during certificate verification,"""
class CertificateSet:
"""A set of certificates consisting of a trusted [self-signed] CA root certificate,
and an optional number of intermediate certificates. Can be used to verify the certificate chain
of any given other certificate."""
def __init__(self, root_cert: x509.Certificate):
check_signed(root_cert, root_cert)
# TODO: check other mandatory attributes for CA Cert
if not cert_policy_has_oid(root_cert, oid.id_rspRole_ci):
raise ValueError("Given root certificate doesn't have rspRole_ci OID")
usage_ext = root_cert.extensions.get_extension_for_class(x509.KeyUsage).value
if not usage_ext.key_cert_sign:
raise ValueError('Given root certificate key usage does not permit signing of certificates')
if not usage_ext.crl_sign:
raise ValueError('Given root certificate key usage does not permit signing of CRLs')
self.root_cert = root_cert
self.intermediate_certs = {}
self.crl = None
def load_crl(self, urls: Optional[List[str]] = None):
if urls and isinstance(urls, str):
urls = [urls]
if not urls:
# generate list of CRL URLs from root CA certificate
crl_ext = self.root_cert.extensions.get_extension_for_class(x509.CRLDistributionPoints).value
name_list = [x.full_name for x in crl_ext]
merged_list = []
for n in name_list:
merged_list += n
uri_list = filter(lambda x: isinstance(x, x509.UniformResourceIdentifier), merged_list)
urls = [x.value for x in uri_list]
for url in urls:
try:
crl_bytes = requests.get(url, timeout=10)
except requests.exceptions.ConnectionError:
continue
crl = x509.load_der_x509_crl(crl_bytes)
if not crl.is_signature_valid(self.root_cert.public_key()):
raise ValueError('Given CRL has incorrect signature and cannot be trusted')
# FIXME: various other checks
self.crl = crl
# FIXME: should we support multiple CRLs? we only support a single CRL right now
return
# FIXME: report on success/failure
@property
def root_cert_id(self) -> bytes:
return cert_get_subject_key_id(self.root_cert)
def add_intermediate_cert(self, cert: x509.Certificate):
"""Add a potential intermediate certificate to the CertificateSet."""
# TODO: check mandatory attributes for intermediate cert
usage_ext = cert.extensions.get_extension_for_class(x509.KeyUsage).value
if not usage_ext.key_cert_sign:
raise ValueError('Given intermediate certificate key usage does not permit signing of certificates')
aki = cert_get_auth_key_id(cert)
ski = cert_get_subject_key_id(cert)
if aki == ski:
raise ValueError('Cannot add self-signed cert as intermediate cert')
self.intermediate_certs[ski] = cert
# TODO: we could test if this cert verifies against the root, and mark it as pre-verified
# so we don't need to verify again and again the chain of intermediate certificates
def verify_cert_crl(self, cert: x509.Certificate):
if not self.crl:
# we cannot check if there's no CRL
return
if self.crl.get_revoked_certificate_by_serial_number(cert.serial_nr):
raise VerifyError('Certificate is present in CRL, verification failed')
def verify_cert_chain(self, cert: x509.Certificate, max_depth: int = 100):
"""Verify if a given certificate's signature chain can be traced back to the root CA of this
CertificateSet."""
depth = 1
c = cert
while True:
aki = cert_get_auth_key_id(c)
if aki == self.root_cert_id:
# last step:
check_signed(c, self.root_cert)
return
parent_cert = self.intermediate_certs.get(aki, None)
if not aki:
raise VerifyError('Could not find intermediate certificate for AuthKeyId %s' % b2h(aki))
check_signed(c, parent_cert)
# if we reach here, we passed (no exception raised)
c = parent_cert
depth += 1
if depth > max_depth:
raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
def ecdsa_dss_to_tr03111(sig: bytes) -> bytes:
"""convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes."""
r, s = decode_dss_signature(sig)
return r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
class CertAndPrivkey:
"""A pair of certificate and private key, as used for ECDSA signing."""
def __init__(self, required_policy_oid: Optional[x509.ObjectIdentifier] = None,
cert: Optional[x509.Certificate] = None, priv_key = None):
self.required_policy_oid = required_policy_oid
self.cert = cert
self.priv_key = priv_key
def cert_from_der_file(self, path: str):
with open(path, 'rb') as f:
cert = x509.load_der_x509_certificate(f.read())
if self.required_policy_oid:
# verify it is the right type of certificate (id-rspRole-dp-auth, id-rspRole-dp-auth-v2, etc.)
assert cert_policy_has_oid(cert, self.required_policy_oid)
self.cert = cert
def privkey_from_pem_file(self, path: str, password: Optional[str] = None):
with open(path, 'rb') as f:
self.priv_key = load_pem_private_key(f.read(), password)
def ecdsa_sign(self, plaintext: bytes) -> bytes:
"""Sign some input-data using an ECDSA signature compliant with SGP.22,
which internally refers to Global Platform 2.2 Annex E, which in turn points
to BSI TS-03111 which states "concatengated raw R + S values". """
sig = self.priv_key.sign(plaintext, ec.ECDSA(hashes.SHA256()))
# convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes
return ecdsa_dss_to_tr03111(sig)
def get_authority_key_identifier(self) -> x509.AuthorityKeyIdentifier:
"""Return the AuthorityKeyIdentifier X.509 extension of the certificate."""
return list(filter(lambda x: isinstance(x.value, x509.AuthorityKeyIdentifier), self.cert.extensions))[0].value
def get_subject_alt_name(self) -> x509.SubjectAlternativeName:
"""Return the SubjectAlternativeName X.509 extension of the certificate."""
return list(filter(lambda x: isinstance(x.value, x509.SubjectAlternativeName), self.cert.extensions))[0].value
def get_cert_as_der(self) -> bytes:
"""Return certificate encoded as DER."""
return self.cert.public_bytes(Encoding.DER)
def get_curve(self) -> ec.EllipticCurve:
return self.cert.public_key().public_numbers().curve

528
pySim/euicc.py Normal file
View File

@@ -0,0 +1,528 @@
# -*- coding: utf-8 -*-
"""
Various definitions related to GSMA eSIM / eUICC
Related Specs: GSMA SGP.22, GSMA SGP.02, etc.
"""
# Copyright (C) 2023 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 argparse
from construct import Array, Struct, FlagsEnum, GreedyRange
from cmd2 import cmd2, CommandSet, with_default_category
from pySim.tlv import *
from pySim.construct import *
from pySim.commands import SimCardCommands
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
import pySim.global_platform
def compute_eid_checksum(eid) -> str:
"""Compute and add/replace check digits of an EID value according to GSMA SGP.29 Section 10."""
if isinstance(eid, str):
if len(eid) == 30:
# first pad by 2 digits
eid += "00"
elif len(eid) == 32:
# zero the last two digits
eid = eid[:-2] + "00"
else:
raise ValueError("and EID must be 30 or 32 digits")
eid_int = int(eid)
elif isinstance(eid, int):
eid_int = eid
if eid_int % 100:
# zero the last two digits
eid_int -= eid_int % 100
# Using the resulting 32 digits as a decimal integer, compute the remainder of that number on division by
# 97, Subtract the remainder from 98, and use the decimal result for the two check digits, if the result
# is one digit long, its value SHALL be prefixed by one digit of 0.
csum = 98 - (eid_int % 97)
eid_int += csum
return str(eid_int)
def verify_eid_checksum(eid) -> bool:
"""Verify the check digits of an EID value according to GSMA SGP.29 Section 10."""
# Using the 32 digits as a decimal integer, compute the remainder of that number on division by 97. If the
# remainder of the division is 1, the verification is successful; otherwise the EID is invalid.
return int(eid) % 97 == 1
class VersionAdapter(Adapter):
"""convert an EUICC Version (3-int array) to a textual representation."""
def _decode(self, obj, context, path):
return "%u.%u.%u" % (obj[0], obj[1], obj[2])
def _encode(self, obj, context, path):
return [int(x) for x in obj.split('.')]
VersionType = VersionAdapter(Array(3, Int8ub))
# Application Identifiers as defined in GSMA SGP.02 Annex H
AID_ISD_R = "A0000005591010FFFFFFFF8900000100"
AID_ECASD = "A0000005591010FFFFFFFF8900000200"
AID_ISD_P_FILE = "A0000005591010FFFFFFFF8900000D00"
AID_ISD_P_MODULE = "A0000005591010FFFFFFFF8900000E00"
class SupportedVersionNumber(BER_TLV_IE, tag=0x82):
_construct = GreedyBytes
class IsdrProprietaryApplicationTemplate(BER_TLV_IE, tag=0xe0, nested=[SupportedVersionNumber]):
# FIXME: lpaeSupport - what kind of tag would it have?
pass
# GlobalPlatform 2.1.1 Section 9.9.3.1 from pySim/global_platform.py extended with E0
class FciTemplate(BER_TLV_IE, tag=0x6f, nested=pySim.global_platform.FciTemplateNestedList +
[IsdrProprietaryApplicationTemplate]):
pass
# SGP.22 Section 5.7.3: GetEuiccConfiguredAddresses
class DefaultDpAddress(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
class RootDsAddress(BER_TLV_IE, tag=0x81):
_construct = Utf8Adapter(GreedyBytes)
class EuiccConfiguredAddresses(BER_TLV_IE, tag=0xbf3c, nested=[DefaultDpAddress, RootDsAddress]):
pass
# SGP.22 Section 5.7.4: SetDefaultDpAddress
class SetDefaultDpAddrRes(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, undefinedError=127)
class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetDefaultDpAddrRes]):
pass
# SGP.22 Section 5.7.7: GetEUICCChallenge
class EuiccChallenge(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Bytes(16))
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
pass
# SGP.22 Section 5.7.8: GetEUICCInfo
class SVN(BER_TLV_IE, tag=0x82):
_construct = VersionType
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
_construct = HexAdapter(GreedyBytes)
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
pass
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
pass
class EuiccInfo1(BER_TLV_IE, tag=0xbf20, nested=[SVN, EuiccCiPkiListForVerification, EuiccCiPkiListForSigning]):
pass
class ProfileVersion(BER_TLV_IE, tag=0x81):
_construct = VersionType
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
_construct = VersionType
class ExtCardResource(BER_TLV_IE, tag=0x84):
_construct = HexAdapter(GreedyBytes)
class UiccCapability(BER_TLV_IE, tag=0x85):
_construct = HexAdapter(GreedyBytes) # FIXME
class TS102241Version(BER_TLV_IE, tag=0x86):
_construct = VersionType
class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
_construct = VersionType
class RspCapability(BER_TLV_IE, tag=0x88):
_construct = HexAdapter(GreedyBytes) # FIXME
class EuiccCategory(BER_TLV_IE, tag=0x8b):
_construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
class PpVersion(BER_TLV_IE, tag=0x04):
_construct = VersionType
class SsAcreditationNumber(BER_TLV_IE, tag=0x0c):
_construct = Utf8Adapter(GreedyBytes)
class IpaMode(BER_TLV_IE, tag=0x90): # see SGP.32 v1.0
_construct = Enum(Int8ub, ipad=0, ipea=1)
class IotVersion(BER_TLV_IE, tag=0x80): # see SGP.32 v1.0
_construct = VersionType
class IotVersionSeq(BER_TLV_IE, tag=0xa0, nested=[IotVersion]): # see SGP.32 v1.0
pass
class IotSpecificInfo(BER_TLV_IE, tag=0x94, nested=[IotVersionSeq]): # see SGP.32 v1.0
pass
class EuiccInfo2(BER_TLV_IE, tag=0xbf22, nested=[ProfileVersion, SVN, EuiccFirmwareVer, ExtCardResource,
UiccCapability, TS102241Version, GlobalPlatformVersion,
RspCapability, EuiccCiPkiListForVerification,
EuiccCiPkiListForSigning, EuiccCategory, PpVersion,
SsAcreditationNumber, IpaMode, IotSpecificInfo]):
pass
# SGP.22 Section 5.7.9: ListNotification
class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
# we have to ignore the first byte which tells us how many padding bits are used in the last octet
_construct = Struct(Byte, "pmo"/FlagsEnum(Byte, install=0x80, enable=0x40, disable=0x20, delete=0x10))
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
pass
class SeqNumber(BER_TLV_IE, tag=0x80):
_construct = GreedyInteger()
class NotificationAddress(BER_TLV_IE, tag=0x0c):
_construct = Utf8Adapter(GreedyBytes)
class Iccid(BER_TLV_IE, tag=0x5a):
_construct = BcdAdapter(GreedyBytes)
class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
NotificationAddress, Iccid]):
pass
class NotificationMetadataList(BER_TLV_IE, tag=0xa0, nested=[NotificationMetadata]):
pass
class ListNotificationsResultError(BER_TLV_IE, tag=0x81):
_construct = Enum(Int8ub, undefinedError=127)
class ListNotificationResp(BER_TLV_IE, tag=0xbf28, nested=[NotificationMetadataList,
ListNotificationsResultError]):
pass
# SGP.22 Section 5.7.11: RemoveNotificationFromList
class DeleteNotificationStatus(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, nothingToDelete=1, undefinedError=127)
class NotificationSentReq(BER_TLV_IE, tag=0xbf30, nested=[SeqNumber]):
pass
class NotificationSentResp(BER_TLV_IE, tag=0xbf30, nested=[DeleteNotificationStatus]):
pass
# SGP.22 Section 5.7.12: LoadCRL: FIXME
class LoadCRL(BER_TLV_IE, tag=0xbf35, nested=[]): # FIXME
pass
# SGP.22 Section 5.7.15: GetProfilesInfo
class TagList(BER_TLV_IE, tag=0x5c):
_construct = GreedyRange(Int8ub) # FIXME: tags could be multi-byte
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
pass
class IsdpAid(BER_TLV_IE, tag=0x4f):
_construct = HexAdapter(GreedyBytes)
class ProfileState(BER_TLV_IE, tag=0x9f70):
_construct = Enum(Int8ub, disabled=0, enabled=1)
class ProfileNickname(BER_TLV_IE, tag=0x90):
_construct = Utf8Adapter(GreedyBytes)
class ServiceProviderName(BER_TLV_IE, tag=0x91):
_construct = Utf8Adapter(GreedyBytes)
class ProfileName(BER_TLV_IE, tag=0x92):
_construct = Utf8Adapter(GreedyBytes)
class IconType(BER_TLV_IE, tag=0x93):
_construct = Enum(Int8ub, jpg=0, png=1)
class Icon(BER_TLV_IE, tag=0x94):
_construct = GreedyBytes
class ProfileClass(BER_TLV_IE, tag=0x95):
_construct = Enum(Int8ub, test=0, provisioning=1, operational=2)
class ProfileInfo(BER_TLV_IE, tag=0xe3, nested=[Iccid, IsdpAid, ProfileState, ProfileNickname,
ServiceProviderName, ProfileName, IconType, Icon,
ProfileClass]): # FIXME: more IEs
pass
class ProfileInfoSeq(BER_TLV_IE, tag=0xa0, nested=[ProfileInfo]):
pass
class ProfileInfoListError(BER_TLV_IE, tag=0x81):
_construct = Enum(Int8ub, incorrectInputValues=1, undefinedError=2)
class ProfileInfoListResp(BER_TLV_IE, tag=0xbf2d, nested=[ProfileInfoSeq, ProfileInfoListError]):
pass
# SGP.22 Section 5.7.16:: EnableProfile
class RefreshFlag(BER_TLV_IE, tag=0x81): # FIXME
_construct = Int8ub # FIXME
class EnableResult(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, iccidOrAidNotFound=1, profileNotInDisabledState=2,
disallowedByPolicy=3, wrongProfileReenabling=4, catBusy=5, undefinedError=127)
class ProfileIdentifier(BER_TLV_IE, tag=0xa0, nested=[IsdpAid, Iccid]):
pass
class EnableProfileReq(BER_TLV_IE, tag=0xbf31, nested=[ProfileIdentifier, RefreshFlag]):
pass
class EnableProfileResp(BER_TLV_IE, tag=0xbf31, nested=[EnableResult]):
pass
# SGP.22 Section 5.7.17 DisableProfile
class DisableResult(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, iccidOrAidNotFound=1, profileNotInEnabledState=2,
disallowedByPolicy=3, catBusy=5, undefinedError=127)
class DisableProfileReq(BER_TLV_IE, tag=0xbf32, nested=[ProfileIdentifier, RefreshFlag]):
pass
class DisableProfileResp(BER_TLV_IE, tag=0xbf32, nested=[DisableResult]):
pass
# SGP.22 Section 5.7.18: DeleteProfile
class DeleteResult(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, iccidOrAidNotFound=1, profileNotInDisabledState=2,
disallowedByPolicy=3, undefinedError=127)
class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
pass
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
pass
# SGP.22 Section 5.7.20 GetEID
class EidValue(BER_TLV_IE, tag=0x5a):
_construct = HexAdapter(GreedyBytes)
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
pass
# SGP.22 Section 5.7.21: ES10c SetNickname
class SnrProfileNickname(BER_TLV_IE, tag=0x8f):
_construct = Utf8Adapter(GreedyBytes)
class SetNicknameReq(BER_TLV_IE, tag=0xbf29, nested=[Iccid, SnrProfileNickname]):
pass
class SetNicknameResult(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, iccidNotFound=1, undefinedError=127)
class SetNicknameResp(BER_TLV_IE, tag=0xbf29, nested=[SetNicknameResult]):
pass
# SGP.32 Section 5.9.10: ES10b: GetCerts
class GetCertsReq(BER_TLV_IE, tag=0xbf56):
pass
class EumCertificate(BER_TLV_IE, tag=0xa5):
_construct = GreedyBytes
class EuiccCertificate(BER_TLV_IE, tag=0xa6):
_construct = GreedyBytes
class GetCertsError(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
pass
# SGP.32 Section 5.9.18: ES10b: GetEimConfigurationData
class EimId(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
class EimFqdn(BER_TLV_IE, tag=0x81):
_construct = Utf8Adapter(GreedyBytes)
class EimIdType(BER_TLV_IE, tag=0x82):
_construct = Enum(Int8ub, eimIdTypeOid=1, eimIdTypeFqdn=2, eimIdTypeProprietary=3)
class CounterValue(BER_TLV_IE, tag=0x83):
_construct = GreedyInteger
class AssociationToken(BER_TLV_IE, tag=0x84):
_construct = GreedyInteger
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
eimProprietary=4)
# FIXME: eimPublicKeyData, trustedPublicKeyDataTls, euiccCiPKId
class EimConfigurationData(BER_TLV_IE, tag=0x80, nested=[EimId, EimFqdn, EimIdType, CounterValue,
AssociationToken, EimSupportedProtocol]):
pass
class EimConfigurationDataSeq(BER_TLV_IE, tag=0xa0, nested=[EimConfigurationData]):
pass
class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
pass
class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
def __init__(self):
super().__init__(name='ADF.ISD-R', aid=AID_ISD_R,
desc='ISD-R (Issuer Security Domain Root) Application')
self.adf.decode_select_response = self.decode_select_response
self.adf.shell_commands += [self.AddlShellCommands()]
@staticmethod
def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
Only single-block store supported for now."""
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
return scc.send_apdu_checksw(capdu, exp_sw)
@staticmethod
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw: SwMatchstr = '9000'):
"""Transceive STORE DATA APDU with the card, transparently encoding the command data from TLV
and decoding the response data tlv."""
if cmd_do:
cmd_do_enc = cmd_do.to_tlv()
cmd_do_len = len(cmd_do_enc)
if cmd_do_len > 255:
return ValueError('DO > 255 bytes not supported yet')
else:
cmd_do_enc = b''
(data, _sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc), exp_sw=exp_sw)
if data:
if resp_cls:
resp_do = resp_cls()
resp_do.from_tlv(h2b(data))
return resp_do
else:
return data
else:
return None
def decode_select_response(self, data_hex: Hexstr) -> object:
t = FciTemplate()
t.from_tlv(h2b(data_hex))
d = t.to_dict()
return flatten_dict_lists(d['fci_template'])
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
es10x_store_data_parser = argparse.ArgumentParser()
es10x_store_data_parser.add_argument('TX_DO', help='Hexstring of encoded to-be-transmitted DO')
@cmd2.with_argparser(es10x_store_data_parser)
def do_es10x_store_data(self, opts):
"""Perform a raw STORE DATA command as defined for the ES10x eUICC interface."""
(_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
def do_get_euicc_configured_addresses(self, _opts):
"""Perform an ES10a GetEuiccConfiguredAddresses function."""
eca = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
d = eca.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['euicc_configured_addresses']))
set_def_dp_addr_parser = argparse.ArgumentParser()
set_def_dp_addr_parser.add_argument('DP_ADDRESS', help='Default SM-DP+ address as UTF-8 string')
@cmd2.with_argparser(set_def_dp_addr_parser)
def do_set_default_dp_address(self, opts):
"""Perform an ES10a SetDefaultDpAddress function."""
sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
sdda = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
d = sdda.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['set_default_dp_address']))
def do_get_euicc_challenge(self, _opts):
"""Perform an ES10b GetEUICCChallenge function."""
gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEuiccChallenge(), GetEuiccChallenge)
d = gec.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_challenge']))
def do_get_euicc_info1(self, _opts):
"""Perform an ES10b GetEUICCInfo (1) function."""
ei1 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
d = ei1.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['euicc_info1']))
def do_get_euicc_info2(self, _opts):
"""Perform an ES10b GetEUICCInfo (2) function."""
ei2 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
d = ei2.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['euicc_info2']))
def do_list_notification(self, _opts):
"""Perform an ES10b ListNotification function."""
ln = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
d = ln.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['list_notification_resp']))
rem_notif_parser = argparse.ArgumentParser()
rem_notif_parser.add_argument('SEQ_NR', type=int, help='Sequence Number of the to-be-removed notification')
@cmd2.with_argparser(rem_notif_parser)
def do_remove_notification_from_list(self, opts):
"""Perform an ES10b RemoveNotificationFromList function."""
rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
rn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
d = rn.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp']))
def do_get_profiles_info(self, _opts):
"""Perform an ES10c GetProfilesInfo function."""
pi = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
d = pi.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['profile_info_list_resp']))
en_prof_parser = argparse.ArgumentParser()
en_prof_grp = en_prof_parser.add_mutually_exclusive_group()
en_prof_grp.add_argument('--isdp-aid', help='Profile identified by its ISD-P AID')
en_prof_grp.add_argument('--iccid', help='Profile identified by its ICCID')
en_prof_parser.add_argument('--refresh-required', action='store_true', help='whether a REFRESH is required')
@cmd2.with_argparser(en_prof_parser)
def do_enable_profile(self, opts):
"""Perform an ES10c EnableProfile function."""
if opts.isdp_aid:
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
if opts.iccid:
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
ep_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
ep = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
d = ep.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['enable_profile_resp']))
dis_prof_parser = argparse.ArgumentParser()
dis_prof_grp = dis_prof_parser.add_mutually_exclusive_group()
dis_prof_grp.add_argument('--isdp-aid', help='Profile identified by its ISD-P AID')
dis_prof_grp.add_argument('--iccid', help='Profile identified by its ICCID')
dis_prof_parser.add_argument('--refresh-required', action='store_true', help='whether a REFRESH is required')
@cmd2.with_argparser(dis_prof_parser)
def do_disable_profile(self, opts):
"""Perform an ES10c DisableProfile function."""
if opts.isdp_aid:
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
if opts.iccid:
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
dp_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
d = dp.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['disable_profile_resp']))
del_prof_parser = argparse.ArgumentParser()
del_prof_grp = del_prof_parser.add_mutually_exclusive_group()
del_prof_grp.add_argument('--isdp-aid', help='Profile identified by its ISD-P AID')
del_prof_grp.add_argument('--iccid', help='Profile identified by its ICCID')
@cmd2.with_argparser(del_prof_parser)
def do_delete_profile(self, opts):
"""Perform an ES10c DeleteProfile function."""
if opts.isdp_aid:
p_id = IsdpAid(decoded=opts.isdp_aid)
if opts.iccid:
p_id = Iccid(decoded=opts.iccid)
dp_cmd_contents = [p_id]
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
d = dp.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['delete_profile_resp']))
def do_get_eid(self, _opts):
"""Perform an ES10c GetEID function."""
(_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
ged = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
d = ged.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_data']))
set_nickname_parser = argparse.ArgumentParser()
set_nickname_parser.add_argument('ICCID', help='ICCID of the profile whose nickname to set')
set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
@cmd2.with_argparser(set_nickname_parser)
def do_set_nickname(self, opts):
"""Perform an ES10c SetNickname function."""
nickname = opts.profile_nickname or ''
sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
sn_cmd = SetNicknameReq(children=sn_cmd_contents)
sn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
d = sn.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['set_nickname_resp']))
def do_get_certs(self, _opts):
"""Perform an ES10c GetCerts() function on an IoT eUICC."""
gc = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
d = gc.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['get_certficiates_resp']))
def do_get_eim_configuration_data(self, _opts):
"""Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
GetEimConfigurationData)
d = gec.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['get_eim_configuration_data']))
class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
def decode_select_response(self, data_hex: Hexstr) -> object:
t = FciTemplate()
t.from_tlv(h2b(data_hex))
d = t.to_dict()
return flatten_dict_lists(d['fci_template'])
def __init__(self):
super().__init__(name='ADF.ECASD', aid=AID_ECASD,
desc='ECASD (eUICC Controlling Authority Security Domain) Application')
self.adf.decode_select_response = self.decode_select_response
self.adf.shell_commands += [self.AddlShellCommands()]
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
pass

View File

@@ -24,17 +24,14 @@
class NoCardError(Exception):
"""No card was found in the reader."""
pass
class ProtocolError(Exception):
"""Some kind of protocol level error interfacing with the card."""
pass
class ReaderError(Exception):
"""Some kind of general error with the card reader."""
pass
class SwMatchError(Exception):

File diff suppressed because it is too large Load Diff

View File

@@ -1,256 +0,0 @@
# coding=utf-8
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
(C) 2022 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/>.
"""
from typing import Optional, List, Dict, Tuple
from construct import Optional as COptional
from construct import *
from bidict import bidict
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from pySim.profile import CardProfile
sw_table = {
'Warnings': {
'6200': 'Logical Channel already closed',
'6283': 'Card Life Cycle State is CARD_LOCKED',
'6310': 'More data available',
},
'Execution errors': {
'6400': 'No specific diagnosis',
'6581': 'Memory failure',
},
'Checking errors': {
'6700': 'Wrong length in Lc',
},
'Functions in CLA not supported': {
'6881': 'Logical channel not supported or active',
'6882': 'Secure messaging not supported',
},
'Command not allowed': {
'6982': 'Security Status not satisfied',
'6985': 'Conditions of use not satisfied',
},
'Wrong parameters': {
'6a80': 'Incorrect values in command data',
'6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
'6a82': 'Application not found',
'6a84': 'Not enough memory space',
'6a86': 'Incorrect P1 P2',
'6a88': 'Referenced data not found',
},
'GlobalPlatform': {
'6d00': 'Invalid instruction',
'6e00': 'Invalid class',
},
'Application errors': {
'9484': 'Algorithm not supported',
'9485': 'Invalid key check value',
},
}
# GlobalPlatform 2.1.1 Section 9.1.6
KeyType = Enum(Byte, des=0x80,
rsa_public_exponent_e_cleartex=0xA0,
rsa_modulus_n_cleartext=0xA1,
rsa_modulus_n=0xA2,
rsa_private_exponent_d=0xA3,
rsa_chines_remainder_p=0xA4,
rsa_chines_remainder_q=0xA5,
rsa_chines_remainder_pq=0xA6,
rsa_chines_remainder_dpi=0xA7,
rsa_chines_remainder_dqi=0xA8,
not_available=0xff)
# GlobalPlatform 2.1.1 Section 9.3.3.1
# example:
# e0 48
# c0 04 01708010
# c0 04 02708010
# c0 04 03708010
# c0 04 01018010
# c0 04 02018010
# c0 04 03018010
# c0 04 01028010
# c0 04 02028010
# c0 04 03028010
# c0 04 01038010
# c0 04 02038010
# c0 04 03038010
class KeyInformationData(BER_TLV_IE, tag=0xc0):
_construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
'key_types'/GreedyRange(KeyType))
class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
pass
# card data sample, returned in response to GET DATA (80ca006600):
# 66 31
# 73 2f
# 06 07
# 2a864886fc6b01
# 60 0c
# 06 0a
# 2a864886fc6b02020101
# 63 09
# 06 07
# 2a864886fc6b03
# 64 0b
# 06 09
# 2a864886fc6b040215
# GlobalPlatform 2.1.1 Table F-1
class ObjectIdentifier(BER_TLV_IE, tag=0x06):
_construct = GreedyBytes
class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
pass
class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
pass
class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
pass
class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
_construct = GreedyBytes
class CardChipDetails(BER_TLV_IE, tag=0x66):
_construct = GreedyBytes
class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
CardManagementTypeAndVersion,
CardIdentificationScheme,
SecureChannelProtocolOfISD,
CardConfigurationDetails,
CardChipDetails]):
pass
class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
pass
# GlobalPlatform 2.1.1 Table F-2
class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
pass
class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
CardIdentificationScheme,
SecureChannelProtocolOfSelectedSD,
CardConfigurationDetails,
CardChipDetails]):
pass
# GlobalPlatform 2.1.1 Section 9.1.1
IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
card_locked = 0x7f, terminated=0xff)
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ApplicationID(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
_construct = GreedyInteger()
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
ApplicationProductionLifeCycleData,
MaximumLengthOfDataFieldInCommandMessage]):
pass
# GlobalPlatform 2.1.1 Section 9.9.3.1
class FciTemplate(BER_TLV_IE, tag=0x6f, nested=[ApplicationID, SecurityDomainManagementData,
ApplicationProductionLifeCycleData,
MaximumLengthOfDataFieldInCommandMessage,
ProprietaryData]):
pass
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
_construct = BcdAdapter(GreedyBytes)
class CardImageNumber(BER_TLV_IE, tag=0x45):
_construct = BcdAdapter(GreedyBytes)
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
_construct = GreedyInteger()
class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
_construct = GreedyInteger()
# Collection of all the data objects we can get from GET DATA
class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
CardImageNumber,
CardData,
KeyInformation,
SequenceCounterOfDefaultKvn,
ConfirmationCounter]):
pass
def decode_select_response(resp_hex: str) -> object:
t = FciTemplate()
t.from_tlv(h2b(resp_hex))
d = t.to_dict()
return flatten_dict_lists(d['fci_template'])
# Application Dedicated File of a Security Domain
class ADF_SD(CardADF):
def __init__(self, aid: str, name: str, desc: str):
super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
self.shell_commands += [self.AddlShellCommands()]
@staticmethod
def decode_select_response(res_hex: str) -> object:
return decode_select_response(res_hex)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
def __init__(self):
super().__init__()
def do_get_data(self, opts):
tlv_cls_name = opts.arg_list[0]
tlv_cls = DataCollection().members_by_name[tlv_cls_name]
(data, sw) = self._cmd.card._scc.get_data(cla=0x80, tag=tlv_cls.tag)
ie = tlv_cls()
ie.from_tlv(h2b(data))
self._cmd.poutput_json(ie.to_dict())
def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
#data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
data_dict = {str(x.__name__): x for x in DataCollection.possible_nested}
index_dict = {1: data_dict}
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
# Card Application of a Security Domain
class CardApplicationSD(CardApplication):
def __init__(self, aid: str, name: str, desc: str):
super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
# Card Application of Issuer Security Domain
class CardApplicationISD(CardApplicationSD):
# FIXME: ISD AID is not static, but could be different. One can select the empty
# application using '00a4040000' and then parse the response FCI to get the ISD AID
def __init__(self, aid='a000000003000000'):
super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
#class CardProfileGlobalPlatform(CardProfile):
# ORDER = 23
#
# def __init__(self, name='GlobalPlatform'):
# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)

View File

@@ -0,0 +1,926 @@
# coding=utf-8
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
(C) 2022-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 io
from copy import deepcopy
from typing import Optional, List, Dict, Tuple
from construct import Optional as COptional
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
from Cryptodome.Random import get_random_bytes
from Cryptodome.Cipher import DES, DES3, AES
from pySim.global_platform.scp import SCP02, SCP03
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from pySim.profile import CardProfile
sw_table = {
'Warnings': {
'6200': 'Logical Channel already closed',
'6283': 'Card Life Cycle State is CARD_LOCKED',
'6310': 'More data available',
},
'Execution errors': {
'6400': 'No specific diagnosis',
'6581': 'Memory failure',
},
'Checking errors': {
'6700': 'Wrong length in Lc',
},
'Functions in CLA not supported': {
'6881': 'Logical channel not supported or active',
'6882': 'Secure messaging not supported',
},
'Command not allowed': {
'6982': 'Security Status not satisfied',
'6985': 'Conditions of use not satisfied',
},
'Wrong parameters': {
'6a80': 'Incorrect values in command data',
'6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
'6a82': 'Application not found',
'6a84': 'Not enough memory space',
'6a86': 'Incorrect P1 P2',
'6a88': 'Referenced data not found',
},
'GlobalPlatform': {
'6d00': 'Invalid instruction',
'6e00': 'Invalid class',
},
'Application errors': {
'9484': 'Algorithm not supported',
'9485': 'Invalid key check value',
},
}
# GlobalPlatform 2.1.1 Section 9.1.6
KeyType = Enum(Byte, des=0x80,
tls_psk=0x85, # v2.3.1 Section 11.1.8
aes=0x88, # v2.3.1 Section 11.1.8
hmac_sha1=0x90, # v2.3.1 Section 11.1.8
hmac_sha1_160=0x91, # v2.3.1 Section 11.1.8
rsa_public_exponent_e_cleartex=0xA0,
rsa_modulus_n_cleartext=0xA1,
rsa_modulus_n=0xA2,
rsa_private_exponent_d=0xA3,
rsa_chines_remainder_p=0xA4,
rsa_chines_remainder_q=0xA5,
rsa_chines_remainder_pq=0xA6,
rsa_chines_remainder_dpi=0xA7,
rsa_chines_remainder_dqi=0xA8,
ecc_public_key=0xB0, # v2.3.1 Section 11.1.8
ecc_private_key=0xB1, # v2.3.1 Section 11.1.8
ecc_field_parameter_p=0xB2, # v2.3.1 Section 11.1.8
ecc_field_parameter_a=0xB3, # v2.3.1 Section 11.1.8
ecc_field_parameter_b=0xB4, # v2.3.1 Section 11.1.8
ecc_field_parameter_g=0xB5, # v2.3.1 Section 11.1.8
ecc_field_parameter_n=0xB6, # v2.3.1 Section 11.1.8
ecc_field_parameter_k=0xB7, # v2.3.1 Section 11.1.8
ecc_key_parameters_reference=0xF0, # v2.3.1 Section 11.1.8
not_available=0xff)
# GlobalPlatform 2.3 Section 11.10.2.1 Table 11-86
SetStatusScope = Enum(Byte, isd=0x80, app_or_ssd=0x40, isd_and_assoc_apps=0xc0)
# GlobalPlatform 2.3 section 11.1.1
CLifeCycleState = Enum(Byte, loaded=0x01, installed=0x03, selectable=0x07, personalized=0x0f, locked=0x83)
# GlobalPlatform 2.1.1 Section 9.3.3.1
class KeyInformationData(BER_TLV_IE, tag=0xc0):
_test_de_encode = [
( 'c00401708010', {"key_identifier": 1, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402708010', {"key_identifier": 2, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00403708010', {"key_identifier": 3, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00401018010', {"key_identifier": 1, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402018010', {"key_identifier": 2, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00403018010', {"key_identifier": 3, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00401028010', {"key_identifier": 1, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402028010', {"key_identifier": 2, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00403038010', {"key_identifier": 3, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00401038010', {"key_identifier": 1, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402038010', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
( 'c00402038810', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "aes"} ]} ),
]
KeyTypeLen = Struct('type'/KeyType, 'length'/Int8ub)
_construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
'key_types'/GreedyRange(KeyTypeLen))
class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
pass
# GP v2.3 11.1.9
KeyUsageQualifier = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
verification_encryption=0x8000,
computation_decipherment=0x4000,
sm_response=0x2000,
sm_command=0x1000,
confidentiality=0x0800,
crypto_checksum=0x0400,
digital_signature=0x0200,
crypto_authorization=0x0100,
key_agreement=0x0080)
# GP v2.3 11.1.10
KeyAccess = Enum(Byte, sd_and_any_assoc_app=0x00, sd_only=0x01, any_assoc_app_but_not_sd=0x02,
not_available=0xff)
class KeyLoading:
# Global Platform Specification v2.3 Section 11.11.4.2.2.3 DGIs for the CC Private Key
class KeyUsageQualifier(BER_TLV_IE, tag=0x95):
_construct = KeyUsageQualifier
class KeyAccess(BER_TLV_IE, tag=0x96):
_construct = KeyAccess
class KeyType(BER_TLV_IE, tag=0x80):
_construct = KeyType
class KeyLength(BER_TLV_IE, tag=0x81):
_construct = GreedyInteger()
class KeyIdentifier(BER_TLV_IE, tag=0x82):
_construct = Int8ub
class KeyVersionNumber(BER_TLV_IE, tag=0x83):
_construct = Int8ub
class KeyParameterReferenceValue(BER_TLV_IE, tag=0x85):
_construct = Enum(Byte, secp256r1=0x00, secp384r1=0x01, secp521r1=0x02, brainpoolP256r1=0x03,
brainpoolP256t1=0x04, brainpoolP384r1=0x05, brainpoolP384t1=0x06,
brainpoolP512r1=0x07, brainpoolP512t1=0x08)
# pylint: disable=undefined-variable
class ControlReferenceTemplate(BER_TLV_IE, tag=0xb9,
nested=[KeyUsageQualifier,
KeyAccess,
KeyType,
KeyLength,
KeyIdentifier,
KeyVersionNumber,
KeyParameterReferenceValue]):
pass
# Table 11-103
class EccPublicKey(DGI_TLV_IE, tag=0x0036):
_construct = GreedyBytes
# Table 11-105
class EccPrivateKey(DGI_TLV_IE, tag=0x8137):
_construct = GreedyBytes
# Global Platform Specification v2.3 Section 11.11.4 / Table 11-91
class KeyControlReferenceTemplate(DGI_TLV_IE, tag=0x00b9, nested=[ControlReferenceTemplate]):
pass
# GlobalPlatform v2.3.1 Section H.4 / Table H-6
class ScpType(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Byte)
class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
_construct = GreedyBytes
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
_consuruct = GreedyRange(Int16ub)
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
SupportedTlsCipherSuitesForScp81]):
pass
class PrivilegesAvailableSSD(BER_TLV_IE, tag=0x81):
pass
class PrivilegesAvailableApplication(BER_TLV_IE, tag=0x82):
pass
class SupportedLFDBHAlgorithms(BER_TLV_IE, tag=0x83):
pass
# GlobalPlatform Card Specification v2.3 / Table H-8
class CiphersForLFDBEncryption(BER_TLV_IE, tag=0x84):
_construct = Enum(Byte, tripledes16=0x01, aes128=0x02, aes192=0x04, aes256=0x08,
icv_supported_for_lfdb=0x80)
CipherSuitesForSignatures = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
rsa1024_pkcsv15_sha1=0x0100,
rsa_gt1024_pss_sha256=0x0200,
single_des_plus_final_triple_des_mac_16b=0x0400,
cmac_aes128=0x0800, cmac_aes192=0x1000, cmac_aes256=0x2000,
ecdsa_ecc256_sha256=0x4000, ecdsa_ecc384_sha384=0x8000,
ecdsa_ecc512_sha512=0x0001, ecdsa_ecc_521_sha512=0x0002)
class CiphersForTokens(BER_TLV_IE, tag=0x85):
_construct = CipherSuitesForSignatures
class CiphersForReceipts(BER_TLV_IE, tag=0x86):
_construct = CipherSuitesForSignatures
class CiphersForDAPs(BER_TLV_IE, tag=0x87):
_construct = CipherSuitesForSignatures
class KeyParameterReferenceList(BER_TLV_IE, tag=0x88, nested=[KeyLoading.KeyParameterReferenceValue]):
pass
class CardCapabilityInformation(BER_TLV_IE, tag=0x67, nested=[ScpInformation, PrivilegesAvailableSSD,
PrivilegesAvailableApplication,
SupportedLFDBHAlgorithms,
CiphersForLFDBEncryption, CiphersForTokens,
CiphersForReceipts, CiphersForDAPs,
KeyParameterReferenceList]):
pass
class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
_construct = Int8ub
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
class ApplicationAID(BER_TLV_IE, tag=0x4f):
_construct = HexAdapter(GreedyBytes)
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
pass
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
pass
# GlobalPlatform v2.3.1 Section 11.3.3.1.2 + TS 102 226
class NumberOFInstalledApp(BER_TLV_IE, tag=0x81):
_construct = GreedyInteger()
class FreeNonVolatileMemory(BER_TLV_IE, tag=0x82):
_construct = GreedyInteger()
class FreeVolatileMemory(BER_TLV_IE, tag=0x83):
_construct = GreedyInteger()
class ExtendedCardResourcesInfo(BER_TLV_IE, tag=0xff21, nested=[NumberOFInstalledApp, FreeNonVolatileMemory,
FreeVolatileMemory]):
pass
# GlobalPlatform v2.3.1 Section 7.4.2.4 + GP SPDM
class SecurityDomainManagerURL(BER_TLV_IE, tag=0x5f50):
pass
# card data sample, returned in response to GET DATA (80ca006600):
# 66 31
# 73 2f
# 06 07
# 2a864886fc6b01
# 60 0c
# 06 0a
# 2a864886fc6b02020101
# 63 09
# 06 07
# 2a864886fc6b03
# 64 0b
# 06 09
# 2a864886fc6b040215
# GlobalPlatform 2.1.1 Table F-1
class ObjectIdentifier(BER_TLV_IE, tag=0x06):
_construct = GreedyBytes
class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
pass
class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
pass
class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
pass
class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
_construct = GreedyBytes
class CardChipDetails(BER_TLV_IE, tag=0x66):
_construct = GreedyBytes
class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
CardManagementTypeAndVersion,
CardIdentificationScheme,
SecureChannelProtocolOfISD,
CardConfigurationDetails,
CardChipDetails]):
pass
class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
pass
# GlobalPlatform 2.1.1 Table F-2
class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
pass
class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
CardIdentificationScheme,
SecureChannelProtocolOfSelectedSD,
CardConfigurationDetails,
CardChipDetails]):
pass
# GlobalPlatform 2.1.1 Section 9.1.1
IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
card_locked = 0x7f, terminated=0xff)
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ApplicationID(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
_construct = GreedyBytes
# GlobalPlatform 2.1.1 Section 9.9.3.1
class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
_construct = GreedyInteger()
# GlobalPlatform 2.1.1 Section 9.9.3.1
class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
ApplicationProductionLifeCycleData,
MaximumLengthOfDataFieldInCommandMessage]):
pass
# explicitly define this list and give it a name so pySim.euicc can reference it
FciTemplateNestedList = [ApplicationID, SecurityDomainManagementData,
ApplicationProductionLifeCycleData,
MaximumLengthOfDataFieldInCommandMessage,
ProprietaryData]
# GlobalPlatform 2.1.1 Section 9.9.3.1
class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
pass
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
_construct = HexAdapter(GreedyBytes)
class CardImageNumber(BER_TLV_IE, tag=0x45):
_construct = HexAdapter(GreedyBytes)
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
_construct = GreedyInteger()
class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
_construct = GreedyInteger()
# Collection of all the data objects we can get from GET DATA
class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
CardImageNumber,
CardData,
KeyInformation,
SequenceCounterOfDefaultKvn,
ConfirmationCounter,
# v2.3.1
CardCapabilityInformation,
CurrentSecurityLevel,
ListOfApplications,
ExtendedCardResourcesInfo,
SecurityDomainManagerURL]):
pass
def decode_select_response(resp_hex: str) -> object:
t = FciTemplate()
t.from_tlv(h2b(resp_hex))
d = t.to_dict()
return flatten_dict_lists(d['fci_template'])
# 11.4.2.1
StatusSubset = Enum(Byte, isd=0x80, applications=0x40, files=0x20, files_and_modules=0x10)
# Section 11.4.3.1 Table 11-36
class LifeCycleState(BER_TLV_IE, tag=0x9f70):
_construct = CLifeCycleState
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
class Privileges(BER_TLV_IE, tag=0xc5):
# we only support 3-byte encoding. Can't use StripTrailerAdapter as length==2 is not permitted. sigh.
_construct = FlagsEnum(Int24ub,
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)
# Section 11.4.3.1 Table 11-36 + Section 11.1.7
class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
_construct = BitStruct('contactless_io'/Flag,
'contact_io'/Flag,
'_rfu'/Flag,
'logical_channel_number'/BitsInteger(5))
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
# Note: the Executable Load File Version Number format and contents are beyond the scope of this
# specification. It shall consist of the version information contained in the original Load File: on a
# Java Card based card, this version number represents the major and minor version attributes of the
# original Load File Data Block.
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
ImplicitSelectionParameter, ExecutableLoadFileAID,
ExecutableLoadFileVersionNumber,
ExecutableModuleAID, AssociatedSecurityDomainAID]):
pass
# Section 11.6.2.3 / Table 11-58
class SecurityDomainAid(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
class LoadFileDataBlockSignature(BER_TLV_IE, tag=0xc3):
_construct = GreedyBytes
class DapBlock(BER_TLV_IE, tag=0xe2, nested=[SecurityDomainAid, LoadFileDataBlockSignature]):
pass
class LoadFileDataBlock(BER_TLV_IE, tag=0xc4):
_construct = GreedyBytes
class Icv(BER_TLV_IE, tag=0xd3):
_construct = GreedyBytes
class CipheredLoadFileDataBlock(BER_TLV_IE, tag=0xd4):
_construct = GreedyBytes
class LoadFile(TLV_IE_Collection, nested=[DapBlock, LoadFileDataBlock, Icv, CipheredLoadFileDataBlock]):
pass
# Application Dedicated File of a Security Domain
class ADF_SD(CardADF):
StoreData = BitStruct('last_block'/Flag,
'encryption'/Enum(BitsInteger(2), none=0, application_dependent=1, rfu=2, encrypted=3),
'structure'/Enum(BitsInteger(2), none=0, dgi=1, ber_tlv=2, rfu=3),
'_pad'/Padding(2),
'response'/Enum(Bit, not_expected=0, may_be_returned=1))
def __init__(self, aid: str, name: str, desc: str):
super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
self.shell_commands += [self.AddlShellCommands()]
def decode_select_response(self, data_hex: str) -> object:
return decode_select_response(data_hex)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
get_data_parser = argparse.ArgumentParser()
get_data_parser.add_argument('data_object_name', type=str,
help='Name of the data object to be retrieved from the card')
@cmd2.with_argparser(get_data_parser)
def do_get_data(self, opts):
"""Perform the GlobalPlatform GET DATA command in order to obtain some card-specific data."""
tlv_cls_name = opts.data_object_name
try:
tlv_cls = DataCollection().members_by_name[tlv_cls_name]
except KeyError:
do_names = [camel_to_snake(str(x.__name__)) for x in DataCollection.possible_nested]
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
do_names))
return
(data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
ie = tlv_cls()
ie.from_tlv(h2b(data))
self._cmd.poutput_json(ie.to_dict())
def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
index_dict = {1: data_dict}
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
store_data_parser = argparse.ArgumentParser()
store_data_parser.add_argument('--data-structure', type=str, choices=['none','dgi','ber_tlv','rfu'], default='none')
store_data_parser.add_argument('--encryption', type=str, choices=['none','application_dependent', 'rfu', 'encrypted'], default='none')
store_data_parser.add_argument('--response', type=str, choices=['not_expected','may_be_returned'], default='not_expected')
store_data_parser.add_argument('DATA', type=is_hexstr)
@cmd2.with_argparser(store_data_parser)
def do_store_data(self, opts):
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
response_permitted = opts.response == 'may_be_returned'
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
# Table 11-89 of GP Card Specification v2.3
remainder = data
block_nr = 0
response = ''
while len(remainder):
chunk = remainder[:max_cmd_len]
remainder = remainder[max_cmd_len:]
p1b = build_construct(ADF_SD.StoreData,
{'last_block': len(remainder) == 0, 'encryption': encryption,
'structure': structure, 'response': response_permitted})
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk))
block_nr += 1
response += data
return data
put_key_parser = argparse.ArgumentParser()
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
put_key_parser.add_argument('--key-version-nr', type=auto_uint8, required=True, help='Key Version Number')
put_key_parser.add_argument('--key-id', type=auto_uint7, required=True, help='Key Identifier (base)')
put_key_parser.add_argument('--key-type', choices=KeyType.ksymapping.values(), action='append', required=True, help='Key Type')
put_key_parser.add_argument('--key-data', type=is_hexstr, action='append', required=True, help='Key Data Block')
put_key_parser.add_argument('--key-check', type=is_hexstr, action='append', help='Key Check Value')
put_key_parser.add_argument('--suppress-key-check', action='store_true', help='Suppress generation of Key Check Values')
@cmd2.with_argparser(put_key_parser)
def do_put_key(self, opts):
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
otherwise be automatically generated for DES and AES keys. You can suppress the latter using
`--suppress-key-check`.
Example (SCP80 KIC/KID/KIK):
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
--key-type aes --key-data 101112131415161718191a1b1c1d1e1f
--key-type aes --key-data 202122232425262728292a2b2c2d2e2f
Example (SCP81 TLS-PSK/KEK):
put_key --key-version-nr 0x40 --key-id 0x01 --key-type tls_psk --key-data 303132333435363738393a3b3c3d3e3f
--key-type des --key-data 404142434445464748494a4b4c4d4e4f
"""
if len(opts.key_type) != len(opts.key_data):
raise ValueError('There must be an equal number of key-type and key-data arguments')
kdb = []
for i in range(0, len(opts.key_type)):
if opts.key_check and len(opts.key_check) > i:
kcv = opts.key_check[i]
elif opts.suppress_key_check:
kcv = ''
else:
kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
kcv = b2h(kcv_bin)
if self._cmd.lchan.scc.scp:
# encrypte key data with DEK of current SCP
kcb = b2h(self._cmd.lchan.scc.scp.card_keys.encrypt_key(h2b(opts.key_data[i])))
else:
# (for example) during personalization, DEK might not be required)
kcb = opts.key_data[i]
kdb.append({'key_type': opts.key_type[i], 'kcb': kcb, 'kcv': kcv})
p2 = opts.key_id
if len(opts.key_type) > 1:
p2 |= 0x80
self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
# Table 11-68: Key Data Field - Format 1 (Basic Format)
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
'kcb'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'kcv'/HexAdapter(Prefixed(Int8ub, GreedyBytes))))
def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data))
return data
get_status_parser = argparse.ArgumentParser()
get_status_parser.add_argument('subset', choices=StatusSubset.ksymapping.values(),
help='Subset of statuses to be included in the response')
get_status_parser.add_argument('--aid', type=is_hexstr, default='',
help='AID Search Qualifier (search only for given AID)')
@cmd2.with_argparser(get_status_parser)
def do_get_status(self, opts):
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
grd_list = self.get_status(opts.subset, opts.aid)
for grd in grd_list:
self._cmd.poutput_json(grd.to_dict())
def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]:
subset_hex = b2h(build_construct(StatusSubset, subset))
aid = ApplicationAID(decoded=aid_search_qualifier)
cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc')
p2 = 0x02 # TLV format according to Table 11-36
grd_list = []
while True:
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data))
remainder = h2b(data)
while len(remainder):
# tlv sequence, each element is one GpRegistryRelatedData()
grd = GpRegistryRelatedData()
_dec, remainder = grd.from_tlv(remainder)
grd_list.append(grd)
if sw != '6310':
return grd_list
else:
p2 |= 0x01
return grd_list
set_status_parser = argparse.ArgumentParser()
set_status_parser.add_argument('scope', choices=SetStatusScope.ksymapping.values(),
help='Defines the scope of the requested status change')
set_status_parser.add_argument('status', choices=CLifeCycleState.ksymapping.values(),
help='Specify the new intended status')
set_status_parser.add_argument('--aid', type=is_hexstr,
help='AID of the target Application or Security Domain')
@cmd2.with_argparser(set_status_parser)
def do_set_status(self, opts):
"""Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
prior authentication with a Secure Channel Protocol."""
self.set_status(opts.scope, opts.status, opts.aid)
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
'scope'/SetStatusScope, 'status'/CLifeCycleState,
'aid'/HexAdapter(Prefixed(Int8ub, COptional(GreedyBytes))))
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
_data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
inst_perso_parser = argparse.ArgumentParser()
inst_perso_parser.add_argument('application-aid', type=is_hexstr, help='Application AID')
@cmd2.with_argparser(inst_perso_parser)
def do_install_for_personalization(self, opts):
"""Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security
Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
# Section 11.5.2.3.6 / Table 11-47
self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid))
inst_inst_parser = argparse.ArgumentParser()
inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
help='Executable Load File AID')
inst_inst_parser.add_argument('--module-aid', type=is_hexstr, default='',
help='Executable Module AID')
inst_inst_parser.add_argument('--application-aid', type=is_hexstr, required=True,
help='Application AID')
inst_inst_parser.add_argument('--install-parameters', type=is_hexstr, default='',
help='Install Parameters')
inst_inst_parser.add_argument('--privilege', action='append', dest='privileges', default=[],
choices=Privileges._construct.flags.keys(),
help='Privilege granted to newly installed Application')
inst_inst_parser.add_argument('--install-token', type=is_hexstr, default='',
help='Install Token (Section GPCS C.4.2/C.4.7)')
inst_inst_parser.add_argument('--make-selectable', action='store_true',
help='Install and make selectable')
@cmd2.with_argparser(inst_inst_parser)
def do_install_for_install(self, opts):
"""Perform GlobalPlatform INSTALL [for install] command in order to install an application."""
InstallForInstallCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'module_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'application_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'privileges'/Prefixed(Int8ub, Privileges._construct),
'install_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'install_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
p1 = 0x04
if opts.make_selectable:
p1 |= 0x08
decoded = vars(opts)
# convert from list to "true-dict" as required by construct.FlagsEnum
decoded['privileges'] = {x: True for x in decoded['privileges']}
ifi_bytes = build_construct(InstallForInstallCD, decoded)
self.install(p1, 0x00, b2h(ifi_bytes))
inst_load_parser = argparse.ArgumentParser()
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
help='AID of the loded file')
inst_load_parser.add_argument('--security-domain-aid', type=is_hexstr, default='',
help='AID of the Security Domain into which the file shalle be added')
inst_load_parser.add_argument('--load-file-hash', type=is_hexstr, default='',
help='Load File Data Block Hash')
inst_inst_parser.add_argument('--load-parameters', type=is_hexstr, default='',
help='Load Token (Section GPCS C.4.1)')
inst_inst_parser.add_argument('--load-token', type=is_hexstr, default='',
help='Load Token (Section GPCS C.4.1)')
@cmd2.with_argparser(inst_load_parser)
def do_install_for_load(self, opts):
"""Perform GlobalPlatform INSTALL [for load] command."""
if opts.load_token != '' and opts.load_file_hash == '':
raise ValueError('Load File Data Block Hash is mandatory if a Load Token is present')
InstallForLoadCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'security_domain_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'load_file_hash'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'load_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'load_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
self.install(0x02, 0x00, b2h(ifl_bytes))
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
del_cc_parser = argparse.ArgumentParser()
del_cc_parser.add_argument('aid', type=is_hexstr,
help='Executable Load File or Application AID')
del_cc_parser.add_argument('--delete-related-objects', action='store_true',
help='Delete not only the object but also its related objects')
@cmd2.with_argparser(del_cc_parser)
def do_delete_card_content(self, opts):
"""Perform a GlobalPlatform DELETE [card content] command in order to delete an Executable Load
File, an Application or an Executable Load File and its related Applications."""
p2 = 0x80 if opts.delete_related_objects else 0x00
aid = ApplicationAID(decoded=opts.aid)
self.delete(0x00, p2, b2h(aid.to_tlv()))
del_key_parser = argparse.ArgumentParser()
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
del_key_parser.add_argument('--key-ver', type=auto_uint8, help='Key Version Number (KVN)')
del_key_parser.add_argument('--delete-related-objects', action='store_true',
help='Delete not only the object but also its related objects')
@cmd2.with_argparser(del_key_parser)
def do_delete_key(self, opts):
"""Perform GlobalPlaform DELETE (Key) command.
If both KID and KVN are specified, exactly one key is deleted. If only either of the two is
specified, multiple matching keys may be deleted."""
if opts.key_id is None and opts.key_ver is None:
raise ValueError('At least one of KID or KVN must be specified')
p2 = 0x80 if opts.delete_related_objects else 0x00
cmd = ""
if opts.key_id is not None:
cmd += "d001%02x" % opts.key_id
if opts.key_ver is not None:
cmd += "d201%02x" % opts.key_ver
self.delete(0x00, p2, cmd)
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
load_parser = argparse.ArgumentParser()
# we make this a required --optional argument now, so we can later have other sources for load data
load_parser.add_argument('--from-file', required=True)
@cmd2.with_argparser(load_parser)
def do_load(self, opts):
"""Perform a GlobalPlatform LOAD command. We currently only support loading without DAP and
without ciphering."""
with open(opts.from_file, 'rb') as f:
self.load(f)
def load(self, stream: io.RawIOBase, chunk_len:int = 240):
# we might want to tune chunk_len based on the overhead of the used SCP?
contents = stream.readall()
# build TLV according to 11.6.2.3 / Table 11-58 for unencrypted case
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
# transfer this in vaious chunks to the card
total_size = len(remainder)
block_nr = 0
while len(remainder):
block = remainder[:chunk_len]
remainder = remainder[chunk_len:]
# build LOAD command APDU according to 11.6.2 / Table 11-56
p1 = 0x80 if len(remainder) else 0x00
p2 = block_nr % 256
block_nr += 1
cmd_hex = "80E8%02x%02x%02x%s" % (p1, p2, len(block), b2h(block))
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_load now!" % (total_size, block_nr))
est_scp02_parser = argparse.ArgumentParser()
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True,
help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--key-enc', type=is_hexstr, required=True,
help='Secure Channel Encryption Key')
est_scp02_parser.add_argument('--key-mac', type=is_hexstr, required=True,
help='Secure Channel MAC Key')
est_scp02_parser.add_argument('--key-dek', type=is_hexstr, required=True,
help='Data Encryption Key')
est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
help='Hard-code the host challenge; default: random')
est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
help='Security Level. Default: 0x01 (C-MAC only)')
@cmd2.with_argparser(est_scp02_parser)
def do_establish_scp02(self, opts):
"""Establish a secure channel using the GlobalPlatform SCP02 protocol. It can be released
again by using `release_scp`."""
if self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot establish SCP02 as this lchan already has a SCP instance!")
return
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
scp02 = SCP02(card_keys=kset)
self._establish_scp(scp02, host_challenge, opts.security_level)
est_scp03_parser = deepcopy(est_scp02_parser)
est_scp03_parser.add_argument('--s16-mode', action='store_true', help='S16 mode (S8 is default)')
@cmd2.with_argparser(est_scp03_parser)
def do_establish_scp03(self, opts):
"""Establish a secure channel using the GlobalPlatform SCP03 protocol. It can be released
again by using `release_scp`."""
if self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot establish SCP03 as this lchan already has a SCP instance!")
return
s_mode = 16 if opts.s16_mode else 8
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
scp03 = SCP03(card_keys=kset, s_mode = s_mode)
self._establish_scp(scp03, host_challenge, opts.security_level)
def _establish_scp(self, scp, host_challenge, security_level):
# perform the common functionality shared by SCP02 and SCP03 establishment
init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
init_update_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
scp.parse_init_update_resp(h2b(init_update_resp))
ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
_ext_auth_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
# store a reference to the SCP instance
self._cmd.lchan.scc.scp = scp
self._cmd.update_prompt()
def do_release_scp(self, _opts):
"""Release a previously establiehed secure channel."""
if not self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot release SCP as none is established")
return
self._cmd.lchan.scc.scp = None
self._cmd.update_prompt()
# Card Application of a Security Domain
class CardApplicationSD(CardApplication):
__intermediate = True
def __init__(self, aid: str, name: str, desc: str):
super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
# Card Application of Issuer Security Domain
class CardApplicationISD(CardApplicationSD):
# FIXME: ISD AID is not static, but could be different. One can select the empty
# application using '00a4040000' and then parse the response FCI to get the ISD AID
def __init__(self, aid='a000000003000000'):
super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
#class CardProfileGlobalPlatform(CardProfile):
# ORDER = 23
#
# def __init__(self, name='GlobalPlatform'):
# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
class GpCardKeyset:
"""A single set of GlobalPlatform card keys and the associated KVN."""
def __init__(self, kvn: int, enc: bytes, mac: bytes, dek: bytes):
assert 0 < kvn < 256
assert len(enc) == len(mac) == len(dek)
self.kvn = kvn
self.enc = enc
self.mac = mac
self.dek = dek
@classmethod
def from_single_key(cls, kvn: int, base_key: bytes) -> 'GpCardKeyset':
return cls(kvn, base_key, base_key, base_key)
def __str__(self):
return "%s(KVN=%u, ENC=%s, MAC=%s, DEK=%s)" % (self.__class__.__name__,
self.kvn, b2h(self.enc), b2h(self.mac), b2h(self.dek))
def compute_kcv_des(key:bytes) -> bytes:
# GP Card Spec B.6: For a DES key, the key check value is computed by encrypting 8 bytes, each with
# value '00', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
# result.
plaintext = b'\x00' * 8
cipher = DES3.new(key, DES.MODE_ECB)
return cipher.encrypt(plaintext)
def compute_kcv_aes(key:bytes) -> bytes:
# GP Card Spec B.6: For a AES key, the key check value is computed by encrypting 16 bytes, each with
# value '01', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
# result.
plaintext = b'\x01' * 16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(plaintext)
# dict is keyed by the string name of the KeyType enum above in this file
KCV_CALCULATOR = {
'aes': compute_kcv_aes,
'des': compute_kcv_des,
}
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
"""Compute the KCV (Key Check Value) for given key type and key."""
kcv_calculator = KCV_CALCULATOR.get(key_type)
if not kcv_calculator:
return None
else:
return kcv_calculator(key)[:3]

View File

@@ -0,0 +1,534 @@
# Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation
#
# (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 abc
import logging
from typing import Optional
from Cryptodome.Cipher import DES3, DES
from Cryptodome.Util.strxor import strxor
from construct import Struct, Bytes, Int8ub, Int16ub, Const
from construct import Optional as COptional
from pySim.utils import b2h, bertlv_parse_len, bertlv_encode_len
from pySim.secure_channel import SecureChannel
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes:
assert len(constant) == 2
assert(counter >= 0 and counter <= 65535)
assert len(base_key) == 16
derivation_data = constant + counter.to_bytes(2, 'big') + b'\x00' * 12
cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8)
return cipher.encrypt(derivation_data)
# TODO: resolve duplication with BspAlgoCryptAES128
def pad80(s: bytes, BS=8) -> bytes:
""" Pad bytestring s: add '\x80' and '\0'* so the result to be multiple of BS."""
l = BS-1 - len(s) % BS
return s + b'\x80' + b'\0'*l
# TODO: resolve duplication with BspAlgoCryptAES128
def unpad80(padded: bytes) -> bytes:
"""Remove the customary 80 00 00 ... padding used for AES."""
# first remove any trailing zero bytes
stripped = padded.rstrip(b'\0')
# then remove the final 80
assert stripped[-1] == 0x80
return stripped[:-1]
class Scp02SessionKeys:
"""A single set of GlobalPlatform session keys."""
DERIV_CONST_CMAC = b'\x01\x01'
DERIV_CONST_RMAC = b'\x01\x02'
DERIV_CONST_ENC = b'\x01\x82'
DERIV_CONST_DENC = b'\x01\x81'
blocksize = 8
def calc_mac_1des(self, data: bytes, reset_icv: bool = False) -> bytes:
"""Pad and calculate MAC according to B.1.2.2 - Single DES plus final 3DES"""
e = DES.new(self.c_mac[:8], DES.MODE_ECB)
d = DES.new(self.c_mac[8:], DES.MODE_ECB)
padded_data = pad80(data, 8)
q = len(padded_data) // 8
icv = b'\x00' * 8 if reset_icv else self.icv
h = icv
for i in range(q):
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
h = d.decrypt(h)
h = e.encrypt(h)
logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
if self.des_icv_enc:
self.icv = self.des_icv_enc.encrypt(h)
else:
self.icv = h
return h
def calc_mac_3des(self, data: bytes) -> bytes:
e = DES3.new(self.enc, DES.MODE_ECB)
padded_data = pad80(data, 8)
q = len(padded_data) // 8
h = b'\x00' * 8
for i in range(q):
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
return h
def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True):
self.icv = None
self.counter = counter
self.card_keys = card_keys
self.c_mac = scp02_key_derivation(self.DERIV_CONST_CMAC, self.counter, card_keys.mac)
self.r_mac = scp02_key_derivation(self.DERIV_CONST_RMAC, self.counter, card_keys.mac)
self.enc = scp02_key_derivation(self.DERIV_CONST_ENC, self.counter, card_keys.enc)
self.data_enc = scp02_key_derivation(self.DERIV_CONST_DENC, self.counter, card_keys.dek)
self.des_icv_enc = DES.new(self.c_mac[:8], DES.MODE_ECB) if icv_encrypt else None
def __str__(self) -> str:
return "%s(CTR=%u, ICV=%s, ENC=%s, D-ENC=%s, MAC-C=%s, MAC-R=%s)" % (
self.__class__.__name__, self.counter, b2h(self.icv) if self.icv else "None",
b2h(self.enc), b2h(self.data_enc), b2h(self.c_mac), b2h(self.r_mac))
INS_INIT_UPDATE = 0x50
INS_EXT_AUTH = 0x82
CLA_SM = 0x04
class SCP(SecureChannel, abc.ABC):
"""Abstract base class containing some common interface + functionality for SCP protocols."""
def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0):
if hasattr(self, 'kvn_range'):
if not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1):
raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' %
(self.__class__.__name__, self.kvn_range[0], self.kvn_range[1]))
self.lchan_nr = lchan_nr
self.card_keys = card_keys
self.sk = None
self.mac_on_unmodified = False
self.security_level = 0x00
@property
def do_cmac(self) -> bool:
"""Should we perform C-MAC?"""
return self.security_level & 0x01
@property
def do_rmac(self) -> bool:
"""Should we perform R-MAC?"""
return self.security_level & 0x10
@property
def do_cenc(self) -> bool:
"""Should we perform C-ENC?"""
return self.security_level & 0x02
@property
def do_renc(self) -> bool:
"""Should we perform R-ENC?"""
return self.security_level & 0x20
def __str__(self) -> str:
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
def _cla(self, sm: bool = False, b8: bool = True) -> int:
ret = 0x80 if b8 else 0x00
if sm:
ret = ret | CLA_SM
return ret + self.lchan_nr
def wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
# Generic handling of GlobalPlatform SCP, implements SecureChannel.wrap_cmd_apdu
# only protect those APDUs that actually are global platform commands
if apdu[0] & 0x80:
return self._wrap_cmd_apdu(apdu, *args, **kwargs)
return apdu
@abc.abstractmethod
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
"""Method implementation to be provided by derived class."""
pass
@abc.abstractmethod
def gen_init_update_apdu(self, host_challenge: Optional[bytes]) -> bytes:
pass
@abc.abstractmethod
def parse_init_update_resp(self, resp_bin: bytes):
pass
@abc.abstractmethod
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
pass
def encrypt_key(self, key: bytes) -> bytes:
"""Encrypt a key with the DEK."""
num_pad = len(key) % self.sk.blocksize
if num_pad:
return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad)
return self.dek_encrypt(key)
def decrypt_key(self, encrypted_key:bytes) -> bytes:
"""Decrypt a key with the DEK."""
if len(encrypted_key) % self.sk.blocksize:
# If the length of the Key Component Block is not a multiple of the block size of the encryption #
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that the key
# component value was right-padded prior to encryption and that the Key Component Block was
# formatted as described in Table 11-70. In this case, the first byte(s) of the Key Component
# Block provides the actual length of the key component value, which allows recovering the
# clear-text key component value after decryption of the encrypted key component value and removal
# of padding bytes.
decrypted = self.dek_decrypt(encrypted_key)
key_len, remainder = bertlv_parse_len(decrypted)
return remainder[:key_len]
else:
# If the length of the Key Component Block is a multiple of the block size of the encryption
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding
# bytes were added before encrypting the key component value and that the Key Component Block is
# only composed of the encrypted key component value (as shown in Table 11-71). In this case, the
# clear-text key component value is simply recovered by decrypting the Key Component Block.
return self.dek_decrypt(encrypted_key)
@abc.abstractmethod
def dek_encrypt(self, plaintext:bytes) -> bytes:
pass
@abc.abstractmethod
def dek_decrypt(self, ciphertext:bytes) -> bytes:
pass
class SCP02(SCP):
"""An instance of the GlobalPlatform SCP02 secure channel protocol."""
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x02'),
'seq_counter'/Int16ub, 'card_challenge'/Bytes(6), 'card_cryptogram'/Bytes(8))
kvn_range = [0x20, 0x2f]
def __init__(self, *args, **kwargs):
self.overhead = 8
super().__init__(*args, **kwargs)
def dek_encrypt(self, plaintext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
return cipher.encrypt(plaintext)
def dek_decrypt(self, ciphertext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
return cipher.decrypt(ciphertext)
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge)
self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge)
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
"""Generate INITIALIZE UPDATE APDU."""
self.host_challenge = host_challenge
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALZIE UPDATE."""
resp = self.constr_iur.parse(resp_bin)
self.card_challenge = resp['card_challenge']
self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys)
logger.debug(self.sk)
self._compute_cryptograms(self.card_challenge, self.host_challenge)
if self.card_cryptogram != resp['card_cryptogram']:
raise ValueError("card cryptogram doesn't match")
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
"""Generate EXTERNAL AUTHENTICATE APDU."""
if security_level & 0xf0:
raise NotImplementedError('R-MAC/R-ENC for SCP02 not implemented yet.')
self.security_level = security_level
if self.mac_on_unmodified:
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, 8])
else:
header = bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16])
#return self.wrap_cmd_apdu(header + self.host_cryptogram)
mac = self.sk.calc_mac_1des(header + self.host_cryptogram, True)
return bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16]) + self.host_cryptogram + mac
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
lc = len(apdu) - 5
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
cla = apdu[0]
b8 = cla & 0x80
if cla & 0x03 or cla & CLA_SM:
# nonzero logical channel in APDU, check that are the same
assert cla == self._cla(False, b8), "CLA mismatch"
# CLA without log. channel can be 80 or 00 only
if self.do_cmac:
if self.mac_on_unmodified:
mlc = lc
clac = cla
else: # CMAC on modified APDU
mlc = lc + 8
clac = cla | CLA_SM
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
if self.do_cenc:
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
data = k.encrypt(pad80(apdu[5:], 8))
lc = len(data)
else:
data = apdu[5:]
lc += 8
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
# TODO: Implement R-MAC / R-ENC
return rsp_apdu
from Cryptodome.Cipher import AES
from Cryptodome.Hash import CMAC
def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Optional[int] = None) -> bytes:
"""SCP03 Key Derivation Function as specified in Annex D 4.1.5."""
# Data derivation shall use KDF in counter mode as specified in NIST SP 800-108 ([NIST 800-108]). The PRF
# used in the KDF shall be CMAC as specified in [NIST 800-38B], used with full 16-byte output length.
def prf(key: bytes, data:bytes):
return CMAC.new(key, data, AES).digest()
if l is None:
l = len(base_key) * 8
logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
output_len = l // 8
# SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the
# existing Cryptodome.Protocol.KDF.SP800_108_Counter function :(
# A 12-byte “label” consisting of 11 bytes with value '00' followed by a 1-byte derivation constant
assert len(constant) == 1
label = b'\x00' *11 + constant
i = 1
dk = b''
while len(dk) < output_len:
# 12B label, 1B separation, 2B L, 1B i, Context
info = label + b'\x00' + l.to_bytes(2, 'big') + bytes([i]) + context
dk += prf(base_key, info)
i += 1
if i > 0xffff:
raise ValueError("Overflow in SP800 108 counter")
return dk[:output_len]
class Scp03SessionKeys:
# GPC 2.3 Amendment D v1.2 Section 4.1.5 Table 4-1
DERIV_CONST_AUTH_CGRAM_CARD = b'\x00'
DERIV_CONST_AUTH_CGRAM_HOST = b'\x01'
DERIV_CONST_CARD_CHLG_GEN = b'\x02'
DERIV_CONST_KDERIV_S_ENC = b'\x04'
DERIV_CONST_KDERIV_S_MAC = b'\x06'
DERIV_CONST_KDERIV_S_RMAC = b'\x07'
blocksize = 16
def __init__(self, card_keys: 'GpCardKeyset', host_challenge: bytes, card_challenge: bytes):
# GPC 2.3 Amendment D v1.2 Section 6.2.1
context = host_challenge + card_challenge
self.s_enc = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_ENC, context, card_keys.enc)
self.s_mac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_MAC, context, card_keys.mac)
self.s_rmac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_RMAC, context, card_keys.mac)
# The first MAC chaining value is set to 16 bytes '00'
self.mac_chaining_value = b'\x00' * 16
# The encryption counters start value shall be set to 1 (we set it immediately before generating ICV)
self.block_nr = 0
def calc_cmac(self, apdu: bytes):
"""Compute C-MAC for given to-be-transmitted APDU.
Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
cmac_input = self.mac_chaining_value + apdu
cmac_val = CMAC.new(self.s_mac, cmac_input, ciphermod=AES).digest()
self.mac_chaining_value = cmac_val
return cmac_val
def calc_rmac(self, rdata_and_sw: bytes):
"""Compute R-MAC for given received R-APDU data section.
Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
rmac_input = self.mac_chaining_value + rdata_and_sw
return CMAC.new(self.s_rmac, rmac_input, ciphermod=AES).digest()
def _get_icv(self, is_response: bool = False):
"""Obtain the ICV value computed as described in 6.2.6.
This method has two modes:
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
* is_response=False for computing the ICV for R-DEC."""
if not is_response:
self.block_nr += 1
# The binary value of this number SHALL be left padded with zeroes to form a full block.
data = self.block_nr.to_bytes(self.blocksize, "big")
if is_response:
# Section 6.2.7: additional intermediate step: Before encryption, the most significant byte of
# this block shall be set to '80'.
data = b'\x80' + data[1:]
iv = bytes([0] * self.blocksize)
# This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
icv = cipher.encrypt(data)
logger.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv))
return icv
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-wrapping
def _encrypt(self, data: bytes, is_response: bool = False) -> bytes:
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
return cipher.encrypt(data)
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-unwrapping
def _decrypt(self, data: bytes, is_response: bool = True) -> bytes:
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
return cipher.decrypt(data)
class SCP03(SCP):
"""Secure Channel Protocol (SCP) 03 as specified in GlobalPlatform v2.3 Amendment D."""
# Section 7.1.1.6 / Table 7-3
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x03'), 'i_param'/Int8ub,
'card_challenge'/Bytes(lambda ctx: ctx._.s_mode),
'card_cryptogram'/Bytes(lambda ctx: ctx._.s_mode),
'sequence_counter'/COptional(Bytes(3)))
kvn_range = [0x30, 0x3f]
def __init__(self, *args, **kwargs):
self.s_mode = kwargs.pop('s_mode', 8)
self.overhead = self.s_mode
super().__init__(*args, **kwargs)
def dek_encrypt(self, plaintext:bytes) -> bytes:
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
return cipher.encrypt(plaintext)
def dek_decrypt(self, ciphertext:bytes) -> bytes:
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
return cipher.decrypt(ciphertext)
def _compute_cryptograms(self):
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge))
# Card + Host Authentication Cryptogram: Section 6.2.2.2 + 6.2.2.3
context = self.host_challenge + self.card_challenge
self.card_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_CARD, context, self.sk.s_mac, l=self.s_mode*8)
self.host_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_HOST, context, self.sk.s_mac, l=self.s_mode*8)
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
"""Generate INITIALIZE UPDATE APDU."""
if host_challenge is None:
host_challenge = b'\x00' * self.s_mode
if len(host_challenge) != self.s_mode:
raise ValueError('Host Challenge must be %u bytes long' % self.s_mode)
self.host_challenge = host_challenge
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALIZE UPDATE."""
if len(resp_bin) not in [10+3+8+8, 10+3+16+16, 10+3+8+8+3, 10+3+16+16+3]:
raise ValueError('Invalid length of Initialize Update Response')
resp = self.constr_iur.parse(resp_bin, s_mode=self.s_mode)
self.card_challenge = resp['card_challenge']
self.i_param = resp['i_param']
# derive session keys and compute cryptograms
self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge)
logger.debug(self.sk)
self._compute_cryptograms()
# verify computed cryptogram matches received cryptogram
if self.card_cryptogram != resp['card_cryptogram']:
raise ValueError("card cryptogram doesn't match")
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
"""Generate EXTERNAL AUTHENTICATE APDU."""
self.security_level = security_level
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, self.s_mode])
# bypass encryption for EXTERNAL AUTHENTICATE
return self.wrap_cmd_apdu(header + self.host_cryptogram, skip_cenc=True)
def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
cla = apdu[0]
ins = apdu[1]
p1 = apdu[2]
p2 = apdu[3]
lc = apdu[4]
assert lc == len(apdu) - 5
cmd_data = apdu[5:]
if self.do_cenc and not skip_cenc:
assert self.do_cmac
if lc == 0:
# No encryption shall be applied to a command where there is no command data field. In this
# case, the encryption counter shall still be incremented
self.sk.block_nr += 1
else:
# data shall be padded as defined in [GPCS] section B.2.3
padded_data = pad80(cmd_data, 16)
lc = len(padded_data)
if lc >= 256:
raise ValueError('Modified Lc (%u) would exceed maximum when appending padding' % (lc))
# perform AES-CBC with ICV + S_ENC
cmd_data = self.sk._encrypt(padded_data)
if self.do_cmac:
# The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
# mode) to indicate the inclusion of the C-MAC in the data field of the command message.
mlc = lc + self.s_mode
if mlc >= 256:
raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
# The class byte shall be modified for the generation or verification of the C-MAC: The logical
# channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
# GlobalPlatform proprietary secure messaging.
mcla = (cla & 0xF0) | CLA_SM
mapdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
cmac = self.sk.calc_cmac(mapdu)
mapdu += cmac[:self.s_mode]
return mapdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
# No R-MAC shall be generated and no protection shall be applied to a response that includes an error
# status word: in this case only the status word shall be returned in the response. All status words
# except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status
# words.
logger.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu)
if not self.do_rmac:
assert not self.do_renc
return rsp_apdu
if sw != b'\x90\x00' and sw[0] not in [0x62, 0x63]:
return rsp_apdu
response_data = rsp_apdu[:-self.s_mode]
rmac = rsp_apdu[-self.s_mode:]
rmac_exp = self.sk.calc_rmac(response_data + sw)[:self.s_mode]
if rmac != rmac_exp:
raise ValueError("R-MAC value not matching: received: %s, computed: %s" % (rmac, rmac_exp))
if self.do_renc:
# decrypt response data
decrypted = self.sk._decrypt(response_data)
logger.debug("decrypted: %s", b2h(decrypted))
# remove padding
response_data = unpad80(decrypted)
logger.debug("response_data: %s", b2h(response_data))
return response_data

View File

@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
# without this, pylint will fail when inner classes are used
# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
# pylint: disable=undefined-variable
"""
The File (and its derived classes) uses the classes of pySim.filesystem in
order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for GSM-R SIM Cards"
"""
# without this, pylint will fail when inner classes are used
# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
# pylint: disable=undefined-variable
#
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
@@ -28,16 +25,13 @@ order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for
from pySim.utils import *
#from pySim.tlv import *
from struct import pack, unpack
from construct import *
from construct import Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
from construct import Optional as COptional
from pySim.construct import *
import enum
from pySim.construct import *
from pySim.profile import CardProfileAddon
from pySim.filesystem import *
import pySim.ts_102_221
import pySim.ts_51_011
######################################################################
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
@@ -47,10 +41,10 @@ import pySim.ts_51_011
class FuncNTypeAdapter(Adapter):
def _decode(self, obj, context, path):
bcd = swap_nibbles(b2h(obj))
last_digit = bcd[-1]
last_digit = int(bcd[-1], 16)
return {'functional_number': bcd[:-1],
'presentation_of_only_this_fn': last_digit & 4,
'permanent_fn': last_digit & 8}
'presentation_of_only_this_fn': bool(last_digit & 4),
'permanent_fn': bool(last_digit & 8)}
def _encode(self, obj, context, path):
return 'FIXME'
@@ -58,9 +52,13 @@ class FuncNTypeAdapter(Adapter):
class EF_FN(LinFixedEF):
"""Section 7.2"""
_test_decode = [
( "40315801000010ff01",
{ "functional_number_and_type": { "functional_number": "04138510000001f",
"presentation_of_only_this_fn": True, "permanent_fn": True }, "list_number": 1 } ),
]
def __init__(self):
super().__init__(fid='6ff1', sfid=None, name='EF.EN',
super().__init__(fid='6ff1', sfid=None, name='EF.FN',
desc='Functional numbers', rec_len=(9, 9))
self._construct = Struct('functional_number_and_type'/FuncNTypeAdapter(Bytes(8)),
'list_number'/Int8ub)
@@ -73,15 +71,15 @@ class PlConfAdapter(Adapter):
num = int(obj) & 0x7
if num == 0:
return 'None'
elif num == 1:
if num == 1:
return 4
elif num == 2:
if num == 2:
return 3
elif num == 3:
if num == 3:
return 2
elif num == 4:
if num == 4:
return 1
elif num == 5:
if num == 5:
return 0
def _encode(self, obj, context, path):
@@ -90,13 +88,13 @@ class PlConfAdapter(Adapter):
obj = int(obj)
if obj == 4:
return 1
elif obj == 3:
if obj == 3:
return 2
elif obj == 2:
if obj == 2:
return 3
elif obj == 1:
if obj == 1:
return 4
elif obj == 0:
if obj == 0:
return 5
@@ -107,19 +105,19 @@ class PlCallAdapter(Adapter):
num = int(obj) & 0x7
if num == 0:
return 'None'
elif num == 1:
if num == 1:
return 4
elif num == 2:
if num == 2:
return 3
elif num == 3:
if num == 3:
return 2
elif num == 4:
if num == 4:
return 1
elif num == 5:
if num == 5:
return 0
elif num == 6:
if num == 6:
return 'B'
elif num == 7:
if num == 7:
return 'A'
def _encode(self, obj, context, path):
@@ -127,17 +125,17 @@ class PlCallAdapter(Adapter):
return 0
if obj == 4:
return 1
elif obj == 3:
if obj == 3:
return 2
elif obj == 2:
if obj == 2:
return 3
elif obj == 1:
if obj == 1:
return 4
elif obj == 0:
if obj == 0:
return 5
elif obj == 'B':
if obj == 'B':
return 6
elif obj == 'A':
if obj == 'A':
return 7
@@ -147,7 +145,12 @@ NextTableType = Enum(Byte, decision=0xf0, predefined=0xf1,
class EF_CallconfC(TransparentEF):
"""Section 7.3"""
_test_de_encode = [
( "026121ffffffffffff1e000a040a010253600795792426f0",
{ "pl_conf": 3, "conf_nr": "1612ffffffffffff", "max_rand": 30, "n_ack_max": 10,
"pl_ack": 1, "n_nested_max": 10, "train_emergency_gid": 1, "shunting_emergency_gid": 2,
"imei": "350670599742620f" } ),
]
def __init__(self):
super().__init__(fid='6ff2', sfid=None, name='EF.CallconfC', size=(24, 24),
desc='Call Configuration of emergency calls Configuration')
@@ -180,7 +183,9 @@ class EF_CallconfI(LinFixedEF):
class EF_Shunting(TransparentEF):
"""Section 7.6"""
_test_de_encode = [
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
]
def __init__(self):
super().__init__(fid='6ff4', sfid=None,
name='EF.Shunting', desc='Shunting', size=(8, 8))
@@ -190,11 +195,22 @@ class EF_Shunting(TransparentEF):
class EF_GsmrPLMN(LinFixedEF):
"""Section 7.7"""
_test_de_encode = [
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
"fn": True, "eirene": True }, "preference": 0 },
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
"ic_table_ref": "01" } ),
( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
"fn": True, "eirene": False }, "preference": 1 },
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
"ic_table_ref": "02" } ),
]
def __init__(self):
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
desc='GSM-R network selection', rec_len=(9, 9))
self._construct = Struct('plmn'/BcdAdapter(Bytes(3)),
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
'preference'/BitsInteger(3)),
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
@@ -204,19 +220,29 @@ class EF_GsmrPLMN(LinFixedEF):
class EF_IC(LinFixedEF):
"""Section 7.8"""
_test_de_encode = [
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": "6f8e",
"ic_decision_value": "041f", "network_string_table_index": 1 } ),
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": "ffff",
"ic_decision_value": "ffff", "network_string_table_index": 65535 } ),
]
def __init__(self):
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
desc='International Code', rec_len=(7, 7))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)),
'ic_decision_value'/BcdAdapter(Bytes(2)),
'network_string_table_index'/Int8ub)
'network_string_table_index'/Int16ub)
class EF_NW(LinFixedEF):
"""Section 7.9"""
_test_de_encode = [
( "47534d2d52204348", "GSM-R CH" ),
( "537769737347534d", "SwissGSM" ),
( "47534d2d52204442", "GSM-R DB" ),
( "47534d2d52524649", "GSM-RRFI" ),
]
def __init__(self):
super().__init__(fid='6f80', sfid=None, name='EF.NW',
desc='Network Name', rec_len=(8, 8))
@@ -225,8 +251,15 @@ class EF_NW(LinFixedEF):
class EF_Switching(LinFixedEF):
"""Section 8.4"""
def __init__(self, fid, name, desc):
_test_de_encode = [
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": "6f87",
"decision_value": "0fff", "string_table_index": 0 } ),
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": "6f8f",
"decision_value": "1fff", "string_table_index": 1 } ),
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": "6f89",
"decision_value": "5fff", "string_table_index": 5 } ),
]
def __init__(self, fid='1234', name='Switching', desc=None):
super().__init__(fid=fid, sfid=None,
name=name, desc=desc, rec_len=(6, 6))
self._construct = Struct('next_table_type'/NextTableType,
@@ -237,22 +270,41 @@ class EF_Switching(LinFixedEF):
class EF_Predefined(LinFixedEF):
"""Section 8.5"""
_test_de_encode = [
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": "6f85" } ),
( "f0ffc8", 2, { "predefined_value1": "0fff", "string_table_index1": 200 } ),
]
# header and other records have different structure. WTF !?!
construct_first = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)))
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
'string_table_index1'/Int8ub)
def __init__(self, fid, name, desc):
def __init__(self, fid='1234', name='Predefined', desc=None):
super().__init__(fid=fid, sfid=None,
name=name, desc=desc, rec_len=(3, 3))
# header and other records have different structure. WTF !?!
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)),
'predefined_value1'/HexAdapter(Bytes(2)),
'string_table_index1'/Int8ub)
# TODO: predefined value n, ...
def _decode_record_bin(self, raw_bin_data : bytes, record_nr : int) -> dict:
if record_nr == 1:
return parse_construct(self.construct_first, raw_bin_data)
else:
return parse_construct(self.construct_others, raw_bin_data)
def _encode_record_bin(self, abstract_data : dict, record_nr : int) -> bytearray:
r = None
if record_nr == 1:
r = self.construct_first.build(abstract_data)
else:
r = self.construct_others.build(abstract_data)
return filter_dict(r)
class EF_DialledVals(TransparentEF):
"""Section 8.6"""
def __init__(self, fid, name, desc):
_test_de_encode = [
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": "ffff", "dialed_digits": "22" } ),
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": "6f88", "dialed_digits": "58" }),
]
def __init__(self, fid='1234', name='DialledVals', desc=None):
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)),
@@ -304,3 +356,15 @@ class DF_EIRENE(CardDF):
desc='Free Number Call Type 0 and 8'),
]
self.add_files(files)
class AddonGSMR(CardProfileAddon):
"""An Addon that can be found on either classic GSM SIM or on UICC to support GSM-R."""
def __init__(self):
files = [
DF_EIRENE()
]
super().__init__('GSM-R', desc='Railway GSM', files_in_mf=files)
def probe(self, card: 'CardBase') -> bool:
return card.file_exists(self.files_in_mf[0].fid)

View File

@@ -23,9 +23,9 @@ telecom-related protocol traces over UDP.
#
import socket
from typing import List, Dict, Optional
from construct import Optional as COptional
from construct import *
from construct import Int8ub, Int8sb, Int32ub, BitStruct, Enum, GreedyBytes, Struct, Switch
from construct import this, PaddedString
from pySim.construct import *
# The root definition of GSMTAP can be found at

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from construct import *
from construct import GreedyBytes, GreedyString
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *

View File

@@ -1,8 +1,3 @@
# coding=utf-8
import json
import pprint
import jsonpath_ng
"""JSONpath utility functions as needed within pysim.
As pySim-sell has the ability to represent SIM files as JSON strings,
@@ -10,6 +5,8 @@ adding JSONpath allows us to conveniently modify individual sub-fields
of a file or record in its JSON representation.
"""
import jsonpath_ng
# (C) 2021 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify

0
pySim/legacy/__init__.py Normal file
View File

1664
pySim/legacy/cards.py Normal file

File diff suppressed because it is too large Load Diff

134
pySim/legacy/ts_31_102.py Normal file
View File

@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
"""
Various constants from 3GPP TS 31.102 V17.9.0 usd by *legacy* code
"""
#
# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
# Copyright (C) 2021-2023 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/>.
EF_USIM_ADF_map = {
'LI': '6F05',
'ARR': '6F06',
'IMSI': '6F07',
'Keys': '6F08',
'KeysPS': '6F09',
'DCK': '6F2C',
'HPPLMN': '6F31',
'CNL': '6F32',
'ACMmax': '6F37',
'UST': '6F38',
'ACM': '6F39',
'FDN': '6F3B',
'SMS': '6F3C',
'GID1': '6F3E',
'GID2': '6F3F',
'MSISDN': '6F40',
'PUCT': '6F41',
'SMSP': '6F42',
'SMSS': '6F42',
'CBMI': '6F45',
'SPN': '6F46',
'SMSR': '6F47',
'CBMID': '6F48',
'SDN': '6F49',
'EXT2': '6F4B',
'EXT3': '6F4C',
'BDN': '6F4D',
'EXT5': '6F4E',
'CCP2': '6F4F',
'CBMIR': '6F50',
'EXT4': '6F55',
'EST': '6F56',
'ACL': '6F57',
'CMI': '6F58',
'START-HFN': '6F5B',
'THRESHOLD': '6F5C',
'PLMNwAcT': '6F60',
'OPLMNwAcT': '6F61',
'HPLMNwAcT': '6F62',
'PSLOCI': '6F73',
'ACC': '6F78',
'FPLMN': '6F7B',
'LOCI': '6F7E',
'ICI': '6F80',
'OCI': '6F81',
'ICT': '6F82',
'OCT': '6F83',
'AD': '6FAD',
'VGCS': '6FB1',
'VGCSS': '6FB2',
'VBS': '6FB3',
'VBSS': '6FB4',
'eMLPP': '6FB5',
'AAeM': '6FB6',
'ECC': '6FB7',
'Hiddenkey': '6FC3',
'NETPAR': '6FC4',
'PNN': '6FC5',
'OPL': '6FC6',
'MBDN': '6FC7',
'EXT6': '6FC8',
'MBI': '6FC9',
'MWIS': '6FCA',
'CFIS': '6FCB',
'EXT7': '6FCC',
'SPDI': '6FCD',
'MMSN': '6FCE',
'EXT8': '6FCF',
'MMSICP': '6FD0',
'MMSUP': '6FD1',
'MMSUCP': '6FD2',
'NIA': '6FD3',
'VGCSCA': '6FD4',
'VBSCA': '6FD5',
'GBAP': '6FD6',
'MSK': '6FD7',
'MUK': '6FD8',
'EHPLMN': '6FD9',
'GBANL': '6FDA',
'EHPLMNPI': '6FDB',
'LRPLMNSI': '6FDC',
'NAFKCA': '6FDD',
'SPNI': '6FDE',
'PNNI': '6FDF',
'NCP-IP': '6FE2',
'EPSLOCI': '6FE3',
'EPSNSC': '6FE4',
'UFC': '6FE6',
'UICCIARI': '6FE7',
'NASCONFIG': '6FE8',
'PWC': '6FEC',
'FDNURI': '6FED',
'BDNURI': '6FEE',
'SDNURI': '6FEF',
'IWL': '6FF0',
'IPS': '6FF1',
'IPD': '6FF2',
'ePDGId': '6FF3',
'ePDGSelection': '6FF4',
'ePDGIdEm': '6FF5',
'ePDGSelectionEm': '6FF6',
}
LOCI_STATUS_map = {
0: 'updated',
1: 'not updated',
2: 'plmn not allowed',
3: 'locatation area not allowed'
}

43
pySim/legacy/ts_31_103.py Normal file
View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
Various constants from 3GPP TS 31.103 V16.1.0 used by *legacy* code only
"""
# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
# Copyright (C) 2021 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/>.
EF_ISIM_ADF_map = {
'IST': '6F07',
'IMPI': '6F02',
'DOMAIN': '6F03',
'IMPU': '6F04',
'AD': '6FAD',
'ARR': '6F06',
'PCSCF': '6F09',
'GBAP': '6FD5',
'GBANL': '6FD7',
'NAFKCA': '6FDD',
'UICCIARI': '6FE7',
'SMS': '6F3C',
'SMSS': '6F43',
'SMSR': '6F47',
'SMSP': '6F42',
'FromPreferred': '6FF7',
'IMSConfigData': '6FF8',
'XCAPConfigData': '6FFC',
'WebRTCURI': '6FFA'
}

257
pySim/legacy/ts_51_011.py Normal file
View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
""" Various constants from 3GPP TS 51.011 used by *legacy* code only.
This will likely be removed in future versions of pySim. Please instead
use the pySim.filesystem class model with the various profile/application
specific extension modules.
"""
# Copyright (C) 2017 Alexander.Chemeris <Alexander.Chemeris@gmail.com>
# Copyright (C) 2021 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/>.
MF_num = '3F00'
DF_num = {
'TELECOM': '7F10',
'GSM': '7F20',
'IS-41': '7F22',
'FP-CTS': '7F23',
'GRAPHICS': '5F50',
'IRIDIUM': '5F30',
'GLOBST': '5F31',
'ICO': '5F32',
'ACeS': '5F33',
'EIA/TIA-553': '5F40',
'CTS': '5F60',
'SOLSA': '5F70',
'MExE': '5F3C',
}
EF_num = {
# MF
'ICCID': '2FE2',
'ELP': '2F05',
'DIR': '2F00',
# DF_TELECOM
'ADN': '6F3A',
'FDN': '6F3B',
'SMS': '6F3C',
'CCP': '6F3D',
'MSISDN': '6F40',
'SMSP': '6F42',
'SMSS': '6F43',
'LND': '6F44',
'SMSR': '6F47',
'SDN': '6F49',
'EXT1': '6F4A',
'EXT2': '6F4B',
'EXT3': '6F4C',
'BDN': '6F4D',
'EXT4': '6F4E',
'CMI': '6F58',
'ECCP': '6F4F',
# DF_GRAPHICS
'IMG': '4F20',
# DF_SoLSA
'SAI': '4F30',
'SLL': '4F31',
# DF_MExE
'MExE-ST': '4F40',
'ORPK': '4F41',
'ARPK': '4F42',
'TPRPK': '4F43',
# DF_GSM
'LP': '6F05',
'IMSI': '6F07',
'Kc': '6F20',
'DCK': '6F2C',
'PLMNsel': '6F30',
'HPPLMN': '6F31',
'CNL': '6F32',
'ACMmax': '6F37',
'SST': '6F38',
'ACM': '6F39',
'GID1': '6F3E',
'GID2': '6F3F',
'PUCT': '6F41',
'CBMI': '6F45',
'SPN': '6F46',
'CBMID': '6F48',
'BCCH': '6F74',
'ACC': '6F78',
'FPLMN': '6F7B',
'LOCI': '6F7E',
'AD': '6FAD',
'PHASE': '6FAE',
'VGCS': '6FB1',
'VGCSS': '6FB2',
'VBS': '6FB3',
'VBSS': '6FB4',
'eMLPP': '6FB5',
'AAeM': '6FB6',
'ECC': '6FB7',
'CBMIR': '6F50',
'NIA': '6F51',
'KcGPRS': '6F52',
'LOCIGPRS': '6F53',
'SUME': '6F54',
'PLMNwAcT': '6F60',
'OPLMNwAcT': '6F61',
# Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
'HPLMNAcT': '6F62',
'HPLMNwAcT': '6F62',
'CPBCCH': '6F63',
'INVSCAN': '6F64',
'PNN': '6FC5',
'OPL': '6FC6',
'MBDN': '6FC7',
'EXT6': '6FC8',
'MBI': '6FC9',
'MWIS': '6FCA',
'CFIS': '6FCB',
'EXT7': '6FCC',
'SPDI': '6FCD',
'MMSN': '6FCE',
'EXT8': '6FCF',
'MMSICP': '6FD0',
'MMSUP': '6FD1',
'MMSUCP': '6FD2',
}
DF = {
'TELECOM': [MF_num, DF_num['TELECOM']],
'GSM': [MF_num, DF_num['GSM']],
'IS-41': [MF_num, DF_num['IS-41']],
'FP-CTS': [MF_num, DF_num['FP-CTS']],
'GRAPHICS': [MF_num, DF_num['GRAPHICS']],
'IRIDIUM': [MF_num, DF_num['IRIDIUM']],
'GLOBST': [MF_num, DF_num['GLOBST']],
'ICO': [MF_num, DF_num['ICO']],
'ACeS': [MF_num, DF_num['ACeS']],
'EIA/TIA-553': [MF_num, DF_num['EIA/TIA-553']],
'CTS': [MF_num, DF_num['CTS']],
'SoLSA': [MF_num, DF_num['SOLSA']],
'MExE': [MF_num, DF_num['MExE']],
}
EF = {
'ICCID': [MF_num, EF_num['ICCID']],
'ELP': [MF_num, EF_num['ELP']],
'DIR': [MF_num, EF_num['DIR']],
'ADN': DF['TELECOM']+[EF_num['ADN']],
'FDN': DF['TELECOM']+[EF_num['FDN']],
'SMS': DF['TELECOM']+[EF_num['SMS']],
'CCP': DF['TELECOM']+[EF_num['CCP']],
'MSISDN': DF['TELECOM']+[EF_num['MSISDN']],
'SMSP': DF['TELECOM']+[EF_num['SMSP']],
'SMSS': DF['TELECOM']+[EF_num['SMSS']],
'LND': DF['TELECOM']+[EF_num['LND']],
'SMSR': DF['TELECOM']+[EF_num['SMSR']],
'SDN': DF['TELECOM']+[EF_num['SDN']],
'EXT1': DF['TELECOM']+[EF_num['EXT1']],
'EXT2': DF['TELECOM']+[EF_num['EXT2']],
'EXT3': DF['TELECOM']+[EF_num['EXT3']],
'BDN': DF['TELECOM']+[EF_num['BDN']],
'EXT4': DF['TELECOM']+[EF_num['EXT4']],
'CMI': DF['TELECOM']+[EF_num['CMI']],
'ECCP': DF['TELECOM']+[EF_num['ECCP']],
'IMG': DF['GRAPHICS']+[EF_num['IMG']],
'SAI': DF['SoLSA']+[EF_num['SAI']],
'SLL': DF['SoLSA']+[EF_num['SLL']],
'MExE-ST': DF['MExE']+[EF_num['MExE-ST']],
'ORPK': DF['MExE']+[EF_num['ORPK']],
'ARPK': DF['MExE']+[EF_num['ARPK']],
'TPRPK': DF['MExE']+[EF_num['TPRPK']],
'LP': DF['GSM']+[EF_num['LP']],
'IMSI': DF['GSM']+[EF_num['IMSI']],
'Kc': DF['GSM']+[EF_num['Kc']],
'DCK': DF['GSM']+[EF_num['DCK']],
'PLMNsel': DF['GSM']+[EF_num['PLMNsel']],
'HPPLMN': DF['GSM']+[EF_num['HPPLMN']],
'CNL': DF['GSM']+[EF_num['CNL']],
'ACMmax': DF['GSM']+[EF_num['ACMmax']],
'SST': DF['GSM']+[EF_num['SST']],
'ACM': DF['GSM']+[EF_num['ACM']],
'GID1': DF['GSM']+[EF_num['GID1']],
'GID2': DF['GSM']+[EF_num['GID2']],
'PUCT': DF['GSM']+[EF_num['PUCT']],
'CBMI': DF['GSM']+[EF_num['CBMI']],
'SPN': DF['GSM']+[EF_num['SPN']],
'CBMID': DF['GSM']+[EF_num['CBMID']],
'BCCH': DF['GSM']+[EF_num['BCCH']],
'ACC': DF['GSM']+[EF_num['ACC']],
'FPLMN': DF['GSM']+[EF_num['FPLMN']],
'LOCI': DF['GSM']+[EF_num['LOCI']],
'AD': DF['GSM']+[EF_num['AD']],
'PHASE': DF['GSM']+[EF_num['PHASE']],
'VGCS': DF['GSM']+[EF_num['VGCS']],
'VGCSS': DF['GSM']+[EF_num['VGCSS']],
'VBS': DF['GSM']+[EF_num['VBS']],
'VBSS': DF['GSM']+[EF_num['VBSS']],
'eMLPP': DF['GSM']+[EF_num['eMLPP']],
'AAeM': DF['GSM']+[EF_num['AAeM']],
'ECC': DF['GSM']+[EF_num['ECC']],
'CBMIR': DF['GSM']+[EF_num['CBMIR']],
'NIA': DF['GSM']+[EF_num['NIA']],
'KcGPRS': DF['GSM']+[EF_num['KcGPRS']],
'LOCIGPRS': DF['GSM']+[EF_num['LOCIGPRS']],
'SUME': DF['GSM']+[EF_num['SUME']],
'PLMNwAcT': DF['GSM']+[EF_num['PLMNwAcT']],
'OPLMNwAcT': DF['GSM']+[EF_num['OPLMNwAcT']],
# Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
'HPLMNAcT': DF['GSM']+[EF_num['HPLMNAcT']],
'HPLMNwAcT': DF['GSM']+[EF_num['HPLMNAcT']],
'CPBCCH': DF['GSM']+[EF_num['CPBCCH']],
'INVSCAN': DF['GSM']+[EF_num['INVSCAN']],
'PNN': DF['GSM']+[EF_num['PNN']],
'OPL': DF['GSM']+[EF_num['OPL']],
'MBDN': DF['GSM']+[EF_num['MBDN']],
'EXT6': DF['GSM']+[EF_num['EXT6']],
'MBI': DF['GSM']+[EF_num['MBI']],
'MWIS': DF['GSM']+[EF_num['MWIS']],
'CFIS': DF['GSM']+[EF_num['CFIS']],
'EXT7': DF['GSM']+[EF_num['EXT7']],
'SPDI': DF['GSM']+[EF_num['SPDI']],
'MMSN': DF['GSM']+[EF_num['MMSN']],
'EXT8': DF['GSM']+[EF_num['EXT8']],
'MMSICP': DF['GSM']+[EF_num['MMSICP']],
'MMSUP': DF['GSM']+[EF_num['MMSUP']],
'MMSUCP': DF['GSM']+[EF_num['MMSUCP']],
}

332
pySim/legacy/utils.py Normal file
View File

@@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
""" pySim: various utilities only used by legacy tools (pySim-{prog,read})
"""
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2021 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/>.
#
from pySim.utils import Hexstr, rpad, enc_plmn, h2i, i2s, s2h
from pySim.utils import dec_xplmn_w_act, dec_xplmn, dec_mcc_from_plmn, dec_mnc_from_plmn
def hexstr_to_Nbytearr(s, nbytes):
return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
def format_xplmn_w_act(hexstr):
s = ""
for rec_data in hexstr_to_Nbytearr(hexstr, 5):
rec_info = dec_xplmn_w_act(rec_data)
if rec_info['mcc'] == "" and rec_info['mnc'] == "":
rec_str = "unused"
else:
rec_str = "MCC: %s MNC: %s AcT: %s" % (
rec_info['mcc'], rec_info['mnc'], ", ".join(rec_info['act']))
s += "\t%s # %s\n" % (rec_data, rec_str)
return s
def format_xplmn(hexstr: Hexstr) -> str:
s = ""
for rec_data in hexstr_to_Nbytearr(hexstr, 3):
rec_info = dec_xplmn(rec_data)
if not rec_info['mcc'] and not rec_info['mnc']:
rec_str = "unused"
else:
rec_str = "MCC: %s MNC: %s" % (rec_info['mcc'], rec_info['mnc'])
s += "\t%s # %s\n" % (rec_data, rec_str)
return s
def format_ePDGSelection(hexstr):
ePDGSelection_info_tag_chars = 2
ePDGSelection_info_tag_str = hexstr[:2]
s = ""
# Minimum length
len_chars = 2
# TODO: Need to determine length properly - definite length support only
# Inconsistency in spec: 3GPP TS 31.102 version 15.2.0 Release 15, 4.2.104
# As per spec, length is 5n, n - number of PLMNs
# But, each PLMN entry is made of PLMN (3 Bytes) + ePDG Priority (2 Bytes) + ePDG FQDN format (1 Byte)
# Totalling to 6 Bytes, maybe length should be 6n
len_str = hexstr[ePDGSelection_info_tag_chars:ePDGSelection_info_tag_chars+len_chars]
# Not programmed scenario
if int(len_str, 16) == 255 or int(ePDGSelection_info_tag_str, 16) == 255:
len_chars = 0
ePDGSelection_info_tag_chars = 0
if len_str[0] == '8':
# The bits 7 to 1 denotes the number of length octets if length > 127
if int(len_str[1]) > 0:
# Update number of length octets
len_chars = len_chars * int(len_str[1])
len_str = hexstr[ePDGSelection_info_tag_chars:len_chars]
content_str = hexstr[ePDGSelection_info_tag_chars+len_chars:]
# Right pad to prevent index out of range - multiple of 6 bytes
content_str = rpad(content_str, len(content_str) +
(12 - (len(content_str) % 12)))
for rec_data in hexstr_to_Nbytearr(content_str, 6):
rec_info = dec_ePDGSelection(rec_data)
if rec_info['mcc'] == 0xFFF and rec_info['mnc'] == 0xFFF:
rec_str = "unused"
else:
rec_str = "MCC: %03d MNC: %03d ePDG Priority: %s ePDG FQDN format: %s" % \
(rec_info['mcc'], rec_info['mnc'],
rec_info['epdg_priority'], rec_info['epdg_fqdn_format'])
s += "\t%s # %s\n" % (rec_data, rec_str)
return s
def enc_st(st, service, state=1):
"""
Encodes the EF S/U/IST/EST and returns the updated Service Table
Parameters:
st - Current value of SIM/USIM/ISIM Service Table
service - Service Number to encode as activated/de-activated
state - 1 mean activate, 0 means de-activate
Returns:
s - Modified value of SIM/USIM/ISIM Service Table
Default values:
- state: 1 - Sets the particular Service bit to 1
"""
st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
s = ""
# Check whether the requested service is present in each byte
for i in range(0, len(st_bytes)):
# Byte i contains info about Services num (8i+1) to num (8i+8)
if service in range((8*i) + 1, (8*i) + 9):
byte = int(st_bytes[i], 16)
# Services in each byte are in order MSB to LSB
# MSB - Service (8i+8)
# LSB - Service (8i+1)
mod_byte = 0x00
# Copy bit by bit contents of byte to mod_byte with modified bit
# for requested service
for j in range(1, 9):
mod_byte = mod_byte >> 1
if service == (8*i) + j:
mod_byte = state == 1 and mod_byte | 0x80 or mod_byte & 0x7f
else:
mod_byte = byte & 0x01 == 0x01 and mod_byte | 0x80 or mod_byte & 0x7f
byte = byte >> 1
s += ('%02x' % (mod_byte))
else:
s += st_bytes[i]
return s
def dec_st(st, table="sim") -> str:
"""
Parses the EF S/U/IST and prints the list of available services in EF S/U/IST
"""
if table == "isim":
from pySim.ts_31_103 import EF_IST_map
lookup_map = EF_IST_map
elif table == "usim":
from pySim.ts_31_102 import EF_UST_map
lookup_map = EF_UST_map
else:
from pySim.ts_51_011 import EF_SST_map
lookup_map = EF_SST_map
st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
avail_st = ""
# Get each byte and check for available services
for i in range(0, len(st_bytes)):
# Byte i contains info about Services num (8i+1) to num (8i+8)
byte = int(st_bytes[i], 16)
# Services in each byte are in order MSB to LSB
# MSB - Service (8i+8)
# LSB - Service (8i+1)
for j in range(1, 9):
if byte & 0x01 == 0x01 and ((8*i) + j in lookup_map):
# Byte X contains info about Services num (8X-7) to num (8X)
# bit = 1: service available
# bit = 0: service not available
avail_st += '\tService %d - %s\n' % (
(8*i) + j, lookup_map[(8*i) + j])
byte = byte >> 1
return avail_st
def enc_ePDGSelection(hexstr, mcc, mnc, epdg_priority='0001', epdg_fqdn_format='00'):
"""
Encode ePDGSelection so it can be stored at EF.ePDGSelection or EF.ePDGSelectionEm.
See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
Default values:
- epdg_priority: '0001' - 1st Priority
- epdg_fqdn_format: '00' - Operator Identifier FQDN
"""
plmn1 = enc_plmn(mcc, mnc) + epdg_priority + epdg_fqdn_format
# TODO: Handle encoding of Length field for length more than 127 Bytes
content = '80' + ('%02x' % (len(plmn1)//2)) + plmn1
content = rpad(content, len(hexstr))
return content
def dec_ePDGSelection(sixhexbytes):
"""
Decode ePDGSelection to get EF.ePDGSelection or EF.ePDGSelectionEm.
See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
"""
res = {'mcc': 0, 'mnc': 0, 'epdg_priority': 0, 'epdg_fqdn_format': ''}
plmn_chars = 6
epdg_priority_chars = 4
epdg_fqdn_format_chars = 2
# first three bytes (six ascii hex chars)
plmn_str = sixhexbytes[:plmn_chars]
# two bytes after first three bytes
epdg_priority_str = sixhexbytes[plmn_chars:plmn_chars +
epdg_priority_chars]
# one byte after first five bytes
epdg_fqdn_format_str = sixhexbytes[plmn_chars +
epdg_priority_chars:plmn_chars + epdg_priority_chars + epdg_fqdn_format_chars]
res['mcc'] = dec_mcc_from_plmn(plmn_str)
res['mnc'] = dec_mnc_from_plmn(plmn_str)
res['epdg_priority'] = epdg_priority_str
res['epdg_fqdn_format'] = epdg_fqdn_format_str == '00' and 'Operator Identifier FQDN' or 'Location based FQDN'
return res
def first_TLV_parser(bytelist):
'''
first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205])
parses first TLV format record in a list of bytelist
returns a 3-Tuple: Tag, Length, Value
Value is a list of bytes
parsing of length is ETSI'style 101.220
'''
Tag = bytelist[0]
if bytelist[1] == 0xFF:
Len = bytelist[2]*256 + bytelist[3]
Val = bytelist[4:4+Len]
else:
Len = bytelist[1]
Val = bytelist[2:2+Len]
return (Tag, Len, Val)
def TLV_parser(bytelist):
'''
TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...]
loops on the input list of bytes with the "first_TLV_parser()" function
returns a list of 3-Tuples
'''
ret = []
while len(bytelist) > 0:
T, L, V = first_TLV_parser(bytelist)
if T == 0xFF:
# padding bytes
break
ret.append((T, L, V))
# need to manage length of L
if L > 0xFE:
bytelist = bytelist[L+4:]
else:
bytelist = bytelist[L+2:]
return ret
def dec_addr_tlv(hexstr):
"""
Decode hex string to get EF.P-CSCF Address or EF.ePDGId or EF.ePDGIdEm.
See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
"""
# Convert from hex str to int bytes list
addr_tlv_bytes = h2i(hexstr)
# Get list of tuples containing parsed TLVs
tlvs = TLV_parser(addr_tlv_bytes)
for tlv in tlvs:
# tlv = (T, L, [V])
# T = Tag
# L = Length
# [V] = List of value
# Invalid Tag value scenario
if tlv[0] != 0x80:
continue
# Empty field - Zero length
if tlv[1] == 0:
continue
# Uninitialized field
if all([v == 0xff for v in tlv[2]]):
continue
# First byte in the value has the address type
addr_type = tlv[2][0]
# TODO: Support parsing of IPv6
# Address Type: 0x00 (FQDN), 0x01 (IPv4), 0x02 (IPv6), other (Reserved)
if addr_type == 0x00: # FQDN
# Skip address tye byte i.e. first byte in value list
content = tlv[2][1:]
return (i2s(content), '00')
elif addr_type == 0x01: # IPv4
# Skip address tye byte i.e. first byte in value list
# Skip the unused byte in Octect 4 after address type byte as per 3GPP TS 31.102
ipv4 = tlv[2][2:]
content = '.'.join(str(x) for x in ipv4)
return (content, '01')
else:
raise ValueError("Invalid address type")
return (None, None)
def enc_addr_tlv(addr, addr_type='00'):
"""
Encode address TLV object used in EF.P-CSCF Address, EF.ePDGId and EF.ePDGIdEm.
See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
Default values:
- addr_type: 00 - FQDN format of Address
"""
s = ""
# TODO: Encoding of IPv6 address
if addr_type == '00': # FQDN
hex_str = s2h(addr)
s += '80' + ('%02x' % ((len(hex_str)//2)+1)) + '00' + hex_str
elif addr_type == '01': # IPv4
ipv4_list = addr.split('.')
ipv4_str = ""
for i in ipv4_list:
ipv4_str += ('%02x' % (int(i)))
# Unused bytes shall be set to 'ff'. i.e 4th Octet after Address Type is not used
# IPv4 Address is in octet 5 to octet 8 of the TLV data object
s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
return s

441
pySim/ota.py Normal file
View File

@@ -0,0 +1,441 @@
"""Code related to SIM/UICC OTA according to TS 102 225 + TS 31.115."""
# (C) 2021-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 zlib
import abc
import struct
from typing import Optional
from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
from construct import Flag, Padding, Switch, this
from pySim.construct import *
from pySim.utils import b2h
from pySim.sms import UserDataHeader
# ETS TS 102 225 gives the general command structure and the dialects for CAT_TP, TCP/IP and HTTPS
# 3GPP TS 31.115 gives the dialects for SMS-PP, SMS-CB, USSD and HTTP
# CPI CPL CHI CHL SPI KIc KID TAR CNTR PCNTR RC/CC/DS data
# CAT_TP TCP/IP SMS
# CPI 0x01 0x01 =IEIa=70,len=0
# CHI NULL NULL NULL
# CPI, CPL and CHL included in RC/CC/DS true true
# RPI 0x02 0x02 =IEIa=71,len=0
# RHI NULL NULL
# RPI, RPL and RHL included in RC/CC/DS true true
# packet-id 0-bf,ff 0-bf,ff
# identification packet false 102 225 tbl 6
# KVN 1..f; KI1=KIc, KI2=KID, KI3=DEK
# ETSI TS 102 225 Table 5 + 3GPP TS 31.115 Section 7
ResponseStatus = Enum(Int8ub, por_ok=0, rc_cc_ds_failed=1, cntr_low=2, cntr_high=3,
cntr_blocked=4, ciphering_error=5, undefined_security_error=6,
insufficient_memory=7, more_time_needed=8, tar_unknown=9,
insufficient_security_level=0x0A,
actual_response_sms_submit=0x0B,
actual_response_ussd=0x0C)
# ETSI TS 102 226 Section 5.1.2
CompactRemoteResp = Struct('number_of_commands'/Int8ub,
'last_status_word'/HexAdapter(Bytes(2)),
'last_response_data'/HexAdapter(GreedyBytes))
RC_CC_DS = Enum(BitsInteger(2), no_rc_cc_ds=0, rc=1, cc=2, ds=3)
# TS 102 225 Section 5.1.1 + TS 31.115 Section 4.2
SPI = BitStruct( # first octet
Padding(3),
'counter'/Enum(BitsInteger(2), no_counter=0, counter_no_replay_or_seq=1,
counter_must_be_higher=2, counter_must_be_lower=3),
'ciphering'/Flag,
'rc_cc_ds'/RC_CC_DS,
# second octet
Padding(2),
'por_in_submit'/Flag,
'por_shall_be_ciphered'/Flag,
'por_rc_cc_ds'/RC_CC_DS,
'por'/Enum(BitsInteger(2), no_por=0,
por_required=1, por_only_when_error=2)
)
# TS 102 225 Section 5.1.2
KIC = BitStruct('key'/BitsInteger(4),
'algo'/Enum(BitsInteger(4), implicit=0, single_des=1, triple_des_cbc2=5, triple_des_cbc3=9,
aes_cbc=2)
)
# TS 102 225 Section 5.1.3.1
KID_CC = BitStruct('key'/BitsInteger(4),
'algo'/Enum(BitsInteger(4), implicit=0, single_des=1, triple_des_cbc2=5, triple_des_cbc3=9,
aes_cmac=2)
)
# TS 102 225 Section 5.1.3.2
KID_RC = BitStruct('key'/BitsInteger(4),
'algo'/Enum(BitsInteger(4), implicit=0, crc16=1, crc32=5, proprietary=3)
)
SmsCommandPacket = Struct('cmd_pkt_len'/Int16ub,
'cmd_hdr_len'/Int8ub,
'spi'/SPI,
'kic'/KIC,
'kid'/Switch(this.spi.rc_cc_ds, {'cc': KID_CC, 'rc': KID_RC }),
'tar'/Bytes(3),
'secured_data'/GreedyBytes)
class OtaKeyset:
"""The OTA related data (key material, counter) to be used in encrypt/decrypt."""
def __init__(self, algo_crypt: str, kic_idx: int, kic: bytes,
algo_auth: str, kid_idx: int, kid: bytes, cntr: int = 0):
self.algo_crypt = algo_crypt
self.kic = bytes(kic)
self.kic_idx = kic_idx
self.algo_auth = algo_auth
self.kid = bytes(kid)
self.kid_idx = kid_idx
self.cntr = cntr
@property
def auth(self):
"""Return an instance of the matching OtaAlgoAuth."""
return OtaAlgoAuth.from_keyset(self)
@property
def crypt(self):
"""Return an instance of the matching OtaAlgoCrypt."""
return OtaAlgoCrypt.from_keyset(self)
class OtaCheckError(Exception):
pass
class OtaDialect(abc.ABC):
"""Base Class for OTA dialects such as SMS, BIP, ..."""
def _compute_sig_len(self, spi:SPI):
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
return 0
if spi['rc_cc_ds'] == 'rc': # CRC-32
return 4
if spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
# TODO: this is not entirely correct, as in AES case it could be 4 or 8
return 8
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
@abc.abstractmethod
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
pass
@abc.abstractmethod
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
"""Decode a response into a response packet and, if indicted (by a
response status of `"por_ok"`) a decoded response.
The response packet's common characteristics are not fully determined,
and (so far) completely proprietary per dialect."""
from Cryptodome.Cipher import DES, DES3, AES
from Cryptodome.Hash import CMAC
class OtaAlgo(abc.ABC):
iv = property(lambda self: bytes([0] * self.blocksize))
blocksize = None
enum_name = None
@staticmethod
def _get_padding(in_len: int, multiple: int, padding: int = 0):
"""Return padding bytes towards multiple of N."""
if in_len % multiple == 0:
return b''
pad_cnt = multiple - (in_len % multiple)
return b'\x00' * pad_cnt
@staticmethod
def _pad_to_multiple(indat: bytes, multiple: int, padding: int = 0):
"""Pad input bytes to multiple of N."""
return indat + OtaAlgo._get_padding(len(indat), multiple, padding)
def pad_to_blocksize(self, indat: bytes, padding: int = 0):
"""Pad the given input data to multiple of the cipher block size."""
return self._pad_to_multiple(indat, self.blocksize, padding)
def __init__(self, otak: OtaKeyset):
self.otak = otak
def __str__(self):
return self.__class__.__name__
class OtaAlgoCrypt(OtaAlgo, abc.ABC):
def __init__(self, otak: OtaKeyset):
if self.enum_name != otak.algo_crypt:
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_crypt))
super().__init__(otak)
def encrypt(self, data:bytes) -> bytes:
"""Encrypt given input bytes using the key material given in constructor."""
padded_data = self.pad_to_blocksize(data)
return self._encrypt(padded_data)
def decrypt(self, data:bytes) -> bytes:
"""Decrypt given input bytes using the key material given in constructor."""
return self._decrypt(data)
@abc.abstractmethod
def _encrypt(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
@abc.abstractmethod
def _decrypt(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
@classmethod
def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
"""Resolve the class for the encryption algorithm of otak and instantiate it."""
for subc in cls.__subclasses__():
if subc.enum_name == otak.algo_crypt:
return subc(otak)
raise ValueError('No implementation for crypt algorithm %s' % otak.algo_auth)
class OtaAlgoAuth(OtaAlgo, abc.ABC):
def __init__(self, otak: OtaKeyset):
if self.enum_name != otak.algo_auth:
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_crypt))
super().__init__(otak)
def sign(self, data:bytes) -> bytes:
"""Compute the CC/CR check bytes for the input data using key material
given in constructor."""
padded_data = self.pad_to_blocksize(data)
sig = self._sign(padded_data)
return sig
def check_sig(self, data:bytes, cc_received:bytes):
"""Compute the CC/CR check bytes for the input data and compare against cc_received."""
cc = self.sign(data)
if cc_received != cc:
raise OtaCheckError('Received CC (%s) != Computed CC (%s)' % (b2h(cc_received), b2h(cc)))
@abc.abstractmethod
def _sign(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
pass
@classmethod
def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
"""Resolve the class for the authentication algorithm of otak and instantiate it."""
for subc in cls.__subclasses__():
if subc.enum_name == otak.algo_auth:
return subc(otak)
raise ValueError('No implementation for auth algorithm %s' % otak.algo_auth)
class OtaAlgoCryptDES(OtaAlgoCrypt):
"""DES is insecure. For backwards compatibility with pre-Rel8"""
name = 'DES'
enum_name = 'single_des'
blocksize = 8
def _encrypt(self, data:bytes) -> bytes:
cipher = DES.new(self.otak.kic, DES.MODE_CBC, self.iv)
return cipher.encrypt(data)
def _decrypt(self, data:bytes) -> bytes:
cipher = DES.new(self.otak.kic, DES.MODE_CBC, self.iv)
return cipher.decrypt(data)
class OtaAlgoAuthDES(OtaAlgoAuth):
"""DES is insecure. For backwards compatibility with pre-Rel8"""
name = 'DES'
enum_name = 'single_des'
blocksize = 8
def _sign(self, data:bytes) -> bytes:
cipher = DES.new(self.otak.kid, DES.MODE_CBC, self.iv)
ciph = cipher.encrypt(data)
return ciph[len(ciph) - 8:]
class OtaAlgoCryptDES3(OtaAlgoCrypt):
name = '3DES'
enum_name = 'triple_des_cbc2'
blocksize = 8
def _encrypt(self, data:bytes) -> bytes:
cipher = DES3.new(self.otak.kic, DES3.MODE_CBC, self.iv)
return cipher.encrypt(data)
def _decrypt(self, data:bytes) -> bytes:
cipher = DES3.new(self.otak.kic, DES3.MODE_CBC, self.iv)
return cipher.decrypt(data)
class OtaAlgoAuthDES3(OtaAlgoAuth):
name = '3DES'
enum_name = 'triple_des_cbc2'
blocksize = 8
def _sign(self, data:bytes) -> bytes:
cipher = DES3.new(self.otak.kid, DES3.MODE_CBC, self.iv)
ciph = cipher.encrypt(data)
return ciph[len(ciph) - 8:]
class OtaAlgoCryptAES(OtaAlgoCrypt):
name = 'AES'
enum_name = 'aes_cbc'
blocksize = 16 # TODO: is this needed?
def _encrypt(self, data:bytes) -> bytes:
cipher = AES.new(self.otak.kic, AES.MODE_CBC, self.iv)
return cipher.encrypt(data)
def _decrypt(self, data:bytes) -> bytes:
cipher = AES.new(self.otak.kic, AES.MODE_CBC, self.iv)
return cipher.decrypt(data)
class OtaAlgoAuthAES(OtaAlgoAuth):
name = 'AES'
enum_name = 'aes_cmac'
blocksize = 1 # AES CMAC doesn't need any padding by us
def _sign(self, data:bytes) -> bytes:
cmac = CMAC.new(self.otak.kid, ciphermod=AES, mac_len=8)
cmac.update(data)
ciph = cmac.digest()
return ciph[len(ciph) - 8:]
class OtaDialectSms(OtaDialect):
"""OTA dialect for SMS based transport, as described in 3GPP TS 31.115."""
SmsResponsePacket = Struct('rpl'/Int16ub,
'rhl'/Int8ub,
'tar'/Bytes(3),
'cntr'/Bytes(5),
'pcntr'/Int8ub,
'response_status'/ResponseStatus,
'cc_rc'/Bytes(this.rhl-10),
'secured_data'/GreedyBytes)
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
# length of signature in octets
len_sig = self._compute_sig_len(spi)
pad_cnt = 0
if spi['ciphering']: # ciphering is requested
# append padding bytes to end up with blocksize
len_cipher = 6 + len_sig + len(apdu)
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
pad_cnt = len(padding)
apdu += padding
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
kid = {'key': otak.kid_idx, 'algo': otak.algo_auth}
# CHL = number of octets from (and including) SPI to the end of RC/CC/DS
# 13 == SPI(2) + KIc(1) + KId(1) + TAR(3) + CNTR(5) + PCNTR(1)
chl = 13 + len_sig
# CHL + SPI (+ KIC + KID)
c = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
part_head = c.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
#print("part_head: %s" % b2h(part_head))
# CNTR + PCNTR (CNTR not used)
part_cnt = otak.cntr.to_bytes(5, 'big') + pad_cnt.to_bytes(1, 'big')
#print("part_cnt: %s" % b2h(part_cnt))
envelope_data = part_head + part_cnt + apdu
#print("envelope_data: %s" % b2h(envelope_data))
# 2-byte CPL. CPL is part of RC/CC/CPI to end of secured data, including any padding for ciphering
# CPL from and including CPI to end of secured data, including any padding for ciphering
cpl = len(envelope_data) + len_sig
envelope_data = cpl.to_bytes(2, 'big') + envelope_data
#print("envelope_data with cpl: %s" % b2h(envelope_data))
if spi['rc_cc_ds'] == 'cc':
cc = otak.auth.sign(envelope_data)
envelope_data = part_cnt + cc + apdu
elif spi['rc_cc_ds'] == 'rc':
# CRC32
crc32 = zlib.crc32(envelope_data) & 0xffffffff
envelope_data = part_cnt + crc32.to_bytes(4, 'big') + apdu
elif spi['rc_cc_ds'] == 'no_rc_cc_ds':
envelope_data = part_cnt + apdu
else:
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
#print("envelope_data with sig: %s" % b2h(envelope_data))
# encrypt as needed
if spi['ciphering']: # ciphering is requested
ciph = otak.crypt.encrypt(envelope_data)
envelope_data = part_head + ciph
# prefix with another CPL
cpl = len(envelope_data)
envelope_data = cpl.to_bytes(2, 'big') + envelope_data
else:
envelope_data = part_head + envelope_data
#print("envelope_data: %s" % b2h(envelope_data))
return envelope_data
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
if isinstance(data, str):
data = h2b(data)
# plain-text POR: 027100000e0ab000110000000000000001612f
# UDHL RPI IEDLa RPL RHL TAR CNTR PCNTR STS
# 02 71 00 000e 0a b00011 0000000000 00 00 01 612f
# POR with CC: 027100001612b000110000000000000055f47118381175fb01612f
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
if data[0] != 0x02:
raise ValueError('Unexpected UDL=0x%02x' % data[0])
udhd, remainder = UserDataHeader.from_bytes(data)
if not udhd.has_ie(0x71):
raise ValueError('RPI 0x71 not found in UDH')
rph_rhl_tar = remainder[:6] # RPH+RHL+TAR; not ciphered
res = self.SmsResponsePacket.parse(remainder)
if spi['por_shall_be_ciphered']:
# decrypt
ciphered_part = remainder[6:]
deciph = otak.crypt.decrypt(ciphered_part)
temp_data = rph_rhl_tar + deciph
res = self.SmsResponsePacket.parse(temp_data)
# remove specified number of padding bytes, if any
if res['pcntr'] != 0:
# this conditional is needed as python [:-0] renders an empty return!
res['secured_data'] = res['secured_data'][:-res['pcntr']]
remainder = temp_data
# is there a CC/RC present?
len_sig = res['rhl'] - 10
if spi['por_rc_cc_ds'] == 'no_rc_cc_ds':
if len_sig:
raise OtaCheckError('No RC/CC/DS requested, but len_sig=%u' % len_sig)
elif spi['por_rc_cc_ds'] == 'cc':
# verify signature
# UDH is part of CC/RC!
udh = data[:3]
# RPL, RHL, TAR, CNTR, PCNTR and STSare part of CC/RC
rpl_rhl_tar_cntr_pcntr_sts = remainder[:13]
# remove the CC/RC bytes
temp_data = udh + rpl_rhl_tar_cntr_pcntr_sts + remainder[13+len_sig:]
otak.auth.check_sig(temp_data, res['cc_rc'])
# TODO: CRC
else:
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
if res.response_status == 'por_ok':
dec = CompactRemoteResp.parse(res['secured_data'])
else:
dec = None
return (res, dec)

View File

@@ -21,14 +21,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import abc
import operator
from typing import List
from pySim.commands import SimCardCommands
from pySim.filesystem import CardApplication, interpret_sw
from pySim.utils import all_subclasses
import abc
import operator
def _mf_select_test(scc: SimCardCommands, cla_byte: str, sel_ctrl: str) -> bool:
def _mf_select_test(scc: SimCardCommands,
cla_byte: str, sel_ctrl: str,
fids: List[str]) -> bool:
cla_byte_bak = scc.cla_byte
sel_ctrl_bak = scc.sel_ctrl
scc.reset_card()
@@ -37,7 +40,8 @@ def _mf_select_test(scc: SimCardCommands, cla_byte: str, sel_ctrl: str) -> bool:
scc.sel_ctrl = sel_ctrl
rc = True
try:
scc.select_file('3f00')
for fid in fids:
scc.select_file(fid)
except:
rc = False
@@ -51,7 +55,7 @@ def match_uicc(scc: SimCardCommands) -> bool:
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
card is considered a UICC card.
"""
return _mf_select_test(scc, "00", "0004")
return _mf_select_test(scc, "00", "0004", ["3f00"])
def match_sim(scc: SimCardCommands) -> bool:
@@ -59,7 +63,14 @@ def match_sim(scc: SimCardCommands) -> bool:
is also a simcard. This will be the case for most UICC cards, but there may
also be plain UICC cards without 2G support as well.
"""
return _mf_select_test(scc, "a0", "0000")
return _mf_select_test(scc, "a0", "0000", ["3f00"])
def match_ruim(scc: SimCardCommands) -> bool:
""" Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
the card is considered an R-UIM card for CDMA.
"""
return _mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
class CardProfile:
@@ -77,6 +88,7 @@ class CardProfile:
shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
cla : class byte that should be used with cards of this profile
sel_ctrl : selection control bytes class byte that should be used with cards of this profile
addons: List of optional CardAddons that a card of this profile might have
"""
self.name = name
self.desc = kw.get("desc", None)
@@ -86,6 +98,8 @@ class CardProfile:
self.shell_cmdsets = kw.get("shell_cmdsets", [])
self.cla = kw.get("cla", "00")
self.sel_ctrl = kw.get("sel_ctrl", "0004")
# list of optional addons that a card of this profile might have
self.addons = kw.get("addons", [])
def __str__(self):
return self.name
@@ -150,3 +164,32 @@ class CardProfile:
return p()
return None
def add_addon(self, addon: 'CardProfileAddon'):
assert addon not in self.addons
# we don't install any additional files, as that is happening in the RuntimeState.
self.addons.append(addon)
class CardProfileAddon(abc.ABC):
"""A Card Profile Add-on is something that is not a card application or a full stand-alone
card profile, but an add-on to an existing profile. Think of GSM-R specific files existing
on what is otherwise a SIM or USIM+SIM card."""
def __init__(self, name: str, **kw):
"""
Args:
desc (str) : Description
files_in_mf : List of CardEF instances present in MF
shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
"""
self.name = name
self.desc = kw.get("desc", None)
self.files_in_mf = kw.get("files_in_mf", [])
self.shell_cmdsets = kw.get("shell_cmdsets", [])
def __str__(self):
return self.name
@abc.abstractmethod
def probe(self, card: 'CardBase') -> bool:
"""Probe a given card to determine whether or not this add-on is present/supported."""

545
pySim/runtime.py Normal file
View File

@@ -0,0 +1,545 @@
# coding=utf-8
"""Representation of the runtime state of an application like pySim-shell.
"""
# (C) 2021 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/>.
from typing import Optional, Tuple
from pySim.utils import h2b, i2h, is_hex, bertlv_parse_one, Hexstr
from pySim.exceptions import *
from pySim.filesystem import *
def lchan_nr_from_cla(cla: int) -> int:
"""Resolve the logical channel number from the CLA byte."""
# TS 102 221 10.1.1 Coding of Class Byte
if cla >> 4 in [0x0, 0xA, 0x8]:
# Table 10.3
return cla & 0x03
if cla & 0xD0 in [0x40, 0xC0]:
# Table 10.4a
return 4 + (cla & 0x0F)
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
class RuntimeState:
"""Represent the runtime state of a session with a card."""
def __init__(self, card: 'CardBase', profile: 'CardProfile'):
"""
Args:
card : pysim.cards.Card instance
profile : CardProfile instance
"""
self.mf = CardMF(profile=profile)
self.card = card
self.profile = profile
self.lchan = {}
# the basic logical channel always exists
self.lchan[0] = RuntimeLchan(0, self)
# make sure the class and selection control bytes, which are specified
# by the card profile are used
self.card.set_apdu_parameter(
cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
for addon_cls in self.profile.addons:
addon = addon_cls()
if addon.probe(self.card):
print("Detected %s Add-on \"%s\"" % (self.profile, addon))
for f in addon.files_in_mf:
self.mf.add_file(f)
# go back to MF before the next steps (addon probing might have changed DF)
self.lchan[0].select('MF')
# add application ADFs + MF-files from profile
apps = self._match_applications()
for a in apps:
if a.adf:
self.mf.add_application_df(a.adf)
for f in self.profile.files_in_mf:
self.mf.add_file(f)
self.conserve_write = True
# make sure that when the runtime state is created, the card is also
# in a defined state.
self.reset()
def _match_applications(self):
"""match the applications from the profile with applications on the card"""
apps_profile = self.profile.applications
# When the profile does not feature any applications, then we are done already
if not apps_profile:
return []
# Read AIDs from card and match them against the applications defined by the
# card profile
aids_card = self.card.read_aids()
apps_taken = []
if aids_card:
aids_taken = []
print("AIDs on card:")
for a in aids_card:
for f in apps_profile:
if f.aid in a:
print(" %s: %s (EF.DIR)" % (f.name, a))
aids_taken.append(a)
apps_taken.append(f)
aids_unknown = set(aids_card) - set(aids_taken)
for a in aids_unknown:
print(" unknown: %s (EF.DIR)" % a)
else:
print("warning: EF.DIR seems to be empty!")
# Some card applications may not be registered in EF.DIR, we will actively
# probe for those applications
for f in sorted(set(apps_profile) - set(apps_taken), key=str):
try:
# we can not use the lchan provided methods select, or select_file
# since those method work on an already finished file model. At
# this point we are still in the initialization process, so it is
# no problem when we access the card object directly without caring
# about updating other states. For normal selects at runtime, the
# caller must use the lchan provided methods select or select_file!
_data, sw = self.card.select_adf_by_aid(f.aid)
self.selected_adf = f
if sw == "9000":
print(" %s: %s" % (f.name, f.aid))
apps_taken.append(f)
except (SwMatchError, ProtocolError):
pass
return apps_taken
def reset(self, cmd_app=None) -> Hexstr:
"""Perform physical card reset and obtain ATR.
Args:
cmd_app : Command Application State (for unregistering old file commands)
"""
# delete all lchan != 0 (basic lchan)
for lchan_nr in list(self.lchan.keys()):
if lchan_nr == 0:
continue
del self.lchan[lchan_nr]
atr = i2h(self.card.reset())
# select MF to reset internal state and to verify card really works
self.lchan[0].select('MF', cmd_app)
self.lchan[0].selected_adf = None
return atr
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
"""Add a logical channel to the runtime state. You shouldn't call this
directly but always go through RuntimeLchan.add_lchan()."""
if lchan_nr in self.lchan.keys():
raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
return self.lchan[lchan_nr]
def del_lchan(self, lchan_nr: int):
if lchan_nr in self.lchan.keys():
del self.lchan[lchan_nr]
return True
else:
return False
def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
lchan_nr = lchan_nr_from_cla(cla)
if lchan_nr in self.lchan.keys():
return self.lchan[lchan_nr]
else:
return None
class RuntimeLchan:
"""Represent the runtime state of a logical channel with a card."""
def __init__(self, lchan_nr: int, rs: RuntimeState):
self.lchan_nr = lchan_nr
self.rs = rs
self.scc = self.rs.card._scc.fork_lchan(lchan_nr)
# File reference data
self.selected_file = self.rs.mf
self.selected_adf = None
self.selected_file_fcp = None
self.selected_file_fcp_hex = None
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
"""Add a new logical channel from the current logical channel. Just affects
internal state, doesn't actually open a channel with the UICC."""
new_lchan = self.rs.add_lchan(lchan_nr)
# See TS 102 221 Table 8.3
if self.lchan_nr != 0:
new_lchan.selected_file = self.get_cwd()
new_lchan.selected_adf = self.selected_adf
return new_lchan
def selected_file_descriptor_byte(self) -> dict:
return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
def selected_file_shareable(self) -> bool:
return self.selected_file_descriptor_byte()['shareable']
def selected_file_structure(self) -> str:
return self.selected_file_descriptor_byte()['structure']
def selected_file_type(self) -> str:
return self.selected_file_descriptor_byte()['file_type']
def selected_file_num_of_rec(self) -> Optional[int]:
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
def get_cwd(self) -> CardDF:
"""Obtain the current working directory.
Returns:
CardDF instance
"""
if isinstance(self.selected_file, CardDF):
return self.selected_file
else:
return self.selected_file.parent
def get_application_df(self) -> Optional[CardADF]:
"""Obtain the currently selected application DF (if any).
Returns:
CardADF() instance or None"""
# iterate upwards from selected file; check if any is an ADF
node = self.selected_file
while node.parent != node:
if isinstance(node, CardADF):
return node
node = node.parent
return None
def interpret_sw(self, sw: str):
"""Interpret a given status word relative to the currently selected application
or the underlying card profile.
Args:
sw : Status word as string of 4 hex digits
Returns:
Tuple of two strings
"""
res = None
adf = self.get_application_df()
if adf:
app = adf.application
# The application either comes with its own interpret_sw
# method or we will use the interpret_sw method from the
# card profile.
if app and hasattr(app, "interpret_sw"):
res = app.interpret_sw(sw)
return res or self.rs.profile.interpret_sw(sw)
def probe_file(self, fid: str, cmd_app=None):
"""Blindly try to select a file and automatically add a matching file
object if the file actually exists."""
if not is_hex(fid, 4, 4):
raise ValueError(
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
self._select_pre(cmd_app)
try:
# We access the card through the select_file method of the scc object.
# If we succeed, we know that the file exists on the card and we may
# proceed with creating a new CardEF object in the local file model at
# run time. In case the file does not exist on the card, we just abort.
# The state on the card (selected file/application) wont't be changed,
# so we do not have to update any state in that case.
(data, _sw) = self.scc.select_file(fid)
except SwMatchError as swm:
self._select_post(cmd_app)
k = self.interpret_sw(swm.sw_actual)
if not k:
raise swm
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) from swm
select_resp = self.selected_file.decode_select_response(data)
if select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df':
f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
desc="dedicated file, manually added at runtime")
else:
if select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent':
f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
desc="elementary file, manually added at runtime")
else:
f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
desc="elementary file, manually added at runtime")
self.selected_file.add_files([f])
self._select_post(cmd_app, f, data)
def _select_pre(self, cmd_app):
# unregister commands of old file
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.unregister_command_set(c)
def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
# we store some reference data (see above) about the currently selected file.
# This data must be updated after every select.
if file:
self.selected_file = file
if isinstance(file, CardADF):
self.selected_adf = file
if select_resp_data:
self.selected_file_fcp_hex = select_resp_data
self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
else:
self.selected_file_fcp_hex = None
self.selected_file_fcp = None
# register commands of new file
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.register_command_set(c)
def select_file(self, file: CardFile, cmd_app=None):
"""Select a file (EF, DF, ADF, MF, ...).
Args:
file : CardFile [or derived class] instance
cmd_app : Command Application State (for unregistering old file commands)
"""
# we need to find a path from our self.selected_file to the destination
inter_path = self.selected_file.build_select_path_to(file)
if not inter_path:
raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
self._select_pre(cmd_app)
# be sure the variables that we pass to _select_post contain valid values.
selected_file = self.selected_file
data = self.selected_file_fcp_hex
for f in inter_path:
try:
# We now directly accessing the card to perform the selection. This
# will change the state of the card, so we must take care to update
# the local state (lchan) as well. This is done in the method
# _select_post. It should be noted that the caller must always use
# the methods select_file or select. The caller must not access the
# card directly since this would lead into an incoherence of the
# card state and the state of the lchan.
if isinstance(f, CardADF):
(data, _sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
else:
(data, _sw) = self.scc.select_file(f.fid)
selected_file = f
except SwMatchError as swm:
self._select_post(cmd_app, selected_file, data)
raise swm
self._select_post(cmd_app, f, data)
def select(self, name: str, cmd_app=None):
"""Select a file (EF, DF, ADF, MF, ...).
Args:
name : Name of file to select
cmd_app : Command Application State (for unregistering old file commands)
"""
# if any intermediate step fails, we must be able to go back where we were
prev_sel_file = self.selected_file
# handling of entire paths with multiple directories/elements
if '/' in name:
pathlist = name.split('/')
# treat /DF.GSM/foo like MF/DF.GSM/foo
if pathlist[0] == '':
pathlist[0] = 'MF'
try:
for p in pathlist:
self.select(p, cmd_app)
return self.selected_file_fcp
except Exception as e:
self.select_file(prev_sel_file, cmd_app)
raise e
# we are now in the directory where the target file is located
# so we can now refer to the get_selectables() method to get the
# file object and select it using select_file()
sels = self.selected_file.get_selectables()
if is_hex(name):
name = name.lower()
try:
if name in sels:
self.select_file(sels[name], cmd_app)
else:
self.probe_file(name, cmd_app)
except Exception as e:
self.select_file(prev_sel_file, cmd_app)
raise e
return self.selected_file_fcp
def status(self):
"""Request STATUS (current selected file FCP) from card."""
(data, _sw) = self.scc.status()
return self.selected_file.decode_select_response(data)
def get_file_for_selectable(self, name: str):
sels = self.selected_file.get_selectables()
return sels[name]
def activate_file(self, name: str):
"""Request ACTIVATE FILE of specified file."""
sels = self.selected_file.get_selectables()
f = sels[name]
data, sw = self.scc.activate_file(f.fid)
return data, sw
def read_binary(self, length: int = None, offset: int = 0):
"""Read [part of] a transparent EF binary data.
Args:
length : Amount of data to read (None: as much as possible)
offset : Offset into the file from which to read 'length' bytes
Returns:
binary data read from the file
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF")
return self.scc.read_binary(self.selected_file.fid, length, offset)
def read_binary_dec(self) -> Tuple[dict, str]:
"""Read [part of] a transparent EF binary data and decode it.
Args:
length : Amount of data to read (None: as much as possible)
offset : Offset into the file from which to read 'length' bytes
Returns:
abstract decode data read from the file
"""
(data, sw) = self.read_binary()
dec_data = self.selected_file.decode_hex(data)
return (dec_data, sw)
def update_binary(self, data_hex: str, offset: int = 0):
"""Update transparent EF binary data.
Args:
data_hex : hex string of data to be written
offset : Offset into the file from which to write 'data_hex'
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF")
return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
def update_binary_dec(self, data: dict):
"""Update transparent EF from abstract data. Encodes the data to binary and
then updates the EF with it.
Args:
data : abstract data which is to be encoded and written
"""
data_hex = self.selected_file.encode_hex(data)
return self.update_binary(data_hex)
def read_record(self, rec_nr: int = 0):
"""Read a record as binary data.
Args:
rec_nr : Record number to read
Returns:
hex string of binary data contained in record
"""
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF")
# returns a string of hex nibbles
return self.scc.read_record(self.selected_file.fid, rec_nr)
def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
"""Read a record and decode it to abstract data.
Args:
rec_nr : Record number to read
Returns:
abstract data contained in record
"""
(data, sw) = self.read_record(rec_nr)
return (self.selected_file.decode_record_hex(data, rec_nr), sw)
def update_record(self, rec_nr: int, data_hex: str):
"""Update a record with given binary data
Args:
rec_nr : Record number to read
data_hex : Hex string binary data to be written
"""
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF")
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
conserve=self.rs.conserve_write,
leftpad=self.selected_file.leftpad)
def update_record_dec(self, rec_nr: int, data: dict):
"""Update a record with given abstract data. Will encode abstract to binary data
and then write it to the given record on the card.
Args:
rec_nr : Record number to read
data_hex : Abstract data to be written
"""
data_hex = self.selected_file.encode_record_hex(data, rec_nr)
return self.update_record(rec_nr, data_hex)
def retrieve_data(self, tag: int = 0):
"""Read a DO/TLV as binary data.
Args:
tag : Tag of TLV/DO to read
Returns:
hex string of full BER-TLV DO including Tag and Length
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
# returns a string of hex nibbles
return self.scc.retrieve_data(self.selected_file.fid, tag)
def retrieve_tags(self):
"""Retrieve tags available on BER-TLV EF.
Returns:
list of integer tags contained in EF
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
data, _sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
_tag, _length, value, _remainder = bertlv_parse_one(h2b(data))
return list(value)
def set_data(self, tag: int, data_hex: str):
"""Update a TLV/DO with given binary data
Args:
tag : Tag of TLV/DO to be written
data_hex : Hex string binary data to be written (value portion)
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
def unregister_cmds(self, cmd_app=None):
"""Unregister all file specific commands."""
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.unregister_command_set(c)

37
pySim/secure_channel.py Normal file
View File

@@ -0,0 +1,37 @@
# Generic code related to Secure Channel processing
#
# (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 abc
from pySim.utils import b2h, h2b, ResTuple, Hexstr
class SecureChannel(abc.ABC):
@abc.abstractmethod
def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
"""Wrap Command APDU according to specific Secure Channel Protocol."""
pass
@abc.abstractmethod
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
"""UnWrap Response-APDU according to specific Secure Channel Protocol."""
pass
def send_apdu_wrapper(self, send_fn: callable, pdu: Hexstr, *args, **kwargs) -> ResTuple:
"""Wrapper function to wrap command APDU and unwrap repsonse APDU around send_apdu callable."""
pdu_wrapped = b2h(self.wrap_cmd_apdu(h2b(pdu)))
res, sw = send_fn(pdu_wrapped, *args, **kwargs)
res_unwrapped = b2h(self.unwrap_rsp_apdu(h2b(sw), h2b(res)))
return res_unwrapped, sw

399
pySim/sms.py Normal file
View File

@@ -0,0 +1,399 @@
"""Code related to SMS Encoding/Decoding"""
# simplistic SMS T-PDU code, as unfortunately nobody bothered to port the python smspdu
# module to python3, and I gave up after >= 3 hours of trying and failing to do so
# (C) 2022 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 typing
import abc
from bidict import bidict
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger
from construct import Struct, Enum, Tell, BitStruct, this, Padding
from construct import Prefixed, GreedyRange, GreedyBytes
from pySim.construct import HexAdapter, BcdAdapter, TonNpi
from pySim.utils import Hexstr, h2b, b2h
from smpp.pdu import pdu_types, operations
BytesOrHex = typing.Union[Hexstr, bytes]
class UserDataHeader:
# a single IE in the user data header
ie_c = Struct('iei'/Int8ub, 'length'/Int8ub, 'value'/Bytes(this.length))
# parser for the full UDH: Length octet followed by sequence of IEs
_construct = Struct('ies'/Prefixed(Int8ub, GreedyRange(ie_c)),
'data'/GreedyBytes)
def __init__(self, ies=[]):
self.ies = ies
def __repr__(self) -> str:
return 'UDH(%r)' % self.ies
def has_ie(self, iei:int) -> bool:
for ie in self.ies:
if ie['iei'] == iei:
return True
return False
@classmethod
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
if isinstance(inb, str):
inb = h2b(inb)
res = cls._construct.parse(inb)
return cls(res['ies']), res['data']
def to_bytes(self) -> bytes:
return self._construct.build({'ies':self.ies, 'data':b''})
def smpp_dcs_is_8bit(dcs: pdu_types.DataCoding) -> bool:
"""Determine if the given SMPP data coding scheme is 8-bit or not."""
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
return True
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED_COMMON):
return True
# pySim/sms.py:72:21: E1101: Instance of 'DataCodingScheme' has no 'GSM_MESSAGE_CLASS' member (no-member)
# pylint: disable=no-member
if dcs.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS and dcs.schemeData['msgCoding'] == pdu_types.DataCodingGsmMsgCoding.DATA_8BIT:
return True
else:
return False
def ensure_smpp_is_8bit(dcs: pdu_types.DataCoding):
"""Assert if given SMPP data coding scheme is not 8-bit."""
if not smpp_dcs_is_8bit(dcs):
raise ValueError('We only support 8bit coded SMS for now')
class AddressField:
"""Representation of an address field as used in SMS T-PDU."""
_construct = Struct('addr_len'/Int8ub,
'type_of_addr'/TonNpi,
'digits'/BcdAdapter(Bytes(this.addr_len//2 + this.addr_len%2)),
'tell'/Tell)
smpp_map_npi = bidict({
'UNKNOWN': 'unknown',
'ISDN': 'isdn_e164',
'DATA': 'data_x121',
'TELEX': 'telex_f69',
'LAND_MOBILE': 'sc_specific6',
'NATIONAL': 'national',
'PRIVATE': 'private',
'ERMES': 'ermes',
})
smpp_map_ton = bidict({
'UNKNOWN': 'unknown',
'INTERNATIONAL': 'international',
'NATIONAL': 'national',
'NETWORK_SPECIFIC': 'network_specific',
'SUBSCRIBER_NUMBER': 'short_code',
'ALPHANUMERIC': 'alphanumeric',
'ABBREVIATED': 'abbreviated',
})
def __init__(self, digits, ton='unknown', npi='unknown'):
self.ton = ton
self.npi = npi
self.digits = digits
def __str__(self):
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
@classmethod
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
"""Construct an AddressField instance from the binary T-PDU address format."""
if isinstance(inb, str):
inb = h2b(inb)
res = cls._construct.parse(inb)
#print("size: %s" % cls._construct.sizeof())
ton = res['type_of_addr']['type_of_number']
npi = res['type_of_addr']['numbering_plan_id']
# return resulting instance + remainder bytes
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
@classmethod
def from_smpp(cls, addr, ton, npi) -> 'AddressField':
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
# return the resulting instance
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
def to_smpp(self):
"""Return smpp.pdo.*.source,dest}_addr_{,ton,npi} attributes for given AddressField."""
return (self.digits, self.smpp_map_ton.inverse[self.ton], self.smpp_map_npi.inverse[self.npi])
def to_bytes(self) -> bytes:
"""Encode the AddressField into the binary representation as used in T-PDU."""
num_digits = len(self.digits)
if num_digits % 2:
self.digits += 'f'
d = {
'addr_len': num_digits,
'type_of_addr': {
'ext': True,
'type_of_number': self.ton,
'numbering_plan_id': self.npi,
},
'digits': self.digits,
}
return self._construct.build(d)
class SMS_TPDU(abc.ABC):
"""Base class for a SMS T-PDU."""
def __init__(self, **kwargs):
self.tp_mti = kwargs.get('tp_mti', None)
self.tp_rp = kwargs.get('tp_rp', False)
self.tp_udhi = kwargs.get('tp_udhi', False)
self.tp_pid = kwargs.get('tp_pid', None)
self.tp_dcs = kwargs.get('tp_dcs', None)
self.tp_udl = kwargs.get('tp_udl', None)
self.tp_ud = kwargs.get('tp_ud', None)
class SMS_DELIVER(SMS_TPDU):
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
flags_construct = BitStruct('tp_rp'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag, 'tp_sri'/Flag,
Padding(1), 'tp_mms'/Flag, 'tp_mti'/BitsInteger(2))
def __init__(self, **kwargs):
kwargs['tp_mti'] = 0
super().__init__(**kwargs)
self.tp_lp = kwargs.get('tp_lp', False)
self.tp_mms = kwargs.get('tp_mms', False)
self.tp_oa = kwargs.get('tp_oa', None)
self.tp_scts = kwargs.get('tp_scts', None)
self.tp_sri = kwargs.get('tp_sri', False)
def __repr__(self):
return '%s(MTI=%s, MMS=%s, LP=%s, RP=%s, UDHI=%s, SRI=%s, OA=%s, PID=%2x, DCS=%x, SCTS=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_mms, self.tp_lp, self.tp_rp, self.tp_udhi, self.tp_sri, self.tp_oa, self.tp_pid, self.tp_dcs, self.tp_scts, self.tp_udl, self.tp_ud)
@classmethod
def from_bytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from the binary encoded format as used in T-PDU."""
if isinstance(inb, str):
inb = h2b(inb)
d = SMS_DELIVER.flags_construct.parse(inb)
oa, remainder = AddressField.from_bytes(inb[1:])
d['tp_oa'] = oa
offset = 0
d['tp_pid'] = remainder[offset]
offset += 1
d['tp_dcs'] = remainder[offset]
offset += 1
# TODO: further decode
d['tp_scts'] = remainder[offset:offset+7]
offset += 7
d['tp_udl'] = remainder[offset]
offset += 1
d['tp_ud'] = remainder[offset:]
return cls(**d)
def to_bytes(self) -> bytes:
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
outb = bytearray()
d = {
'tp_mti': self.tp_mti, 'tp_mms': self.tp_mms, 'tp_lp': self.tp_lp,
'tp_rp': self.tp_rp, 'tp_udhi': self.tp_udhi, 'tp_sri': self.tp_sri,
}
flags = SMS_DELIVER.flags_construct.build(d)
outb.extend(flags)
outb.extend(self.tp_oa.to_bytes())
outb.append(self.tp_pid)
outb.append(self.tp_dcs)
outb.extend(self.tp_scts)
outb.append(self.tp_udl)
outb.extend(self.tp_ud)
return outb
@classmethod
def from_smpp(cls, smpp_pdu) -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from the deliver format used by smpp.pdu."""
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
return cls.from_smpp_submit(smpp_pdu)
else:
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
@classmethod
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
tp_oa = AddressField.from_smpp(smpp_pdu.params['source_addr'],
smpp_pdu.params['source_addr_ton'],
smpp_pdu.params['source_addr_npi'])
tp_ud = smpp_pdu.params['short_message']
d = {
'tp_lp': False,
'tp_mms': False,
'tp_oa': tp_oa,
'tp_scts': h2b('22705200000000'), # FIXME
'tp_sri': False,
'tp_rp': False,
'tp_udhi': pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET in smpp_pdu.params['esm_class'].gsmFeatures,
'tp_pid': smpp_pdu.params['protocol_id'],
'tp_dcs': 0xF6, # we only deal with binary SMS here
'tp_udl': len(tp_ud),
'tp_ud': tp_ud,
}
return cls(**d)
class SMS_SUBMIT(SMS_TPDU):
"""Representation of a SMS-SUBMIT T-PDU. This is the MS/UE -> network (uplink) direction."""
flags_construct = BitStruct('tp_srr'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag,
'tp_vpf'/Enum(BitsInteger(2), none=0, relative=2, enhanced=1, absolute=3),
'tp_rd'/Flag, 'tp_mti'/BitsInteger(2))
def __init__(self, **kwargs):
kwargs['tp_mti'] = 1
super().__init__(**kwargs)
self.tp_rd = kwargs.get('tp_rd', False)
self.tp_vpf = kwargs.get('tp_vpf', 'none')
self.tp_srr = kwargs.get('tp_srr', False)
self.tp_mr = kwargs.get('tp_mr', None)
self.tp_da = kwargs.get('tp_da', None)
self.tp_vp = kwargs.get('tp_vp', None)
def __repr__(self):
return '%s(MTI=%s, RD=%s, VPF=%u, RP=%s, UDHI=%s, SRR=%s, DA=%s, PID=%2x, DCS=%x, VP=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_rd, self.tp_vpf, self.tp_rp, self.tp_udhi, self.tp_srr, self.tp_da, self.tp_pid, self.tp_dcs, self.tp_vp, self.tp_udl, self.tp_ud)
@classmethod
def from_bytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the binary encoded format as used in T-PDU."""
offset = 0
if isinstance(inb, str):
inb = h2b(inb)
d = SMS_SUBMIT.flags_construct.parse(inb)
offset += 1
d['tp_mr']= inb[offset]
offset += 1
da, remainder = AddressField.from_bytes(inb[2:])
d['tp_da'] = da
offset = 0
d['tp_pid'] = remainder[offset]
offset += 1
d['tp_dcs'] = remainder[offset]
offset += 1
if d['tp_vpf'] == 'none':
pass
elif d['tp_vpf'] == 'relative':
# TODO: further decode
d['tp_vp'] = remainder[offset:offset+1]
offset += 1
elif d['tp_vpf'] == 'enhanced':
# TODO: further decode
d['tp_vp'] = remainder[offset:offset+7]
offset += 7
elif d['tp_vpf'] == 'absolute':
# TODO: further decode
d['tp_vp'] = remainder[offset:offset+7]
offset += 7
else:
raise ValueError('Invalid VPF: %s' % d['tp_vpf'])
d['tp_udl'] = remainder[offset]
offset += 1
d['tp_ud'] = remainder[offset:]
return cls(**d)
def to_bytes(self) -> bytes:
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
outb = bytearray()
d = {
'tp_mti': self.tp_mti, 'tp_rd': self.tp_rd, 'tp_vpf': self.tp_vpf,
'tp_rp': self.tp_rp, 'tp_udhi': self.tp_udhi, 'tp_srr': self.tp_srr,
}
flags = SMS_SUBMIT.flags_construct.build(d)
outb.extend(flags)
outb.append(self.tp_mr)
outb.extend(self.tp_da.to_bytes())
outb.append(self.tp_pid)
outb.append(self.tp_dcs)
if self.tp_vpf != 'none':
outb.extend(self.tp_vp)
outb.append(self.tp_udl)
outb.extend(self.tp_ud)
return outb
@classmethod
def from_smpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
return cls.from_smpp_submit(smpp_pdu)
else:
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
@classmethod
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
tp_da = AddressField.from_smpp(smpp_pdu.params['destination_addr'],
smpp_pdu.params['dest_addr_ton'],
smpp_pdu.params['dest_addr_npi'])
tp_ud = smpp_pdu.params['short_message']
#vp_smpp = smpp_pdu.params['validity_period']
#if not vp_smpp:
# vpf = 'none'
d = {
'tp_rd': True if smpp_pdu.params['replace_if_present_flag'].name == 'REPLACE' else False,
'tp_vpf': None, # vpf,
'tp_rp': False, # related to ['registered_delivery'] ?
'tp_udhi': pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET in smpp_pdu.params['esm_class'].gsmFeatures,
'tp_srr': True if smpp_pdu.params['registered_delivery'] else False,
'tp_mr': 0, # FIXME: sm_default_msg_id ?
'tp_da': tp_da,
'tp_pid': smpp_pdu.params['protocol_id'],
'tp_dcs': 0xF6, # FIXME: we only deal with binary SMS here
'tp_vp': None, # FIXME: implement VPF conversion
'tp_udl': len(tp_ud),
'tp_ud': tp_ud,
}
return cls(**d)
def to_smpp(self) -> pdu_types.PDU:
"""Translate a SMS_SUBMIT instance to a smpp.pdu.operations.SubmitSM instance."""
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT)
reg_del = pdu_types.RegisteredDelivery(pdu_types.RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED)
if self.tp_rp:
repl_if = pdu_types.ReplaceIfPresentFlag.REPLACE
else:
repl_if = pdu_types.ReplaceIfPresentFlag.DO_NOT_REPLACE
# we only deal with binary SMS here:
if self.tp_dcs != 0xF6:
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
dc = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT, pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
(daddr, ton, npi) = self.tp_da.to_smpp()
return operations.SubmitSM(service_type='',
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
source_addr='simcard',
dest_addr_ton=ton,
dest_addr_npi=npi,
destination_addr=daddr,
esm_class=esm_class,
protocol_id=self.tp_pid,
priority_flag=pdu_types.PriorityFlag.LEVEL_0,
#schedule_delivery_time,
#validity_period,
registered_delivery=reg_del,
replace_if_present_flag=repl_if,
data_coding=dc,
#sm_default_msg_id,
short_message=self.tp_ud)

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""Utilities / Functions related to sysmocom SJA2 cards
"""Utilities / Functions related to sysmocom SJA2/SJA5 cards
(C) 2021 by Harald Welte <laforge@osmocom.org>
(C) 2021-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
@@ -17,13 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pytlv.TLV import *
from struct import pack, unpack
from struct import unpack
from construct import FlagsEnum, Byte, Struct, Int8ub, Bytes, Mapping, Enum, Padding, BitsInteger
from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange
from construct import Optional as COptional
from pySim.utils import *
from pySim.filesystem import *
from pySim.ts_102_221 import CardProfileUICC
from pySim.runtime import RuntimeState
from pySim.construct import *
from construct import *
import pySim
key_type2str = {
@@ -45,69 +47,73 @@ mac_length = {
class EF_PIN(TransparentEF):
def __init__(self, fid, name):
_test_de_encode = [
( 'f1030331323334ffffffff0a0a3132333435363738',
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
'not_initialized': False, 'disabled': True },
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '31323334',
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': '3132333435363738' }
} ),
( 'f003039999999999999999',
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
'not_initialized': False, 'disabled': False },
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '9999999999999999',
'puk': None } ),
]
def __init__(self, fid='6f01', name='EF.CHV1'):
super().__init__(fid, name=name, desc='%s PIN file' % name)
def _decode_bin(self, raw_bin_data):
u = unpack('!BBB8s', raw_bin_data[:11])
res = {'enabled': (True, False)[u[0] & 0x01],
'initialized': (True, False)[u[0] & 0x02],
'disable_able': (False, True)[u[0] & 0x10],
'unblock_able': (False, True)[u[0] & 0x20],
'change_able': (False, True)[u[0] & 0x40],
'valid': (False, True)[u[0] & 0x80],
'attempts_remaining': u[1],
'maximum_attempts': u[2],
'pin': u[3].hex(),
}
if len(raw_bin_data) == 21:
u2 = unpack('!BB8s', raw_bin_data[11:10])
res['attempts_remaining_puk'] = u2[0]
res['maximum_attempts_puk'] = u2[1]
res['puk'] = u2[2].hex()
return res
StateByte = FlagsEnum(Byte, disabled=1, not_initialized=2, disable_able=0x10, unblock_able=0x20,
change_able=0x40, valid=0x80)
PukStruct = Struct('attempts_remaining'/Int8ub,
'maximum_attempts'/Int8ub,
'puk'/HexAdapter(Rpad(Bytes(8))))
self._construct = Struct('state'/StateByte,
'attempts_remaining'/Int8ub,
'maximum_attempts'/Int8ub,
'pin'/HexAdapter(Rpad(Bytes(8))),
'puk'/COptional(PukStruct))
class EF_MILENAGE_CFG(TransparentEF):
_test_de_encode = [
( '40002040600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008',
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96, "c1": "00000000000000000000000000000000", "c2":
"00000000000000000000000000000001", "c3": "00000000000000000000000000000002", "c4":
"00000000000000000000000000000004", "c5": "00000000000000000000000000000008"} ),
]
def __init__(self, fid='6f21', name='EF.MILENAGE_CFG', desc='Milenage connfiguration'):
super().__init__(fid, name=name, desc=desc)
def _decode_bin(self, raw_bin_data):
u = unpack('!BBBBB16s16s16s16s16s', raw_bin_data)
return {'r1': u[0], 'r2': u[1], 'r3': u[2], 'r4': u[3], 'r5': u[4],
'c1': u[5].hex(),
'c2': u[6].hex(),
'c3': u[7].hex(),
'c4': u[8].hex(),
'c5': u[9].hex(),
}
self._construct = Struct('r1'/Int8ub, 'r2'/Int8ub, 'r3'/Int8ub, 'r4'/Int8ub, 'r5'/Int8ub,
'c1'/HexAdapter(Bytes(16)),
'c2'/HexAdapter(Bytes(16)),
'c3'/HexAdapter(Bytes(16)),
'c4'/HexAdapter(Bytes(16)),
'c5'/HexAdapter(Bytes(16)))
class EF_0348_KEY(LinFixedEF):
def __init__(self, fid='6f22', name='EF.0348_KEY', desc='TS 03.48 OTA Keys'):
super().__init__(fid, name=name, desc=desc, rec_len=(27, 35))
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BBB', raw_bin_data[0:3])
key_algo = (u[2] >> 6) & 1
key_length = ((u[2] >> 3) & 3) * 8
return {'sec_domain': u[0],
'key_set_version': u[1],
'key_type': key_type2str[u[2] & 3],
'key_length': key_length,
'algorithm': key_algo2str[key_algo],
'mac_length': mac_length[(u[2] >> 7)],
'key': raw_bin_data[3:key_length].hex()
}
KeyLenAndType = BitStruct('mac_length'/Mapping(Bit, {8:0, 4:1}),
'algorithm'/Enum(Bit, des=0, aes=1),
'key_length'/MultiplyAdapter(BitsInteger(3), 8),
'_rfu'/BitsRFU(1),
'key_type'/Enum(BitsInteger(2), kic=0, kid=1, kik=2, any=3))
self._construct = Struct('security_domain'/Int8ub,
'key_set_version'/Int8ub,
'key_len_and_type'/KeyLenAndType,
'key'/HexAdapter(Bytes(this.key_len_and_type.key_length)))
class EF_0348_COUNT(LinFixedEF):
_test_de_encode = [
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": "0000000000"} ),
]
def __init__(self, fid='6f23', name='EF.0348_COUNT', desc='TS 03.48 OTA Counters'):
super().__init__(fid, name=name, desc=desc, rec_len=(7, 7))
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BB5s', raw_bin_data)
return {'sec_domain': u[0], 'key_set_version': u[1], 'counter': u[2]}
self._construct = Struct('sec_domain'/Int8ub,
'key_set_version'/Int8ub,
'counter'/HexAdapter(Bytes(5)))
class EF_SIM_AUTH_COUNTER(TransparentEF):
@@ -117,38 +123,40 @@ class EF_SIM_AUTH_COUNTER(TransparentEF):
class EF_GP_COUNT(LinFixedEF):
_test_de_encode = [
( '0070000000', {"sec_domain": 0, "key_set_version": 112, "counter": 0, "rfu": 0} ),
]
def __init__(self, fid='6f26', name='EF.GP_COUNT', desc='GP SCP02 Counters'):
super().__init__(fid, name=name, desc=desc, rec_len=(5, 5))
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BBHB', raw_bin_data)
return {'sec_domain': u[0], 'key_set_version': u[1], 'counter': u[2], 'rfu': u[3]}
self._construct = Struct('sec_domain'/Int8ub,
'key_set_version'/Int8ub,
'counter'/Int16ub,
'rfu'/Int8ub)
class EF_GP_DIV_DATA(LinFixedEF):
def __init__(self, fid='6f27', name='EF.GP_DIV_DATA', desc='GP SCP02 key diversification data'):
super().__init__(fid, name=name, desc=desc, rec_len=(12, 12))
def _decode_record_bin(self, raw_bin_data):
def _decode_record_bin(self, raw_bin_data, **kwargs):
u = unpack('!BB8s', raw_bin_data)
return {'sec_domain': u[0], 'key_set_version': u[1], 'key_div_data': u[2].hex()}
class EF_SIM_AUTH_KEY(TransparentEF):
_test_de_encode = [
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"}, "key":
"000102030405060708090a0b0c0d0e0f", "op_opc": "101112131415161718191a1b1c1d1e1f"} ),
]
def __init__(self, fid='6f20', name='EF.SIM_AUTH_KEY'):
super().__init__(fid, name=name, desc='USIM authentication key')
CfgByte = BitStruct(Padding(2),
'use_sres_deriv_func_2'/Bit,
'use_opc_instead_of_op'/Bit,
'sres_deriv_func'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Flag,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
'op'/ If(this.cfg.algorithm == 'milenage' and not this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16))),
'opc' /
If(this.cfg.algorithm == 'milenage' and this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16)))
)
'op_opc' /HexAdapter(Bytes(16)))
class DF_SYSTEM(CardDF):
@@ -176,13 +184,21 @@ class DF_SYSTEM(CardDF):
class EF_USIM_SQN(TransparentEF):
_test_de_encode = [
( 'd503000200000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
{"flag1": {"skip_next_sqn_check": True, "delta_max_check": True, "age_limit_check": False, "sqn_check": True,
"ind_len": 5}, "flag2": {"rfu": 0, "dont_clear_amf_for_macs": False, "aus_concealed": True,
"autn_concealed": True}, "delta_max": 8589934592, "age_limit":
8589934592, "freshness": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]} ),
]
def __init__(self, fid='af30', name='EF.USIM_SQN'):
super().__init__(fid, name=name, desc='SQN parameters for AKA')
Flag1 = BitStruct('skip_next_sqn_check'/Bit, 'delta_max_check'/Bit,
'age_limit_check'/Bit, 'sqn_check'/Bit,
Flag1 = BitStruct('skip_next_sqn_check'/Flag, 'delta_max_check'/Flag,
'age_limit_check'/Flag, 'sqn_check'/Flag,
'ind_len'/BitsInteger(4))
Flag2 = BitStruct('rfu'/BitsRFU(5), 'dont_clear_amf_for_macs'/Bit,
'aus_concealed'/Bit, 'autn_concealed'/Bit)
Flag2 = BitStruct('rfu'/BitsRFU(5), 'dont_clear_amf_for_macs'/Flag,
'aus_concealed'/Flag, 'autn_concealed'/Flag)
self._construct = Struct('flag1'/Flag1, 'flag2'/Flag2,
'delta_max' /
BytesInteger(6), 'age_limit'/BytesInteger(6),
@@ -192,37 +208,61 @@ class EF_USIM_SQN(TransparentEF):
class EF_USIM_AUTH_KEY(TransparentEF):
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
super().__init__(fid, name=name, desc='USIM authentication key')
CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Bit,
'use_sres_deriv_func_2_in_3g'/Bit,
'use_opc_instead_of_op'/Bit,
'algorithm'/Enum(Nibble, milenage=4, sha1_aka=5, xor=15))
Algorithm = Enum(Nibble, milenage=4, sha1_aka=5, tuak=6, xor=15)
CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Flag,
'sres_deriv_func_in_2g'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
'algorithm'/Algorithm)
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
'op' /
If(this.cfg.algorithm == 'milenage' and not this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16))),
'opc' /
If(this.cfg.algorithm == 'milenage' and this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16)))
)
'op_opc' /HexAdapter(Bytes(16)))
# TUAK has a rather different layout for the data, so we define a different
# construct below and use explicit _{decode,encode}_bin() methods for separating
# the TUAK and non-TUAK situation
CfgByteTuak = BitStruct(Padding(1),
'key_length'/Mapping(Bit, {128:0, 256:1}),
'sres_deriv_func_in_2g'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
'algorithm'/Algorithm)
TuakCfgByte = BitStruct(Padding(1),
'ck_and_ik_size'/Mapping(Bit, {128:0, 256:1}),
'mac_size'/Mapping(BitsInteger(3), {64:0, 128:1, 256:2}),
'res_size'/Mapping(BitsInteger(3), {32:0, 64:1, 128:2, 256:3}))
self._constr_tuak = Struct('cfg'/CfgByteTuak,
'tuak_cfg'/TuakCfgByte,
'num_of_keccak_iterations'/Int8ub,
'op_opc'/HexAdapter(Bytes(32)),
'k'/HexAdapter(Bytes(this.cfg.key_length//8)))
def _decode_bin(self, raw_bin_data: bytearray) -> dict:
if raw_bin_data[0] & 0x0F == 0x06:
return parse_construct(self._constr_tuak, raw_bin_data)
else:
return parse_construct(self._construct, raw_bin_data)
def _encode_bin(self, abstract_data: dict) -> bytearray:
if abstract_data['cfg']['algorithm'] == 'tuak':
return build_construct(self._constr_tuak, abstract_data)
else:
return build_construct(self._construct, abstract_data)
class EF_USIM_AUTH_KEY_2G(TransparentEF):
_test_de_encode = [
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
{"cfg": {"only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True,
"algorithm": "milenage"}, "key": "000102030405060708090a0b0c0d0e0f", "op_opc":
"101112131415161718191a1b1c1d1e1f"} ),
]
def __init__(self, fid='af22', name='EF.USIM_AUTH_KEY_2G'):
super().__init__(fid, name=name, desc='USIM authentication key in 2G context')
CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Bit,
'use_sres_deriv_func_2_in_3g'/Bit,
'use_opc_instead_of_op'/Bit,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Flag,
'sres_deriv_func_in_2g'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Flag,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3, xor=14))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
'op' /
If(this.cfg.algorithm == 'milenage' and not this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16))),
'opc' /
If(this.cfg.algorithm == 'milenage' and this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16)))
)
'op_opc' /HexAdapter(Bytes(16)))
class EF_GBA_SK(TransparentEF):
@@ -275,3 +315,34 @@ class SysmocomSJA2(CardModel):
EF_USIM_SQN(name='EF.ISIM_SQN'),
]
isim_adf.add_files(files_adf_isim)
class SysmocomSJA5(CardModel):
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"]
@classmethod
def add_files(cls, rs: RuntimeState):
"""Add sysmocom SJA2 specific files to given RuntimeState."""
rs.mf.add_file(DF_SYSTEM())
# optional USIM application
if 'a0000000871002' in rs.mf.applications:
usim_adf = rs.mf.applications['a0000000871002']
files_adf_usim = [
EF_USIM_AUTH_KEY(),
EF_USIM_AUTH_KEY_2G(),
EF_GBA_SK(),
EF_GBA_REC_LIST(),
EF_GBA_INT_KEY(),
EF_USIM_SQN(),
]
usim_adf.add_files(files_adf_usim)
# optional ISIM application
if 'a0000000871004' in rs.mf.applications:
isim_adf = rs.mf.applications['a0000000871004']
files_adf_isim = [
EF_USIM_AUTH_KEY(name='EF.ISIM_AUTH_KEY'),
EF_USIM_AUTH_KEY_2G(name='EF.ISIM_AUTH_KEY_2G'),
EF_USIM_SQN(name='EF.ISIM_SQN'),
]
isim_adf.add_files(files_adf_isim)

View File

@@ -16,22 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, List, Dict, Any, Tuple
from bidict import bidict
from construct import *
from pySim.utils import bertlv_encode_len, bertlv_parse_len, bertlv_encode_tag, bertlv_parse_tag
from pySim.utils import comprehensiontlv_encode_tag, comprehensiontlv_parse_tag
from pySim.utils import bertlv_parse_one, comprehensiontlv_parse_one
from pySim.utils import bertlv_parse_tag_raw, comprehensiontlv_parse_tag_raw
from pySim.construct import parse_construct, LV, HexAdapter, BcdAdapter, BitsRFU, GsmStringAdapter
from pySim.exceptions import *
import inspect
import abc
import re
from typing import List, Tuple
from pySim.utils import bertlv_encode_len, bertlv_parse_len, bertlv_encode_tag, bertlv_parse_tag
from pySim.utils import comprehensiontlv_encode_tag, comprehensiontlv_parse_tag
from pySim.utils import bertlv_parse_tag_raw, comprehensiontlv_parse_tag_raw
from pySim.utils import dgi_parse_tag_raw, dgi_parse_len, dgi_encode_tag, dgi_encode_len
from pySim.construct import build_construct, parse_construct
def camel_to_snake(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
@@ -41,9 +37,9 @@ class TlvMeta(abc.ABCMeta):
"""Metaclass which we use to set some class variables at the time of defining a subclass.
This allows us to create subclasses for each TLV/IE type, where the class represents fixed
parameters like the tag/type and instances of it represent the actual TLV data."""
def __new__(metacls, name, bases, namespace, **kwargs):
#print("TlvMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
x = super().__new__(metacls, name, bases, namespace)
def __new__(mcs, name, bases, namespace, **kwargs):
#print("TlvMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
x = super().__new__(mcs, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.tag = namespace.get('tag', kwargs.get('tag', None))
x.desc = namespace.get('desc', kwargs.get('desc', None))
@@ -64,9 +60,9 @@ class TlvCollectionMeta(abc.ABCMeta):
"""Metaclass which we use to set some class variables at the time of defining a subclass.
This allows us to create subclasses for each Collection type, where the class represents fixed
parameters like the nested IE classes and instances of it represent the actual TLV data."""
def __new__(metacls, name, bases, namespace, **kwargs):
#print("TlvCollectionMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
x = super().__new__(metacls, name, bases, namespace)
def __new__(mcs, name, bases, namespace, **kwargs):
#print("TlvCollectionMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
x = super().__new__(mcs, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
return x
@@ -84,15 +80,15 @@ class Transcodable(abc.ABC):
self.decoded = None
self._construct = None
def to_bytes(self) -> bytes:
def to_bytes(self, context: dict = {}) -> bytes:
"""Convert from internal representation to binary bytes. Store the binary result
in the internal state and return it."""
if not self.decoded:
if self.decoded is None:
do = b''
elif self._construct:
do = self._construct.build(self.decoded, total_len=None)
do = build_construct(self._construct, self.decoded, context)
elif self.__class__._construct:
do = self.__class__._construct.build(self.decoded, total_len=None)
do = build_construct(self.__class__._construct, self.decoded, context)
else:
do = self._to_bytes()
self.encoded = do
@@ -102,16 +98,16 @@ class Transcodable(abc.ABC):
def _to_bytes(self):
raise NotImplementedError('%s._to_bytes' % type(self).__name__)
def from_bytes(self, do: bytes):
def from_bytes(self, do: bytes, context: dict = {}):
"""Convert from binary bytes to internal representation. Store the decoded result
in the internal state and return it."""
self.encoded = do
if self.encoded == b'':
self.decoded = None
elif self._construct:
self.decoded = parse_construct(self._construct, do)
self.decoded = parse_construct(self._construct, do, context=context)
elif self.__class__._construct:
self.decoded = parse_construct(self.__class__._construct, do)
self.decoded = parse_construct(self.__class__._construct, do, context=context)
else:
self.decoded = self._from_bytes(do)
return self.decoded
@@ -157,49 +153,46 @@ class IE(Transcodable, metaclass=TlvMeta):
def from_dict(self, decoded: dict):
"""Set the IE internal decoded representation to data from the argument.
If this is a nested IE, the child IE instance list is re-created."""
expected_key_name = camel_to_snake(type(self).__name__)
if not expected_key_name in decoded:
raise ValueError("Dict %s doesn't contain expected key %s" % (decoded, expected_key_name))
if self.nested_collection:
self.children = self.nested_collection.from_dict(decoded)
self.children = self.nested_collection.from_dict(decoded[expected_key_name])
else:
self.children = []
self.decoded = decoded
self.decoded = decoded[expected_key_name]
def is_constructed(self):
"""Is this IE constructed by further nested IEs?"""
if len(self.children):
return True
else:
return False
return bool(len(self.children) > 0)
@abc.abstractmethod
def to_ie(self) -> bytes:
def to_ie(self, context: dict = {}) -> bytes:
"""Convert the internal representation to entire IE including IE header."""
def to_bytes(self) -> bytes:
"""Convert the internal representation _of the value part_ to binary bytes."""
def to_bytes(self, context: dict = {}) -> bytes:
"""Convert the internal representation *of the value part* to binary bytes."""
if self.is_constructed():
# concatenate the encoded IE of all children to form the value part
out = b''
for c in self.children:
out += c.to_ie()
out += c.to_ie(context=context)
return out
else:
return super().to_bytes()
return super().to_bytes(context=context)
def from_bytes(self, do: bytes):
"""Parse _the value part_ from binary bytes to internal representation."""
def from_bytes(self, do: bytes, context: dict = {}):
"""Parse *the value part* from binary bytes to internal representation."""
if self.nested_collection:
self.children = self.nested_collection.from_bytes(do)
self.children = self.nested_collection.from_bytes(do, context=context)
else:
self.children = []
return super().from_bytes(do)
return super().from_bytes(do, context=context)
class TLV_IE(IE):
"""Abstract base class for various TLV type Information Elements."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
def _compute_tag(self) -> int:
"""Compute the tag (sometimes the tag encodes part of the value)."""
return self.tag
@@ -223,15 +216,15 @@ class TLV_IE(IE):
"""Encode the length part assuming a certain binary value. Must be provided by
derived (TLV format specific) class."""
def to_ie(self):
return self.to_tlv()
def to_ie(self, context: dict = {}):
return self.to_tlv(context=context)
def to_tlv(self):
def to_tlv(self, context: dict = {}):
"""Convert the internal representation to binary TLV bytes."""
val = self.to_bytes()
val = self.to_bytes(context=context)
return self._encode_tag() + self._encode_len(val) + val
def from_tlv(self, do: bytes):
def from_tlv(self, do: bytes, context: dict = {}):
if len(do) == 0:
return {}, b''
(rawtag, remainder) = self.__class__._parse_tag_raw(do)
@@ -245,16 +238,13 @@ class TLV_IE(IE):
else:
value = do
remainder = b''
dec = self.from_bytes(value)
dec = self.from_bytes(value, context=context)
return dec, remainder
class BER_TLV_IE(TLV_IE):
"""TLV_IE formatted as ASN.1 BER described in ITU-T X.690 8.1.2."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
@classmethod
def _decode_tag(cls, do: bytes) -> Tuple[dict, bytes]:
return bertlv_parse_tag(do)
@@ -300,6 +290,24 @@ class COMPR_TLV_IE(TLV_IE):
return bertlv_encode_len(len(val))
class DGI_TLV_IE(TLV_IE):
"""TLV_IE formated as GlobalPlatform Systems Scripting Language Specification v1.1.0 Annex B."""
@classmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
return dgi_parse_tag_raw(do)
@classmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return dgi_parse_len(do)
def _encode_tag(self) -> bytes:
return dgi_encode_tag(self._compute_tag())
def _encode_len(self, val: bytes) -> bytes:
return dgi_encode_len(len(val))
class TLV_IE_Collection(metaclass=TlvCollectionMeta):
# we specify the metaclass so any downstream subclasses will automatically use it
"""A TLV_IE_Collection consists of multiple TLV_IE classes identified by their tags.
@@ -315,7 +323,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
self.members_by_tag = {}
self.members_by_name = {}
self.members_by_tag = {m.tag: m for m in self.members}
self.members_by_name = {m.__name__: m for m in self.members}
self.members_by_name = {camel_to_snake(m.__name__): m for m in self.members}
# if we are a constructed IE, [ordered] list of actual child-IE instances
self.children = kwargs.get('children', [])
self.encoded = None
@@ -340,7 +348,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
else:
raise TypeError
def from_bytes(self, binary: bytes) -> List[TLV_IE]:
def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
"""Create a list of TLV_IEs from the collection based on binary input data.
Args:
binary : binary bytes of encoded data
@@ -354,15 +362,16 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
first = next(iter(self.members_by_tag.values()))
# iterate until no binary trailer is left
while len(remainder):
context['siblings'] = res
# obtain the tag at the start of the remainder
tag, r = first._parse_tag_raw(remainder)
if tag == None:
return res
tag, _r = first._parse_tag_raw(remainder)
if tag is None:
break
if tag in self.members_by_tag:
cls = self.members_by_tag[tag]
# create an instance and parse accordingly
inst = cls()
dec, remainder = inst.from_tlv(remainder)
_dec, remainder = inst.from_tlv(remainder, context=context)
res.append(inst)
else:
# unknown tag; create the related class on-the-fly using the same base class
@@ -373,7 +382,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
# create an instance and parse accordingly
inst = cls()
dec, remainder = inst.from_tlv(remainder)
_dec, remainder = inst.from_tlv(remainder, context=context)
res.append(inst)
self.children = res
return res
@@ -383,33 +392,45 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
of dicts, where they key indicates the name of the TLV_IE subclass to use."""
# list of instances of TLV_IE collection member classes appearing in the data
res = []
# iterate over members of the list passed into "decoded"
for i in decoded:
# iterate over all the keys (typically one!) within the current list item dict
for k in i.keys():
# check if we have a member identified by the dict key
if k in self.members_by_name:
# resolve the class for that name; create an instance of it
cls = self.members_by_name[k]
inst = cls()
inst.from_dict(i[k])
if cls.nested_collection_cls:
# in case of collections, we want to pass the raw "value" portion to from_dict,
# as to_dict() below intentionally omits the collection-class-name as key
inst.from_dict(i[k])
else:
inst.from_dict({k: i[k]})
res.append(inst)
else:
raise ValueError('%s: Unknown TLV Class %s in %s; expected %s' %
(self, i[0], decoded, self.members_by_name.keys()))
(self, k, decoded, self.members_by_name.keys()))
self.children = res
return res
def to_dict(self):
# we intentionally return not a dict, but a list of dicts. We could prefix by
# self.__class__.__name__, but that is usually some meaningless auto-generated collection name.
return [x.to_dict() for x in self.children]
def to_bytes(self):
def to_bytes(self, context: dict = {}):
out = b''
context['siblings'] = self.children
for c in self.children:
out += c.to_tlv()
out += c.to_tlv(context=context)
return out
def from_tlv(self, do):
return self.from_bytes(do)
def from_tlv(self, do, context: dict = {}):
return self.from_bytes(do, context=context)
def to_tlv(self):
return self.to_bytes()
def to_tlv(self, context: dict = {}):
return self.to_bytes(context=context)
def flatten_dict_lists(inp):
@@ -421,8 +442,12 @@ def flatten_dict_lists(inp):
return False
return True
def are_elements_unique(lod):
set_of_keys = {list(x.keys())[0] for x in lod}
return len(lod) == len(set_of_keys)
if isinstance(inp, list):
if are_all_elements_dict(inp):
if are_all_elements_dict(inp) and are_elements_unique(inp):
# flatten into one shared dict
newdict = {}
for e in inp:
@@ -430,10 +455,10 @@ def flatten_dict_lists(inp):
newdict[key] = e[key]
inp = newdict
# process result as any native dict
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
return {k:flatten_dict_lists(v) for k,v in inp.items()}
else:
return [flatten_dict_lists(x) for x in inp]
elif isinstance(inp, dict):
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
return {k:flatten_dict_lists(v) for k,v in inp.items()}
else:
return inp

View File

@@ -3,18 +3,19 @@
""" pySim: PCSC reader transport link base
"""
import os
import abc
import argparse
from typing import Optional, Tuple
from construct import Construct
from pySim.exceptions import *
from pySim.construct import filter_dict
from pySim.utils import sw_match, b2h, h2b, i2h, Hexstr
from pySim.utils import sw_match, b2h, h2b, i2h, Hexstr, SwHexstr, SwMatchstr, ResTuple
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2021-2022 Harald Welte <laforge@osmocom.org>
# Copyright (C) 2021-2023 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
@@ -60,14 +61,18 @@ class ProactiveHandler(abc.ABC):
class LinkBase(abc.ABC):
"""Base class for link/transport to card."""
def __init__(self, sw_interpreter=None, apdu_tracer=None,
def __init__(self, sw_interpreter=None, apdu_tracer: Optional[ApduTracer]=None,
proactive_handler: Optional[ProactiveHandler]=None):
self.sw_interpreter = sw_interpreter
self.apdu_tracer = apdu_tracer
self.proactive_handler = proactive_handler
@abc.abstractmethod
def _send_apdu_raw(self, pdu: str) -> Tuple[str, str]:
def __str__(self) -> str:
"""Implementation specific method for printing an information to identify the device."""
@abc.abstractmethod
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the PDU."""
def set_sw_interpreter(self, interp):
@@ -75,7 +80,7 @@ class LinkBase(abc.ABC):
self.sw_interpreter = interp
@abc.abstractmethod
def wait_for_card(self, timeout: int = None, newcardonly: bool = False):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
"""Wait for a card and connect to it
Args:
@@ -98,7 +103,7 @@ class LinkBase(abc.ABC):
"""Resets the card (power down/up)
"""
def send_apdu_raw(self, pdu: str):
def send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU with minimal processing
Args:
@@ -115,7 +120,7 @@ class LinkBase(abc.ABC):
self.apdu_tracer.trace_response(pdu, sw, data)
return (data, sw)
def send_apdu(self, pdu):
def send_apdu(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
@@ -131,12 +136,13 @@ class LinkBase(abc.ABC):
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if (sw is not None):
if ((sw[0:2] == '9f') or (sw[0:2] == '61')):
if sw is not None:
while ((sw[0:2] == '9f') or (sw[0:2] == '61')):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
data, sw = self.send_apdu_raw(pdu_gr)
d, sw = self.send_apdu_raw(pdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
pdu_gr = pdu[0:8] + sw[2:4]
@@ -144,7 +150,7 @@ class LinkBase(abc.ABC):
return data, sw
def send_apdu_checksw(self, pdu, sw="9000"):
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW
Args:
@@ -212,108 +218,42 @@ class LinkBase(abc.ABC):
raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter)
return rv
def send_apdu_constr(self, cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr):
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
cla : string (in hex) ISO 7816 class byte
ins : string (in hex) ISO 7816 instruction byte
p1 : string (in hex) ISO 7116 Parameter 1 byte
p2 : string (in hex) ISO 7116 Parameter 2 byte
cmd_cosntr : defining how to generate binary APDU command data
cmd_data : command data passed to cmd_constr
resp_cosntr : defining how to decode binary APDU response data
Returns:
Tuple of (decoded_data, sw)
"""
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
p3 = i2h([len(cmd)])
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
(data, sw) = self.send_apdu(pdu)
if data:
# filter the resulting dict to avoid '_io' members inside
rsp = filter_dict(resp_constr.parse(h2b(data)))
else:
rsp = None
return (rsp, sw)
def send_apdu_constr_checksw(self, cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr,
sw_exp="9000"):
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
cla : string (in hex) ISO 7816 class byte
ins : string (in hex) ISO 7816 instruction byte
p1 : string (in hex) ISO 7116 Parameter 1 byte
p2 : string (in hex) ISO 7116 Parameter 2 byte
cmd_cosntr : defining how to generate binary APDU command data
cmd_data : command data passed to cmd_constr
resp_cosntr : defining how to decode binary APDU response data
exp_sw : string (in hex) of status word (ex. "9000")
Returns:
Tuple of (decoded_data, sw)
"""
(rsp, sw) = self.send_apdu_constr(cla, ins,
p1, p2, cmd_constr, cmd_data, resp_constr)
if not sw_match(sw, sw_exp):
raise SwMatchError(sw, sw_exp.lower(), self.sw_interpreter)
return (rsp, sw)
def argparse_add_reader_args(arg_parser):
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
"""Add all reader related arguments to the given argparse.Argumentparser instance."""
serial_group = arg_parser.add_argument_group('Serial Reader')
serial_group.add_argument('-d', '--device', metavar='DEV', default='/dev/ttyUSB0',
help='Serial Device for SIM access')
serial_group.add_argument('-b', '--baud', dest='baudrate', type=int, metavar='BAUD', default=9600,
help='Baud rate used for SIM access')
from pySim.transport.serial import SerialSimLink
from pySim.transport.pcsc import PcscSimLink
from pySim.transport.modem_atcmd import ModemATCommandLink
from pySim.transport.calypso import CalypsoSimLink
pcsc_group = arg_parser.add_argument_group('PC/SC Reader')
pcsc_group.add_argument('-p', '--pcsc-device', type=int, dest='pcsc_dev', metavar='PCSC', default=None,
help='PC/SC reader number to use for SIM access')
modem_group = arg_parser.add_argument_group('AT Command Modem Reader')
modem_group.add_argument('--modem-device', dest='modem_dev', metavar='DEV', default=None,
help='Serial port of modem for Generic SIM Access (3GPP TS 27.007)')
modem_group.add_argument('--modem-baud', type=int, metavar='BAUD', default=115200,
help='Baud rate used for modem port')
osmobb_group = arg_parser.add_argument_group('OsmocomBB Reader')
osmobb_group.add_argument('--osmocon', dest='osmocon_sock', metavar='PATH', default=None,
help='Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)')
SerialSimLink.argparse_add_reader_args(arg_parser)
PcscSimLink.argparse_add_reader_args(arg_parser)
ModemATCommandLink.argparse_add_reader_args(arg_parser)
CalypsoSimLink.argparse_add_reader_args(arg_parser)
return arg_parser
def init_reader(opts, **kwargs) -> Optional[LinkBase]:
def init_reader(opts, **kwargs) -> LinkBase:
"""
Init card reader driver
"""
sl = None # type : :Optional[LinkBase]
try:
if opts.pcsc_dev is not None:
print("Using PC/SC reader interface")
from pySim.transport.pcsc import PcscSimLink
sl = PcscSimLink(opts.pcsc_dev, **kwargs)
elif opts.osmocon_sock is not None:
print("Using Calypso-based (OsmocomBB) reader interface")
from pySim.transport.calypso import CalypsoSimLink
sl = CalypsoSimLink(sock_path=opts.osmocon_sock, **kwargs)
elif opts.modem_dev is not None:
print("Using modem for Generic SIM Access (3GPP TS 27.007)")
from pySim.transport.modem_atcmd import ModemATCommandLink
sl = ModemATCommandLink(
device=opts.modem_dev, baudrate=opts.modem_baud, **kwargs)
else: # Serial reader is default
print("Using serial reader interface")
from pySim.transport.serial import SerialSimLink
sl = SerialSimLink(device=opts.device,
baudrate=opts.baudrate, **kwargs)
return sl
except Exception as e:
if str(e):
print("Card reader initialization failed with exception:\n" + str(e))
else:
print(
"Card reader initialization failed with an exception of type:\n" + str(type(e)))
return None
if opts.pcsc_dev is not None or opts.pcsc_regex is not None:
from pySim.transport.pcsc import PcscSimLink
sl = PcscSimLink(opts, **kwargs)
elif opts.osmocon_sock is not None:
from pySim.transport.calypso import CalypsoSimLink
sl = CalypsoSimLink(opts, **kwargs)
elif opts.modem_dev is not None:
from pySim.transport.modem_atcmd import ModemATCommandLink
sl = ModemATCommandLink(opts, **kwargs)
else: # Serial reader is default
print("No reader/driver specified; falling back to default (Serial reader)")
from pySim.transport.serial import SerialSimLink
sl = SerialSimLink(opts, **kwargs)
if os.environ.get('PYSIM_INTEGRATION_TEST') == "1":
print("Using %s reader interface" % (sl.name))
else:
print("Using reader %s" % sl)
return sl

View File

@@ -20,10 +20,12 @@ import select
import struct
import socket
import os
import argparse
from typing import Optional
from pySim.transport import LinkBase
from pySim.exceptions import *
from pySim.utils import h2b, b2h
from pySim.exceptions import ReaderError, ProtocolError
from pySim.utils import h2b, b2h, Hexstr, ResTuple
class L1CTLMessage:
@@ -56,9 +58,9 @@ class L1CTLMessageReset(L1CTLMessage):
L1CTL_RES_T_FULL = 0x01
L1CTL_RES_T_SCHED = 0x02
def __init__(self, type=L1CTL_RES_T_FULL):
super(L1CTLMessageReset, self).__init__(self.L1CTL_RESET_REQ)
self.data += struct.pack("Bxxx", type)
def __init__(self, ttype=L1CTL_RES_T_FULL):
super().__init__(self.L1CTL_RESET_REQ)
self.data += struct.pack("Bxxx", ttype)
class L1CTLMessageSIM(L1CTLMessage):
@@ -68,14 +70,16 @@ class L1CTLMessageSIM(L1CTLMessage):
L1CTL_SIM_CONF = 0x17
def __init__(self, pdu):
super(L1CTLMessageSIM, self).__init__(self.L1CTL_SIM_REQ)
super().__init__(self.L1CTL_SIM_REQ)
self.data += pdu
class CalypsoSimLink(LinkBase):
"""Transport Link for Calypso based phones."""
name = 'Calypso-based (OsmocomBB) reader'
def __init__(self, sock_path: str = "/tmp/osmocom_l2", **kwargs):
def __init__(self, opts: argparse.Namespace = argparse.Namespace(osmocon_sock="/tmp/osmocom_l2"), **kwargs):
sock_path = opts.osmocon_sock
super().__init__(**kwargs)
# Make sure that a given socket path exists
if not os.path.exists(sock_path):
@@ -88,10 +92,13 @@ class CalypsoSimLink(LinkBase):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(sock_path)
# Remember socket path
self._sock_path = sock_path
def __del__(self):
self.sock.close()
def wait_for_rsp(self, exp_len=128):
def wait_for_rsp(self, exp_len: int = 128):
# Wait for incoming data (timeout is 3 seconds)
s, _, _ = select.select([self.sock], [], [], 3.0)
if not s:
@@ -118,10 +125,10 @@ class CalypsoSimLink(LinkBase):
def disconnect(self):
pass # Nothing to do really ...
def wait_for_card(self, timeout=None, newcardonly=False):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ...
def _send_apdu_raw(self, pdu):
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
# Request FULL reset
req_msg = L1CTLMessageSIM(h2b(pdu))
@@ -154,3 +161,15 @@ class CalypsoSimLink(LinkBase):
sw = rsp[-2:]
return b2h(data), b2h(sw)
def __str__(self) -> str:
return "osmocon:%s" % (self._sock_path)
@staticmethod
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
osmobb_group = arg_parser.add_argument_group('OsmocomBB Reader', """Use an OsmocomBB compatible phone
to access the SIM inserted to the phone SIM slot. This will require you to run the OsmocomBB firmware inside
the phone (can be ram-loaded). It also requires that you run the ``osmocon`` program, which provides a unix
domain socket to which this reader driver can attach.""")
osmobb_group.add_argument('--osmocon', dest='osmocon_sock', metavar='PATH', default=None,
help='Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)')

View File

@@ -17,12 +17,15 @@
#
import logging as log
import serial
import time
import re
import argparse
from typing import Optional
import serial
from pySim.utils import Hexstr, ResTuple
from pySim.transport import LinkBase
from pySim.exceptions import *
from pySim.exceptions import ReaderError, ProtocolError
# HACK: if somebody needs to debug this thing
# log.root.setLevel(log.DEBUG)
@@ -30,8 +33,12 @@ from pySim.exceptions import *
class ModemATCommandLink(LinkBase):
"""Transport Link for 3GPP TS 27.007 compliant modems."""
name = "modem for Generic SIM Access (3GPP TS 27.007)"
def __init__(self, device: str = '/dev/ttyUSB0', baudrate: int = 115200, **kwargs):
def __init__(self, opts: argparse.Namespace = argparse.Namespace(modem_dev='/dev/ttyUSB0',
modem_baud=115200), **kwargs):
device = opts.modem_dev
baudrate = opts.modem_baud
super().__init__(**kwargs)
self._sl = serial.Serial(device, baudrate, timeout=5)
self._echo = False # this will be auto-detected by _check_echo()
@@ -50,7 +57,7 @@ class ModemATCommandLink(LinkBase):
def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
# Convert from string to bytes, if needed
bcmd = cmd if type(cmd) is bytes else cmd.encode()
bcmd = cmd if isinstance(cmd, bytes) else cmd.encode()
bcmd += b'\r'
# Clean input buffer from previous/unexpected data
@@ -60,9 +67,9 @@ class ModemATCommandLink(LinkBase):
log.debug('Sending AT command: %s', cmd)
try:
wlen = self._sl.write(bcmd)
assert(wlen == len(bcmd))
except:
raise ReaderError('Failed to send AT command: %s' % cmd)
assert wlen == len(bcmd)
except Exception as exc:
raise ReaderError('Failed to send AT command: %s' % cmd) from exc
rsp = b''
its = 1
@@ -84,8 +91,7 @@ class ModemATCommandLink(LinkBase):
break
time.sleep(patience)
its += 1
log.debug('Command took %0.6fs (%d cycles a %fs)',
time.time() - t_start, its, patience)
log.debug('Command took %0.6fs (%d cycles a %fs)', time.time() - t_start, its, patience)
if self._echo:
# Skip echo chars
@@ -113,11 +119,10 @@ class ModemATCommandLink(LinkBase):
if result[-1] == b'OK':
self._echo = False
return
elif result[-1] == b'AT\r\r\nOK':
if result[-1] == b'AT\r\r\nOK':
self._echo = True
return
raise ReaderError(
'Interface \'%s\' does not respond to \'AT\' command' % self._device)
raise ReaderError('Interface \'%s\' does not respond to \'AT\' command' % self._device)
def reset_card(self):
# Reset the modem, just to be sure
@@ -128,7 +133,7 @@ class ModemATCommandLink(LinkBase):
if self.send_at_cmd('AT+CSIM=?') != [b'OK']:
raise ReaderError('The modem does not seem to support SIM access')
log.info('Modem at \'%s\' is ready!' % self._device)
log.info('Modem at \'%s\' is ready!', self._device)
def connect(self):
pass # Nothing to do really ...
@@ -136,10 +141,10 @@ class ModemATCommandLink(LinkBase):
def disconnect(self):
pass # Nothing to do really ...
def wait_for_card(self, timeout=None, newcardonly=False):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ...
def _send_apdu_raw(self, pdu):
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
# Make sure pdu has upper case hex digits [A-F]
pdu = pdu.upper()
@@ -148,8 +153,9 @@ class ModemATCommandLink(LinkBase):
log.debug('Sending command: %s', cmd)
# Send AT+CSIM command to the modem
# TODO: also handle +CME ERROR: <err>
rsp = self.send_at_cmd(cmd)
if rsp[-1].startswith(b'+CME ERROR:'):
raise ProtocolError('AT+CSIM failed with: %s' % str(rsp))
if len(rsp) != 2 or rsp[-1] != b'OK':
raise ReaderError('APDU transfer failed: %s' % str(rsp))
rsp = rsp[0] # Get rid of b'OK'
@@ -157,12 +163,25 @@ class ModemATCommandLink(LinkBase):
# Make sure that the response has format: b'+CSIM: %d,\"%s\"'
try:
result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
(rsp_pdu_len, rsp_pdu) = result.groups()
except:
raise ReaderError('Failed to parse response from modem: %s' % rsp)
(_rsp_pdu_len, rsp_pdu) = result.groups()
except Exception as exc:
raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc
# TODO: make sure we have at least SW
data = rsp_pdu[:-4].decode().lower()
sw = rsp_pdu[-4:].decode().lower()
log.debug('Command response: %s, %s', data, sw)
return data, sw
def __str__(self) -> str:
return "modem:%s" % self._device
@staticmethod
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
modem_group = arg_parser.add_argument_group('AT Command Modem Reader', """Talk to a SIM Card inside a
mobile phone or cellular modem which is attached to this computer and offers an AT command interface including
the AT+CSIM interface for Generic SIM access as specified in 3GPP TS 27.007.""")
modem_group.add_argument('--modem-device', dest='modem_dev', metavar='DEV', default=None,
help='Serial port of modem for Generic SIM Access (3GPP TS 27.007)')
modem_group.add_argument('--modem-baud', type=int, metavar='BAUD', default=115200,
help='Baud rate used for modem port')

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2010 Harald Welte <laforge@gnumonks.org>
# Copyright (C) 2010-2023 Harald Welte <laforge@gnumonks.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
@@ -17,25 +17,44 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import re
from typing import Optional
from smartcard.CardConnection import CardConnection
from smartcard.CardRequest import CardRequest
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException, CardConnectionException
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException
from smartcard.System import readers
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
from pySim.transport import LinkBase
from pySim.utils import h2i, i2h
from pySim.utils import h2i, i2h, Hexstr, ResTuple
class PcscSimLink(LinkBase):
""" pySim: PCSC reader transport link."""
name = 'PC/SC'
def __init__(self, reader_number: int = 0, **kwargs):
def __init__(self, opts: argparse.Namespace = argparse.Namespace(pcsc_dev=0), **kwargs):
super().__init__(**kwargs)
self._reader = None
r = readers()
if reader_number >= len(r):
raise ReaderError('No reader found for number %d' % reader_number)
self._reader = r[reader_number]
if opts.pcsc_dev is not None:
# actual reader index number (integer)
reader_number = opts.pcsc_dev
if reader_number >= len(r):
raise ReaderError('No reader found for number %d' % reader_number)
self._reader = r[reader_number]
else:
# reader regex string
cre = re.compile(opts.pcsc_regex)
for reader in r:
if cre.search(reader.name):
self._reader = reader
break
if not self._reader:
raise ReaderError('No matching reader found for regex %s' % opts.pcsc_regex)
self._con = self._reader.createConnection()
def __del__(self):
@@ -44,15 +63,14 @@ class PcscSimLink(LinkBase):
self._con.disconnect()
except:
pass
return
def wait_for_card(self, timeout: int = None, newcardonly: bool = False):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
cr = CardRequest(readers=[self._reader],
timeout=timeout, newcardonly=newcardonly)
try:
cr.waitforcard()
except CardRequestTimeoutException:
raise NoCardError()
except CardRequestTimeoutException as exc:
raise NoCardError() from exc
self.connect()
def connect(self):
@@ -63,12 +81,12 @@ class PcscSimLink(LinkBase):
# Explicitly select T=0 communication protocol
self._con.connect(CardConnection.T0_protocol)
except CardConnectionException:
raise ProtocolError()
except NoCardException:
raise NoCardError()
except CardConnectionException as exc:
raise ProtocolError() from exc
except NoCardException as exc:
raise NoCardError() from exc
def get_atr(self):
def get_atr(self) -> Hexstr:
return self._con.getATR()
def disconnect(self):
@@ -79,7 +97,7 @@ class PcscSimLink(LinkBase):
self.connect()
return 1
def _send_apdu_raw(self, pdu):
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
apdu = h2i(pdu)
@@ -89,3 +107,20 @@ class PcscSimLink(LinkBase):
# Return value
return i2h(data), i2h(sw)
def __str__(self) -> str:
return "PCSC[%s]" % (self._reader)
@staticmethod
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
pcsc_group = arg_parser.add_argument_group('PC/SC Reader',
"""Use a PC/SC card reader to talk to the SIM card. PC/SC is a standard API for how applications
access smart card readers, and is available on a variety of operating systems, such as Microsoft
Windows, MacOS X and Linux. Most vendors of smart card readers provide drivers that offer a PC/SC
interface, if not even a generic USB CCID driver is used. You can use a tool like ``pcsc_scan -r``
to obtain a list of readers available on your system. """)
dev_group = pcsc_group.add_mutually_exclusive_group()
dev_group.add_argument('-p', '--pcsc-device', type=int, dest='pcsc_dev', metavar='PCSC', default=None,
help='Number of PC/SC reader to use for SIM access')
dev_group.add_argument('--pcsc-regex', type=str, dest='pcsc_regex', metavar='REGEX', default=None,
help='Regex matching PC/SC reader to use for SIM access')

View File

@@ -16,42 +16,45 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import serial
import time
import os.path
import os
import argparse
from typing import Optional
import serial
from pySim.exceptions import NoCardError, ProtocolError
from pySim.transport import LinkBase
from pySim.utils import h2b, b2h
from pySim.utils import h2b, b2h, Hexstr, ResTuple
class SerialSimLink(LinkBase):
""" pySim: Transport Link for serial (RS232) based readers included with simcard"""
name = 'Serial'
def __init__(self, device: str = '/dev/ttyUSB0', baudrate: int = 9600, rst: str = '-rts',
def __init__(self, opts = argparse.Namespace(device='/dev/ttyUSB0', baudrate=9600), rst: str = '-rts',
debug: bool = False, **kwargs):
super().__init__(**kwargs)
if not os.path.exists(device):
raise ValueError("device file %s does not exist -- abort" % device)
if not os.path.exists(opts.device):
raise ValueError("device file %s does not exist -- abort" % opts.device)
self._sl = serial.Serial(
port=device,
port=opts.device,
parity=serial.PARITY_EVEN,
bytesize=serial.EIGHTBITS,
stopbits=serial.STOPBITS_TWO,
timeout=1,
xonxoff=0,
rtscts=0,
baudrate=baudrate,
baudrate=opts.baudrate,
)
self._rst_pin = rst
self._debug = debug
self._atr = None
def __del__(self):
if (hasattr(self, "_sl")):
if hasattr(self, "_sl"):
self._sl.close()
def wait_for_card(self, timeout=None, newcardonly=False):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
# Direct try
existing = False
@@ -59,8 +62,7 @@ class SerialSimLink(LinkBase):
self.reset_card()
if not newcardonly:
return
else:
existing = True
existing = True
except NoCardError:
pass
@@ -83,7 +85,7 @@ class SerialSimLink(LinkBase):
# Tolerate a couple of protocol error ... can happen if
# we try when the card is 'half' inserted
pe += 1
if (pe > 2):
if pe > 2:
raise
# Timed out ...
@@ -92,7 +94,7 @@ class SerialSimLink(LinkBase):
def connect(self):
self.reset_card()
def get_atr(self):
def get_atr(self) -> Hexstr:
return self._atr
def disconnect(self):
@@ -102,8 +104,9 @@ class SerialSimLink(LinkBase):
rv = self._reset_card()
if rv == 0:
raise NoCardError()
elif rv < 0:
if rv < 0:
raise ProtocolError()
return rv
def _reset_card(self):
self._atr = None
@@ -116,8 +119,8 @@ class SerialSimLink(LinkBase):
try:
rst_meth = rst_meth_map[self._rst_pin[1:]]
rst_val = rst_val_map[self._rst_pin[0]]
except:
raise ValueError('Invalid reset pin %s' % self._rst_pin)
except Exception as exc:
raise ValueError('Invalid reset pin %s' % self._rst_pin) from exc
rst_meth(rst_val)
time.sleep(0.1) # 100 ms
@@ -183,7 +186,7 @@ class SerialSimLink(LinkBase):
def _rx_byte(self):
return self._sl.read()
def _send_apdu_raw(self, pdu):
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
pdu = h2b(pdu)
data_len = pdu[4] # P3
@@ -199,7 +202,7 @@ class SerialSimLink(LinkBase):
b = self._rx_byte()
if ord(b) == pdu[1]:
break
elif b != '\x60':
if b != '\x60':
# Ok, it 'could' be SW1
sw1 = b
sw2 = self._rx_byte()
@@ -218,7 +221,7 @@ class SerialSimLink(LinkBase):
to_recv = data_len - len(pdu) + 5 + 2
data = bytes(0)
while (len(data) < to_recv):
while len(data) < to_recv:
b = self._rx_byte()
if (to_recv == 2) and (b == '\x60'): # Ignore NIL if we have no RX data (hack ?)
continue
@@ -234,3 +237,16 @@ class SerialSimLink(LinkBase):
# Return value
return b2h(data), b2h(sw)
def __str__(self) -> str:
return "serial:%s" % (self._sl.name)
@staticmethod
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
serial_group = arg_parser.add_argument_group('Serial Reader', """Use a simple/ultra-low-cost serial reader
attached to a (physical or USB/virtual) RS232 port. This doesn't work with all RS232-attached smart card
readers, only with the very primitive readers following the ancient `Phoenix` or `Smart Mouse` design.""")
serial_group.add_argument('-d', '--device', metavar='DEV', default='/dev/ttyUSB0',
help='Serial Device for SIM access')
serial_group.add_argument('-b', '--baud', dest='baudrate', type=int, metavar='BAUD', default=9600,
help='Baud rate used for SIM access')

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""Utilities / Functions related to ETSI TS 102 221, the core UICC spec.
(C) 2021 by Harald Welte <laforge@osmocom.org>
(C) 2021-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
@@ -16,22 +16,24 @@ 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/>.
"""
from bidict import bidict
from construct import *
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
from construct import Optional as COptional
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from bidict import bidict
from pySim.profile import CardProfile
from pySim.profile import match_uicc
from pySim.profile import match_sim
import pySim.iso7816_4 as iso7816_4
from pySim import iso7816_4
# A UICC will usually also support 2G functionality. If this is the case, we
# need to add DF_GSM and DF_TELECOM along with the UICC related files
from pySim.ts_51_011 import DF_GSM, DF_TELECOM
from pySim.ts_51_011 import AddonSIM
from pySim.gsm_r import AddonGSMR
from pySim.cdma_ruim import AddonRUIM
ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
# TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte"
@@ -78,6 +80,10 @@ ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
CardCommand('RESIZE FILE', 0xD4, ['8X', 'CX']),
])
# ETSI TS 102 221 6.2.1
SupplyVoltageClasses = FlagsEnum(Int8ub, a=0x1, b=0x2, c=0x4, d=0x8, e=0x10)
# ETSI TS 102 221 11.1.1.4.2
class FileSize(BER_TLV_IE, tag=0x80):
_construct = GreedyInteger(minlen=2)
@@ -88,14 +94,24 @@ class TotalFileSize(BER_TLV_IE, tag=0x81):
# ETSI TS 102 221 11.1.1.4.3
class FileDescriptor(BER_TLV_IE, tag=0x82):
_test_decode = [
# FIXME: this doesn't work as _encode test for some strange reason.
( '82027921', { "file_descriptor_byte": { "shareable": True, "structure": "ber_tlv" }, "record_len": None, "num_of_rec": None } ),
]
_test_de_encode = [
( '82027821', { "file_descriptor_byte": { "shareable": True, "file_type": "df", "structure": "no_info_given" }, "record_len": None, "num_of_rec": None }),
( '82024121', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "transparent" }, "record_len": None, "num_of_rec": None } ),
( '82054221006e05', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "linear_fixed" }, "record_len": 110, "num_of_rec": 5 } ),
]
class BerTlvAdapter(Adapter):
def _parse(self, obj, context, path):
if obj == 0x39:
data = obj.read()
if data == b'\x01\x01\x01\x00\x00\x01':
return 'ber_tlv'
raise ValidationError
def _build(self, obj, context, path):
if obj == 'ber_tlv':
return 0x39
return b'\x01\x01\x01\x00\x00\x01'
raise ValidationError
FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6)))),
@@ -119,7 +135,7 @@ class UiccCharacteristics(BER_TLV_IE, tag=0x80):
# ETSI TS 102 221 11.1.1.4.6.2
class ApplicationPowerConsumption(BER_TLV_IE, tag=0x81):
_construct = Struct('voltage_class'/Int8ub,
_construct = Struct('voltage_class'/SupplyVoltageClasses,
'power_consumption_ma'/Int8ub,
'reference_freq_100k'/Int8ub)
@@ -198,6 +214,9 @@ class ShortFileIdentifier(BER_TLV_IE, tag=0x88):
# ETSI TS 102 221 11.1.1.4.9
class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A):
_test_de_encode = [
( '8a0105', 'operational_activated' ),
]
def _from_bytes(self, do: bytes):
lcsi = int.from_bytes(do, 'big')
if lcsi == 0x00:
@@ -219,20 +238,19 @@ class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A):
def _to_bytes(self):
if self.decoded == 'no_information':
return b'\x00'
elif self.decoded == 'creation':
if self.decoded == 'creation':
return b'\x01'
elif self.decoded == 'initialization':
if self.decoded == 'initialization':
return b'\x03'
elif self.decoded == 'operational_activated':
if self.decoded == 'operational_activated':
return b'\x05'
elif self.decoded == 'operational_deactivated':
if self.decoded == 'operational_deactivated':
return b'\x04'
elif self.decoded == 'termination':
if self.decoded == 'termination':
return b'\x0c'
elif isinstance(self.decoded, int):
if isinstance(self.decoded, int):
return self.decoded.to_bytes(1, 'big')
else:
raise ValueError
raise ValueError
# ETSI TS 102 221 11.1.1.4.9
class PS_DO(BER_TLV_IE, tag=0x90):
@@ -269,6 +287,33 @@ def tlv_val_interpret(inmap, indata):
return val
return {d[0]: newval(inmap, d[0], d[1]) for d in indata.items()}
# TS 102 221 11.1.19.2.1
class TerminalPowerSupply(BER_TLV_IE, tag=0x80):
_construct = Struct('used_supply_voltage_class'/SupplyVoltageClasses,
'maximum_available_power_supply'/Int8ub,
'actual_used_freq_100k'/Int8ub)
# TS 102 221 11.1.19.2.2
class ExtendedLchanTerminalSupport(BER_TLV_IE, tag=0x81):
_construct = GreedyBytes
# TS 102 221 11.1.19.2.3
class AdditionalInterfacesSupport(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Int8ub, uicc_clf=0x01)
# TS 102 221 11.1.19.2.4 + SGP.32 v3.0 3.4.2 RSP Device Capabilities
class AdditionalTermCapEuicc(BER_TLV_IE, tag=0x83):
_construct = FlagsEnum(Int8ub, lui_d=0x01, lpd_d=0x02, lds_d=0x04, lui_e_scws=0x08,
metadata_update_alerting=0x10,
enterprise_capable_device=0x20,
lui_e_e4e=0x40,
lpr=0x80)
# TS 102 221 11.1.19.2.0
class TerminalCapability(BER_TLV_IE, tag=0xa9, nested=[TerminalPowerSupply, ExtendedLchanTerminalSupport,
AdditionalInterfacesSupport, AdditionalTermCapEuicc]):
pass
# ETSI TS 102 221 Section 9.2.7 + ISO7816-4 9.3.3/9.3.4
class _AM_DO_DF(DataObject):
def __init__(self):
@@ -466,12 +511,11 @@ class CRT_DO(DataObject):
def from_bytes(self, do: bytes):
"""Decode a Control Reference Template DO."""
if len(do) != 6:
raise ValueError('Unsupported CRT DO length: %s', do)
raise ValueError('Unsupported CRT DO length: %s' %do)
if do[0] != 0x83 or do[1] != 0x01:
raise ValueError('Unsupported Key Ref Tag or Len in CRT DO %s', do)
raise ValueError('Unsupported Key Ref Tag or Len in CRT DO %s' % do)
if do[3:] != b'\x95\x01\x08':
raise ValueError(
'Unsupported Usage Qualifier Tag or Len in CRT DO %s', do)
raise ValueError('Unsupported Usage Qualifier Tag or Len in CRT DO %s' % do)
self.encoded = do[0:6]
self.decoded = pin_names[do[2]]
return do[6:]
@@ -505,7 +549,7 @@ class SecCondByte_DO(DataObject):
if inb & 0x10:
res.append('user_auth')
rd = {'mode': cond}
if len(res):
if len(res) > 0:
rd['conditions'] = res
self.decoded = rd
@@ -574,6 +618,18 @@ SC_DO = DataObjectChoice('security_condition', 'Security Condition',
# TS 102 221 Section 13.1
class EF_DIR(LinFixedEF):
# FIXME: re-encode failure when changing to _test_de_encode
_test_decode = [
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
{ "application_label": "USim1" },
{ "discretionary_template": h2b("a00c80011781025f608203454150") } ] }
),
( '61194f10a0000000871004ffffffff890709000050054953696d31',
{ "application_template": [ { "application_id": h2b("a0000000871004ffffffff8907090000") },
{ "application_label": "ISim1" } ] }
),
]
class ApplicationLabel(BER_TLV_IE, tag=0x50):
# TODO: UCS-2 coding option as per Annex A of TS 102 221
_construct = GreedyString('ascii')
@@ -593,6 +649,9 @@ class EF_DIR(LinFixedEF):
# TS 102 221 Section 13.2
class EF_ICCID(TransparentEF):
_test_de_encode = [
( '988812010000400310f0', { "iccid": "8988211000000430010" } ),
]
def __init__(self, fid='2fe2', sfid=0x02, name='EF.ICCID', desc='ICC Identification'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(10, 10))
@@ -604,17 +663,23 @@ class EF_ICCID(TransparentEF):
# TS 102 221 Section 13.3
class EF_PL(TransRecEF):
_test_de_encode = [
( '6465', "de" ),
( '656e', "en" ),
( 'ffff', None ),
]
def __init__(self, fid='2f05', sfid=0x05, name='EF.PL', desc='Preferred Languages'):
super().__init__(fid, sfid=sfid, name=name,
desc=desc, rec_len=2, size=(2, None))
def _decode_record_bin(self, bin_data):
def _decode_record_bin(self, bin_data, **kwargs):
if bin_data == b'\xff\xff':
return None
else:
return bin_data.decode('ascii')
def _encode_record_bin(self, in_json):
def _encode_record_bin(self, in_json, **kwargs):
if in_json is None:
return b'\xff\xff'
else:
@@ -623,6 +688,28 @@ class EF_PL(TransRecEF):
# TS 102 221 Section 13.4
class EF_ARR(LinFixedEF):
_test_de_encode = [
( '800101a40683010a950108800106900080016097008401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "control_reference_template": "ADM1" } ],
[ { "access_mode": [ "write_append", "update_erase" ] },
{ "always": None } ],
[ { "access_mode": [ "delete_file", "terminate_ef" ] },
{ "never": None } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
( '80010190008001029700800118a40683010a9501088401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "always": None } ],
[ { "access_mode": [ "update_erase" ] },
{ "never": None } ],
[ { "access_mode": [ "activate_file_or_record", "deactivate_file_or_record" ] },
{ "control_reference_template": "ADM1" } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
]
def __init__(self, fid='2f06', sfid=0x06, name='EF.ARR', desc='Access Rule Reference'):
super().__init__(fid, sfid=sfid, name=name, desc=desc)
# add those commands to the general commands of a TransparentEF
@@ -665,7 +752,7 @@ class EF_ARR(LinFixedEF):
raise ValueError
return by_mode
def _decode_record_bin(self, raw_bin_data):
def _decode_record_bin(self, raw_bin_data, **_kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
dec = arr_seq.decode_multi(raw_bin_data)
@@ -673,20 +760,17 @@ class EF_ARR(LinFixedEF):
# 'un-flattening' decoder, and hence would be unable to encode :(
return dec[0]
def _encode_record_bin(self, in_json):
def _encode_record_bin(self, in_json, **_kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
return arr_seq.encode_multi(in_json)
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
def __init__(self):
super().__init__()
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
def do_read_arr_record(self, opts):
"""Read one EF.ARR record in flattened, human-friendly form."""
(data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
(data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
data = self._cmd.lchan.selected_file.flatten(data)
self._cmd.poutput_json(data, opts.oneline)
@@ -697,7 +781,7 @@ class EF_ARR(LinFixedEF):
# collect all results in list so they are rendered as JSON list when printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
(data, sw) = self._cmd.lchan.read_record_dec(recnr)
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
data = self._cmd.lchan.selected_file.flatten(data)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
@@ -705,6 +789,12 @@ class EF_ARR(LinFixedEF):
# TS 102 221 Section 13.6
class EF_UMPC(TransparentEF):
_test_de_encode = [
( '3cff02', { "max_current_mA": 60, "t_op_s": 255,
"addl_info": { "req_inc_idle_current": False, "support_uicc_suspend": True } } ),
( '320500', { "max_current_mA": 50, "t_op_s": 5, "addl_info": {"req_inc_idle_current": False,
"support_uicc_suspend": False } } ),
]
def __init__(self, fid='2f08', sfid=0x08, name='EF.UMPC', desc='UICC Maximum Power Consumption'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(5, 5))
addl_info = FlagsEnum(Byte, req_inc_idle_current=1,
@@ -726,6 +816,11 @@ class CardProfileUICC(CardProfile):
# FIXME: DF.CD
EF_UMPC(),
]
addons = [
AddonSIM,
AddonGSMR,
AddonRUIM,
]
sw = {
'Normal': {
'9000': 'Normal ending of the command',
@@ -739,7 +834,7 @@ class CardProfileUICC(CardProfile):
'6200': 'No information given, state of non-volatile memory unchanged',
'6281': 'Part of returned data may be corrupted',
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
'6283': 'Selected file invalidated',
'6283': 'Selected file invalidated/disabled; needs to be activated before use',
'6284': 'Selected file in termination state',
'62f1': 'More data available',
'62f2': 'More data available and proactive command pending',
@@ -796,13 +891,14 @@ class CardProfileUICC(CardProfile):
}
super().__init__(name, desc='ETSI TS 102 221', cla="00",
sel_ctrl="0004", files_in_mf=files, sw=sw)
sel_ctrl="0004", files_in_mf=files, sw=sw,
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
@staticmethod
def decode_select_response(resp_hex: str) -> object:
def decode_select_response(data_hex: str) -> object:
"""ETSI TS 102 221 Section 11.1.1.3"""
t = FcpTemplate()
t.from_tlv(h2b(resp_hex))
t.from_tlv(h2b(data_hex))
d = t.to_dict()
return flatten_dict_lists(d['fcp_template'])
@@ -810,19 +906,107 @@ class CardProfileUICC(CardProfile):
def match_with_card(scc: SimCardCommands) -> bool:
return match_uicc(scc)
@with_default_category('TS 102 221 Specific Commands')
class AddlShellCommands(CommandSet):
suspend_uicc_parser = argparse.ArgumentParser()
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
help='Proposed minimum duration of suspension')
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
help='Proposed maximum duration of suspension')
class CardProfileUICCSIM(CardProfileUICC):
"""Same as above, but including 2G SIM support"""
# not ISO7816-4 but TS 102 221
@cmd2.with_argparser(suspend_uicc_parser)
def do_suspend_uicc(self, opts):
"""Perform the SUSPEND UICC command. Only supported on some UICC (check EF.UMPC)."""
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
max_len_secs=opts.max_duration_secs)
self._cmd.poutput(
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
ORDER = 0
resume_uicc_parser = argparse.ArgumentParser()
resume_uicc_parser.add_argument('token', type=str, help='Token provided during SUSPEND')
def __init__(self):
super().__init__('UICC-SIM')
@cmd2.with_argparser(resume_uicc_parser)
def do_resume_uicc(self, opts):
"""Perform the REUSME UICC operation. Only supported on some UICC. Also: A power-cycle
of the card is required between SUSPEND and RESUME, and only very few non-RESUME
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
self._cmd.card._scc.resume_uicc(opts.token)
# Add GSM specific files
self.files_in_mf.append(DF_TELECOM())
self.files_in_mf.append(DF_GSM())
term_cap_parser = argparse.ArgumentParser()
# power group
tc_power_grp = term_cap_parser.add_argument_group('Terminal Power Supply')
tc_power_grp.add_argument('--used-supply-voltage-class', type=str, choices=['a','b','c','d','e'],
help='Actual used Supply voltage class')
tc_power_grp.add_argument('--maximum-available-power-supply', type=auto_uint8,
help='Maximum available power supply of the terminal')
tc_power_grp.add_argument('--actual-used-freq-100k', type=auto_uint8,
help='Actual used clock frequency (in units of 100kHz)')
# no separate groups for those two
tc_elc_grp = term_cap_parser.add_argument_group('Extended logical channels terminal support')
tc_elc_grp.add_argument('--extended-logical-channel', action='store_true',
help='Extended Logical Channel supported')
tc_aif_grp = term_cap_parser.add_argument_group('Additional interfaces support')
tc_aif_grp.add_argument('--uicc-clf', action='store_true',
help='Local User Interface in the Device (LUId) supported')
# eUICC group
tc_euicc_grp = term_cap_parser.add_argument_group('Additional Terminal capability indications related to eUICC')
tc_euicc_grp.add_argument('--lui-d', action='store_true',
help='Local User Interface in the Device (LUId) supported')
tc_euicc_grp.add_argument('--lpd-d', action='store_true',
help='Local Profile Download in the Device (LPDd) supported')
tc_euicc_grp.add_argument('--lds-d', action='store_true',
help='Local Discovery Service in the Device (LPDd) supported')
tc_euicc_grp.add_argument('--lui-e-scws', action='store_true',
help='LUIe based on SCWS supported')
tc_euicc_grp.add_argument('--metadata-update-alerting', action='store_true',
help='Metadata update alerting supported')
tc_euicc_grp.add_argument('--enterprise-capable-device', action='store_true',
help='Enterprise Capable Device')
tc_euicc_grp.add_argument('--lui-e-e4e', action='store_true',
help='LUIe using E4E (ENVELOPE tag E4) supported')
tc_euicc_grp.add_argument('--lpr', action='store_true',
help='LPR (LPA Proxy) supported')
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_uicc(scc) and match_sim(scc)
@cmd2.with_argparser(term_cap_parser)
def do_terminal_capability(self, opts):
"""Perform the TERMINAL CAPABILITY function. Used to inform the UICC about terminal capability."""
ps_flags = {}
addl_if_flags = {}
euicc_flags = {}
opts_dict = vars(opts)
power_items = ['used_supply_voltage_class', 'maximum_available_power_supply', 'actual_used_freq_100k']
if any(opts_dict[x] for x in power_items):
if not all(opts_dict[x] for x in power_items):
raise argparse.ArgumentTypeError('If any of the Terminal Power Supply group options are used, all must be specified')
for k, v in opts_dict.items():
if k in AdditionalInterfacesSupport._construct.flags.keys():
addl_if_flags[k] = v
elif k in AdditionalTermCapEuicc._construct.flags.keys():
euicc_flags[k] = v
elif k in [f.name for f in TerminalPowerSupply._construct.subcons]:
if k == 'used_supply_voltage_class' and v:
v = {v: True}
ps_flags[k] = v
child_list = []
if any(x for x in ps_flags.values()):
child_list.append(TerminalPowerSupply(decoded=ps_flags))
if opts.extended_logical_channel:
child_list.append(ExtendedLchanTerminalSupport())
if any(x for x in addl_if_flags.values()):
child_list.append(AdditionalInterfacesSupport(decoded=addl_if_flags))
if any(x for x in euicc_flags.values()):
child_list.append(AdditionalTermCapEuicc(decoded=euicc_flags))
print(child_list)
tc = TerminalCapability(children=child_list)
self.terminal_capability(b2h(tc.to_tlv()))
def terminal_capability(self, data:Hexstr):
cmd_hex = "80AA0000%02x%s" % (len(data)//2, data)
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)

View File

@@ -18,16 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List
import cmd2
from cmd2 import CommandSet, with_default_category, with_argparser
import argparse
from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
import cmd2
from cmd2 import CommandSet, with_default_category
from pySim.exceptions import *
from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
from pySim.utils import b2h, auto_uint8, auto_uint16, is_hexstr
from pySim.ts_102_221 import *
@@ -35,9 +31,6 @@ from pySim.ts_102_221 import *
class Ts102222Commands(CommandSet):
"""Administrative commands for telecommunication applications."""
def __init__(self):
super().__init__()
delfile_parser = argparse.ArgumentParser()
delfile_parser.add_argument('--force-delete', action='store_true',
help='I really want to permanently delete the file. I know pySim cannot re-create it yet!')
@@ -52,7 +45,7 @@ class Ts102222Commands(CommandSet):
self._cmd.perror("Refusing to permanently delete the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.card._scc.delete_file(f.fid)
(_data, _sw) = self._cmd.lchan.scc.delete_file(f.fid)
def complete_delete_file(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for DELETE FILE"""
@@ -73,7 +66,7 @@ class Ts102222Commands(CommandSet):
self._cmd.perror("Refusing to terminate the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.card._scc.terminate_df(f.fid)
(_data, _sw) = self._cmd.lchan.scc.terminate_df(f.fid)
def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE DF"""
@@ -89,7 +82,7 @@ class Ts102222Commands(CommandSet):
self._cmd.perror("Refusing to terminate the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.card._scc.terminate_ef(f.fid)
(_data, _sw) = self._cmd.lchan.scc.terminate_ef(f.fid)
def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE EF"""
@@ -107,21 +100,21 @@ class Ts102222Commands(CommandSet):
if not opts.force_terminate_card:
self._cmd.perror("Refusing to permanently terminate the card, please read the help text.")
return
(data, sw) = self._cmd.card._scc.terminate_card_usage()
(_data, _sw) = self._cmd.lchan.scc.terminate_card_usage()
create_parser = argparse.ArgumentParser()
create_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
create_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
create_parser._action_groups.pop()
create_required = create_parser.add_argument_group('required arguments')
create_optional = create_parser.add_argument_group('optional arguments')
create_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
create_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
create_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
create_required.add_argument('--ef-arr-record-nr', required=True, type=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
create_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
create_required.add_argument('--structure', required=True, type=str, choices=['transparent', 'linear_fixed', 'ber_tlv'],
help='Structure of the to-be-created EF')
create_optional.add_argument('--short-file-id', type=str, help='Short File Identifier as 2-digit hex string')
create_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
create_optional.add_argument('--record-length', type=int, help='Length of each record in octets')
create_optional.add_argument('--record-length', type=auto_uint16, help='Length of each record in octets')
@cmd2.with_argparser(create_parser)
def do_create_ef(self, opts):
@@ -152,22 +145,22 @@ class Ts102222Commands(CommandSet):
ShortFileIdentifier(decoded=opts.short_file_id),
]
fcp = FcpTemplate(children=ies)
(data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
(_data, _sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
createdf_parser = argparse.ArgumentParser()
createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
createdf_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
createdf_parser._action_groups.pop()
createdf_required = createdf_parser.add_argument_group('required arguments')
createdf_optional = createdf_parser.add_argument_group('optional arguments')
createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments')
createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
createdf_required.add_argument('--ef-arr-record-nr', required=True, type=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
createdf_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
createdf_optional.add_argument('--aid', type=str, help='Application ID (creates an ADF, instead of a DF)')
createdf_optional.add_argument('--aid', type=is_hexstr, help='Application ID (creates an ADF, instead of a DF)')
# mandatory by spec, but ignored by several OS, so don't force the user
createdf_optional.add_argument('--total-file-size', type=int, help='Physical memory allocated for DF/ADi in octets')
createdf_optional.add_argument('--total-file-size', type=auto_uint16, help='Physical memory allocated for DF/ADi in octets')
createdf_sja_optional.add_argument('--permit-rfm-create', action='store_true')
createdf_sja_optional.add_argument('--permit-rfm-delete-terminate', action='store_true')
createdf_sja_optional.add_argument('--permit-other-applet-create', action='store_true')
@@ -195,14 +188,36 @@ class Ts102222Commands(CommandSet):
ies.append(TotalFileSize(decoded=opts.total_file_size))
# TODO: Spec states PIN Status Template DO is mandatory
if opts.permit_rfm_create or opts.permit_rfm_delete_terminate or opts.permit_other_applet_create or opts.permit_other_applet_delete_terminate:
toolkit_ac = {
'rfm_create': opts.permit_rfm_create,
'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
'other_applet_create': opts.permit_other_applet_create,
'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
}
ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
toolkit_ac = {
'rfm_create': opts.permit_rfm_create,
'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
'other_applet_create': opts.permit_other_applet_create,
'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
}
ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
fcp = FcpTemplate(children=ies)
(data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
(_data, _sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
resize_ef_parser = argparse.ArgumentParser()
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
resize_ef_parser._action_groups.pop()
resize_ef_required = resize_ef_parser.add_argument_group('required arguments')
resize_ef_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
@cmd2.with_argparser(resize_ef_parser)
def do_resize_ef(self, opts):
"""Resize an existing EF below the currently selected DF. Requires related privileges."""
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
ies = [FileIdentifier(decoded=f.fid),
FileSize(decoded=opts.file_size)]
fcp = FcpTemplate(children=ies)
(_data, _sw) = self._cmd.lchan.scc.resize_file(b2h(fcp.to_tlv()))
# the resized file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
def complete_resize_ef(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for RESIZE EF"""
index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)

114
pySim/ts_102_310.py Normal file
View File

@@ -0,0 +1,114 @@
# coding=utf-8
"""Utilities / Functions related to ETSI TS 102 310, the EAP UICC spec.
(C) 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/>.
"""
from pySim.construct import *
from construct import *
from construct import Optional as COptional
#from pySim.utils import *
from pySim.filesystem import CardDF, TransparentEF
from pySim.tlv import BER_TLV_IE, TLV_IE_Collection
# TS102 310 Section 7.1
class EF_EAPKEYS(TransparentEF):
class Msk(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(GreedyBytes)
class Emsk(BER_TLV_IE, tag=0x81):
_construct = HexAdapter(GreedyBytes)
class MskCollection(TLV_IE_Collection, nested=[EF_EAPKEYS.Msk, EF_EAPKEYS.Emsk]):
pass
def __init__(self, fid='4f01', name='EF.EAPKEYS', desc='EAP derived keys'):
super().__init__(fid, sfid=0x01, name=name, desc=desc, size=(1,None))
self._tlv = EF_EAPKEYS.MskCollection
# TS 102 310 Section 7.2
class EF_EAPSTATUS(TransparentEF):
def __init__(self, fid='4f02', name='EF.EAPSTATUS', desc='EAP Authentication Status'):
super().__init__(fid, sfid=0x02, name=name, desc=desc, size=(1,1))
self._construct = Enum(Int8ub, no_auth_started=0, authenticating=1,
authenticated=2, held_auth_failure=3)
# TS 102 310 Section 7.3
class EF_PUId(TransparentEF):
def __init__(self, fid='4f03', name='EF.PUId', desc='Permanent User Identity'):
super().__init__(fid, sfid=0x03, name=name, desc=desc, size=(10,None))
self._construct = GreedyBytes
# TS 102 310 Section 7.4
class EF_Ps(TransparentEF):
def __init__(self, fid='4f04', name='EF.Ps', desc='Pseudonym'):
super().__init__(fid, sfid=0x04, name=name, desc=desc, size=(1,None))
self._construct = GreedyBytes
# TS 102 310 Section 7.5
class EF_CurID(TransparentEF):
def __init__(self, fid='4f20', name='EF.CurID', desc='Current Identity'):
super().__init__(fid, sfid=0x10, name=name, desc=desc, size=(1,None))
self._construct = Struct('type'/Enum(Int8ub, permanent=0, pseudonym=1, re_authentication=2, should_not_be_revealed=255),
'_len'/Int8ub,
'value'/Utf8Adapter(this._len))
# TS 102 310 Section 7.6
class EF_ReID(TransparentEF):
class Identity(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
class Counter(BER_TLV_IE, tag=0x81):
_construct = GreedyInteger
class Collection(TLV_IE_Collection, nested=[EF_ReID.Identity, EF_ReID.Counter]):
pass
def __init__(self, fid='4f21', name='EF.ReID', desc='Re-Authentication Identity'):
super().__init__(fid, sfid=0x11, name=name, desc=desc, size=(1,None))
self._tlv = EF_ReID.Collection
# TS 102 310 Section 7.7
class EF_Realm(TransparentEF):
def __init__(self, fid='4f22', name='EF.Realm', desc='Relm value of the identity'):
super().__init__(fid, sfid=0x12, name=name, desc=desc, size=(1,None))
self._construct = Struct('_len'/Int8ub,
'realm'/Utf8Adapter(Bytes(this._len)))
class DF_EAP(CardDF):
# DF.EAP has no default FID; it always must be discovered via the EF.DIR entry
# and the 0x73 "discretionary template"
def __init__(self, fid, name='DF.EAP', desc='EAP client', **kwargs):
super().__init__(fid=fid, name=name, desc=desc, **kwargs)
files = [
EF_EAPKEYS(),
EF_EAPSTATUS(),
EF_PUId(),
EF_CurID(),
EF_ReID(),
]
self.add_files(files)
# TS 102 310 Section 5.2
class EapSupportedTypesList(BER_TLV_IE, tag=0x80):
_construct = GreedyRange(Int8ub)
class EapDedicatedFilesList(BER_TLV_IE, tag=0x81):
_construct = GreedyRange(Int16ub)
class EapLabel(BER_TLV_IE, tag=0x82):
_construct = GreedyBytes
class EapAppSvcSpecData(BER_TLV_IE, tag=0xa0, nested=[EapSupportedTypesList, EapDedicatedFilesList, EapLabel]):
pass
class DiscretionaryTemplate(BER_TLV_IE, tag=0x73, nested=[EapAppSvcSpecData]):
pass

File diff suppressed because it is too large Load Diff

View File

@@ -26,11 +26,12 @@ Needs to be a separate python module to avoid cyclic imports
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from construct import Optional as COptional
from construct import Struct, Int16ub, Int32ub
from pySim.tlv import *
from pySim.filesystem import *
from pySim.construct import *
from construct import Optional as COptional
from construct import *
# TS 31.102 Section 4.2.8
class EF_UServiceTable(TransparentEF):
@@ -42,7 +43,7 @@ class EF_UServiceTable(TransparentEF):
def _bit_byte_offset_for_service(service: int) -> Tuple[int, int]:
i = service - 1
byte_offset = i//8
bit_offset = (i % 8)
bit_offset = i % 8
return (byte_offset, bit_offset)
def _decode_bin(self, in_bin):
@@ -73,7 +74,7 @@ class EF_UServiceTable(TransparentEF):
service_nr = int(srv)
(byte_offset, bit_offset) = EF_UServiceTable._bit_byte_offset_for_service(
service_nr)
if in_json[srv]['activated'] == True:
if in_json[srv]['activated'] is True:
bit = 1
else:
bit = 0
@@ -82,7 +83,7 @@ class EF_UServiceTable(TransparentEF):
def get_active_services(self, cmd):
# obtain list of currently active services
(service_data, sw) = cmd.lchan.read_binary_dec()
(service_data, _sw) = cmd.lchan.read_binary_dec()
active_services = []
for s in service_data.keys():
if service_data[s]['activated']:
@@ -120,10 +121,30 @@ class EF_UServiceTable(TransparentEF):
cmd.lchan.select_file(self)
return num_problems
def ust_update(self, cmd, activate=[], deactivate=[]):
service_data, _sw = cmd.lchan.read_binary()
service_data = h2b(service_data)
for service in activate:
nbyte, nbit = EF_UServiceTable._bit_byte_offset_for_service(service)
if nbyte > len(service_data):
missing = nbyte - service_data
service_data.extend(missing * "00")
service_data[nbyte] |= (1 << nbit)
for service in deactivate:
nbyte, nbit = EF_UServiceTable._bit_byte_offset_for_service(service)
if nbyte > len(service_data):
missing = nbyte - service_data
service_data.extend(missing * "00")
service_data[nbyte] &= ~(1 << nbit)
service_data = b2h(service_data)
cmd.lchan.update_binary(service_data)
# TS 31.102 Section 4.4.2.1
class EF_PBR(LinFixedEF):
# TODO: a80ac0034f3a02c5034f0904aa0acb034f3d07c2034f4a06
def __init__(self, fid='4F30', name='EF.PBR', desc='Phone Book Reference', **kwargs):
super().__init__(fid, name=name, desc=desc, **kwargs)
#self._tlv = FIXME

View File

@@ -22,6 +22,7 @@ Various constants from 3GPP TS 31.103 V16.1.0
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from construct import Struct, Switch, this, Bytes, GreedyString
from pySim.filesystem import *
from pySim.utils import *
from pySim.tlv import *
@@ -30,6 +31,7 @@ from pySim.ts_31_102 import ADF_USIM, EF_FromPreferred
from pySim.ts_31_102_telecom import EF_UServiceTable
import pySim.ts_102_221
from pySim.ts_102_221 import EF_ARR
from pySim.construct import *
# Mapping between ISIM Service Number and its description
EF_IST_map = {
@@ -56,32 +58,15 @@ EF_IST_map = {
21: 'MuD and MiD configuration data',
}
EF_ISIM_ADF_map = {
'IST': '6F07',
'IMPI': '6F02',
'DOMAIN': '6F03',
'IMPU': '6F04',
'AD': '6FAD',
'ARR': '6F06',
'PCSCF': '6F09',
'GBAP': '6FD5',
'GBANL': '6FD7',
'NAFKCA': '6FDD',
'UICCIARI': '6FE7',
'SMS': '6F3C',
'SMSS': '6F43',
'SMSR': '6F47',
'SMSP': '6F42',
'FromPreferred': '6FF7',
'IMSConfigData': '6FF8',
'XCAPConfigData': '6FFC',
'WebRTCURI': '6FFA'
}
# TS 31.103 Section 4.2.2
class EF_IMPI(TransparentEF):
_test_de_encode = [
( '803137333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267',
{ "nai": "738006000000101@ims.mnc000.mcc738.3gppnetwork.org" } ),
]
class nai(BER_TLV_IE, tag=0x80):
_construct = GreedyString("utf8")
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6f02', sfid=0x02, name='EF.IMPI', desc='IMS private user identity', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -89,8 +74,12 @@ class EF_IMPI(TransparentEF):
# TS 31.103 Section 4.2.3
class EF_DOMAIN(TransparentEF):
_test_de_encode = [
( '8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267',
{ "domain": "ims.mnc000.mcc738.3gppnetwork.org" } ),
]
class domain(BER_TLV_IE, tag=0x80):
_construct = GreedyString("utf8")
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6f03', sfid=0x05, name='EF.DOMAIN', desc='Home Network Domain Name', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -98,8 +87,12 @@ class EF_DOMAIN(TransparentEF):
# TS 31.103 Section 4.2.4
class EF_IMPU(LinFixedEF):
_test_de_encode = [
( '80357369703a37333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267',
{ "impu": "sip:738006000000101@ims.mnc000.mcc738.3gppnetwork.org" } ),
]
class impu(BER_TLV_IE, tag=0x80):
_construct = GreedyString("utf8")
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6f04', sfid=0x04, name='EF.IMPU', desc='IMS public user identity', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -119,11 +112,13 @@ class EF_IST(EF_UServiceTable):
def do_ist_service_activate(self, arg):
"""Activate a service within EF.IST"""
self._cmd.card.update_ist(int(arg), 1)
selected_file = self._cmd.lchan.selected_file
selected_file.ust_update(self._cmd, [int(arg)], [])
def do_ist_service_deactivate(self, arg):
"""Deactivate a service within EF.IST"""
self._cmd.card.update_ist(int(arg), 0)
selected_file = self._cmd.lchan.selected_file
selected_file.ust_update(self._cmd, [], [int(arg)])
def do_ist_service_check(self, arg):
"""Check consistency between services of this file and files present/activated.
@@ -139,37 +134,65 @@ class EF_IST(EF_UServiceTable):
# TS 31.103 Section 4.2.8
class EF_PCSCF(LinFixedEF):
_test_de_encode = [
( '802c0070637363662e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267',
{'pcscf_address': { "address": "pcscf.ims.mnc000.mcc738.pub.3gppnetwork.org", "type_of_address": "FQDN" } } ),
( '800501c0a80c22',
{'pcscf_address': { "address": "192.168.12.34", "type_of_address": "IPv4" } } ),
( '801102fe800000000000000042d7fffe530335',
{'pcscf_address': { "address": "fe80::42:d7ff:fe53:335", "type_of_address": "IPv6" } } ),
]
class PcscfAddress(BER_TLV_IE, tag=0x80):
_construct = Struct('type_of_address'/Enum(Byte, FQDN=0, IPv4=1, IPv6=2),
'address'/Switch(this.type_of_address,
{'FQDN': Utf8Adapter(GreedyBytes),
'IPv4': Ipv4Adapter(GreedyBytes),
'IPv6': Ipv6Adapter(GreedyBytes)}))
def __init__(self, fid='6f09', sfid=None, name='EF.P-CSCF', desc='P-CSCF Address', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
def _decode_record_hex(self, raw_hex):
addr, addr_type = dec_addr_tlv(raw_hex)
return {"addr": addr, "addr_type": addr_type}
def _encode_record_hex(self, json_in):
addr = json_in['addr']
addr_type = json_in['addr_type']
return enc_addr_tlv(addr, addr_type)
self._tlv = EF_PCSCF.PcscfAddress
# TS 31.103 Section 4.2.9
class EF_GBABP(TransparentEF):
def __init__(self, fid='6fd5', sfid=None, name='EF.GBABP', desc='GBA Bootstrapping', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._construct = Struct('rand'/LV,
'b_tid'/LV,
'key_lifetime'/LV)
# TS 31.103 Section 4.2.10
class EF_GBANL(LinFixedEF):
class NAF_ID(BER_TLV_IE, tag=0x80):
_construct = Struct('fqdn'/Utf8Adapter(Bytes(this._.total_len-5)),
'ua_spi'/HexAdapter(Bytes(5)))
class B_TID(BER_TLV_IE, tag=0x81):
_construct = Utf8Adapter(GreedyBytes)
# pylint: disable=undefined-variable
class GbaNlCollection(TLV_IE_Collection, nested=[NAF_ID, B_TID]):
pass
def __init__(self, fid='6fd7', sfid=None, name='EF.GBANL', desc='GBA NAF List', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_GBANL.GbaNlCollection
# TS 31.103 Section 4.2.11
class EF_NAFKCA(LinFixedEF):
_test_de_encode = [
( '80296273662e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267',
{ 'naf_key_centre_address': 'bsf.ims.mnc000.mcc738.pub.3gppnetwork.org' } ),
( '8030656e65746e61667830312e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267',
{ 'naf_key_centre_address': 'enetnafx01.ims.mnc000.mcc738.pub.3gppnetwork.org' }),
]
class NafKeyCentreAddress(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6fdd', sfid=None, name='EF.NAFKCA', desc='NAF Key Centre Address', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_NAFKCA.NafKeyCentreAddress
# TS 31.103 Section 4.2.16
class EF_UICCIARI(LinFixedEF):
class iari(BER_TLV_IE, tag=0x80):
_construct = GreedyString("utf8")
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6fe7', sfid=None, name='EF.UICCIARI', desc='UICC IARI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -236,7 +259,7 @@ class EF_XCAPConfigData(BerTlvEF):
# TS 31.103 Section 4.2.20
class EF_WebRTCURI(TransparentEF):
class uri(BER_TLV_IE, tag=0x80):
_construct = GreedyString("utf8")
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6ffa', sfid=None, name='EF.WebRTCURI', desc='WebRTC URI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -258,9 +281,9 @@ class EF_MuDMiDConfigData(BerTlvEF):
class ADF_ISIM(CardADF):
def __init__(self, aid='a0000000871004', name='ADF.ISIM', fid=None, sfid=None,
def __init__(self, aid='a0000000871004', has_fs=True, name='ADF.ISIM', fid=None, sfid=None,
desc='ISIM Application'):
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
super().__init__(aid=aid, has_fs=has_fs, fid=fid, sfid=sfid, name=name, desc=desc)
files = [
EF_IMPI(),

60
pySim/ts_31_104.py Normal file
View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""
Support for 3GPP TS 31.104 V17.0.0
"""
# Copyright (C) 2023 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/>.
#
from pySim.filesystem import *
from pySim.utils import *
from pySim.tlv import *
from pySim.ts_31_102 import ADF_USIM
from pySim.ts_51_011 import EF_IMSI, EF_AD
import pySim.ts_102_221
from pySim.ts_102_221 import EF_ARR
class ADF_HPSIM(CardADF):
def __init__(self, aid='a000000087100A', has_fs=True, name='ADF.HPSIM', fid=None, sfid=None,
desc='HPSIM Application'):
super().__init__(aid=aid, has_fs=has_fs, fid=fid, sfid=sfid, name=name, desc=desc)
files = [
EF_ARR(fid='6f06', sfid=0x06),
EF_IMSI(fid='6f07', sfid=0x07),
EF_AD(fid='6fad', sfid=0x03),
]
self.add_files(files)
# add those commands to the general commands of a TransparentEF
self.shell_commands += [ADF_USIM.AddlShellCommands()]
def decode_select_response(self, data_hex):
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
# TS 31.104 Section 7.1
sw_hpsim = {
'Security management': {
'9862': 'Authentication error, incorrect MAC',
}
}
class CardApplicationHPSIM(CardApplication):
def __init__(self):
super().__init__('HPSIM', adf=ADF_HPSIM(), sw=sw_hpsim)

View File

@@ -30,9 +30,10 @@ order to describe the files specified in the relevant ETSI + 3GPP specifications
#
from pySim.profile import match_sim
from pySim.profile import CardProfile
from pySim.profile import CardProfile, CardProfileAddon
from pySim.filesystem import *
from pySim.ts_31_102_telecom import DF_PHONEBOOK, DF_MULTIMEDIA, DF_MCS, DF_V2X
from pySim.gsm_r import AddonGSMR
import enum
from pySim.construct import *
from construct import Optional as COptional
@@ -41,236 +42,6 @@ from struct import pack, unpack
from typing import Tuple
from pySim.tlv import *
from pySim.utils import *
MF_num = '3F00'
DF_num = {
'TELECOM': '7F10',
'GSM': '7F20',
'IS-41': '7F22',
'FP-CTS': '7F23',
'GRAPHICS': '5F50',
'IRIDIUM': '5F30',
'GLOBST': '5F31',
'ICO': '5F32',
'ACeS': '5F33',
'EIA/TIA-553': '5F40',
'CTS': '5F60',
'SOLSA': '5F70',
'MExE': '5F3C',
}
EF_num = {
# MF
'ICCID': '2FE2',
'ELP': '2F05',
'DIR': '2F00',
# DF_TELECOM
'ADN': '6F3A',
'FDN': '6F3B',
'SMS': '6F3C',
'CCP': '6F3D',
'MSISDN': '6F40',
'SMSP': '6F42',
'SMSS': '6F43',
'LND': '6F44',
'SMSR': '6F47',
'SDN': '6F49',
'EXT1': '6F4A',
'EXT2': '6F4B',
'EXT3': '6F4C',
'BDN': '6F4D',
'EXT4': '6F4E',
'CMI': '6F58',
'ECCP': '6F4F',
# DF_GRAPHICS
'IMG': '4F20',
# DF_SoLSA
'SAI': '4F30',
'SLL': '4F31',
# DF_MExE
'MExE-ST': '4F40',
'ORPK': '4F41',
'ARPK': '4F42',
'TPRPK': '4F43',
# DF_GSM
'LP': '6F05',
'IMSI': '6F07',
'Kc': '6F20',
'DCK': '6F2C',
'PLMNsel': '6F30',
'HPPLMN': '6F31',
'CNL': '6F32',
'ACMmax': '6F37',
'SST': '6F38',
'ACM': '6F39',
'GID1': '6F3E',
'GID2': '6F3F',
'PUCT': '6F41',
'CBMI': '6F45',
'SPN': '6F46',
'CBMID': '6F48',
'BCCH': '6F74',
'ACC': '6F78',
'FPLMN': '6F7B',
'LOCI': '6F7E',
'AD': '6FAD',
'PHASE': '6FAE',
'VGCS': '6FB1',
'VGCSS': '6FB2',
'VBS': '6FB3',
'VBSS': '6FB4',
'eMLPP': '6FB5',
'AAeM': '6FB6',
'ECC': '6FB7',
'CBMIR': '6F50',
'NIA': '6F51',
'KcGPRS': '6F52',
'LOCIGPRS': '6F53',
'SUME': '6F54',
'PLMNwAcT': '6F60',
'OPLMNwAcT': '6F61',
# Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
'HPLMNAcT': '6F62',
'HPLMNwAcT': '6F62',
'CPBCCH': '6F63',
'INVSCAN': '6F64',
'PNN': '6FC5',
'OPL': '6FC6',
'MBDN': '6FC7',
'EXT6': '6FC8',
'MBI': '6FC9',
'MWIS': '6FCA',
'CFIS': '6FCB',
'EXT7': '6FCC',
'SPDI': '6FCD',
'MMSN': '6FCE',
'EXT8': '6FCF',
'MMSICP': '6FD0',
'MMSUP': '6FD1',
'MMSUCP': '6FD2',
}
DF = {
'TELECOM': [MF_num, DF_num['TELECOM']],
'GSM': [MF_num, DF_num['GSM']],
'IS-41': [MF_num, DF_num['IS-41']],
'FP-CTS': [MF_num, DF_num['FP-CTS']],
'GRAPHICS': [MF_num, DF_num['GRAPHICS']],
'IRIDIUM': [MF_num, DF_num['IRIDIUM']],
'GLOBST': [MF_num, DF_num['GLOBST']],
'ICO': [MF_num, DF_num['ICO']],
'ACeS': [MF_num, DF_num['ACeS']],
'EIA/TIA-553': [MF_num, DF_num['EIA/TIA-553']],
'CTS': [MF_num, DF_num['CTS']],
'SoLSA': [MF_num, DF_num['SOLSA']],
'MExE': [MF_num, DF_num['MExE']],
}
EF = {
'ICCID': [MF_num, EF_num['ICCID']],
'ELP': [MF_num, EF_num['ELP']],
'DIR': [MF_num, EF_num['DIR']],
'ADN': DF['TELECOM']+[EF_num['ADN']],
'FDN': DF['TELECOM']+[EF_num['FDN']],
'SMS': DF['TELECOM']+[EF_num['SMS']],
'CCP': DF['TELECOM']+[EF_num['CCP']],
'MSISDN': DF['TELECOM']+[EF_num['MSISDN']],
'SMSP': DF['TELECOM']+[EF_num['SMSP']],
'SMSS': DF['TELECOM']+[EF_num['SMSS']],
'LND': DF['TELECOM']+[EF_num['LND']],
'SMSR': DF['TELECOM']+[EF_num['SMSR']],
'SDN': DF['TELECOM']+[EF_num['SDN']],
'EXT1': DF['TELECOM']+[EF_num['EXT1']],
'EXT2': DF['TELECOM']+[EF_num['EXT2']],
'EXT3': DF['TELECOM']+[EF_num['EXT3']],
'BDN': DF['TELECOM']+[EF_num['BDN']],
'EXT4': DF['TELECOM']+[EF_num['EXT4']],
'CMI': DF['TELECOM']+[EF_num['CMI']],
'ECCP': DF['TELECOM']+[EF_num['ECCP']],
'IMG': DF['GRAPHICS']+[EF_num['IMG']],
'SAI': DF['SoLSA']+[EF_num['SAI']],
'SLL': DF['SoLSA']+[EF_num['SLL']],
'MExE-ST': DF['MExE']+[EF_num['MExE-ST']],
'ORPK': DF['MExE']+[EF_num['ORPK']],
'ARPK': DF['MExE']+[EF_num['ARPK']],
'TPRPK': DF['MExE']+[EF_num['TPRPK']],
'LP': DF['GSM']+[EF_num['LP']],
'IMSI': DF['GSM']+[EF_num['IMSI']],
'Kc': DF['GSM']+[EF_num['Kc']],
'DCK': DF['GSM']+[EF_num['DCK']],
'PLMNsel': DF['GSM']+[EF_num['PLMNsel']],
'HPPLMN': DF['GSM']+[EF_num['HPPLMN']],
'CNL': DF['GSM']+[EF_num['CNL']],
'ACMmax': DF['GSM']+[EF_num['ACMmax']],
'SST': DF['GSM']+[EF_num['SST']],
'ACM': DF['GSM']+[EF_num['ACM']],
'GID1': DF['GSM']+[EF_num['GID1']],
'GID2': DF['GSM']+[EF_num['GID2']],
'PUCT': DF['GSM']+[EF_num['PUCT']],
'CBMI': DF['GSM']+[EF_num['CBMI']],
'SPN': DF['GSM']+[EF_num['SPN']],
'CBMID': DF['GSM']+[EF_num['CBMID']],
'BCCH': DF['GSM']+[EF_num['BCCH']],
'ACC': DF['GSM']+[EF_num['ACC']],
'FPLMN': DF['GSM']+[EF_num['FPLMN']],
'LOCI': DF['GSM']+[EF_num['LOCI']],
'AD': DF['GSM']+[EF_num['AD']],
'PHASE': DF['GSM']+[EF_num['PHASE']],
'VGCS': DF['GSM']+[EF_num['VGCS']],
'VGCSS': DF['GSM']+[EF_num['VGCSS']],
'VBS': DF['GSM']+[EF_num['VBS']],
'VBSS': DF['GSM']+[EF_num['VBSS']],
'eMLPP': DF['GSM']+[EF_num['eMLPP']],
'AAeM': DF['GSM']+[EF_num['AAeM']],
'ECC': DF['GSM']+[EF_num['ECC']],
'CBMIR': DF['GSM']+[EF_num['CBMIR']],
'NIA': DF['GSM']+[EF_num['NIA']],
'KcGPRS': DF['GSM']+[EF_num['KcGPRS']],
'LOCIGPRS': DF['GSM']+[EF_num['LOCIGPRS']],
'SUME': DF['GSM']+[EF_num['SUME']],
'PLMNwAcT': DF['GSM']+[EF_num['PLMNwAcT']],
'OPLMNwAcT': DF['GSM']+[EF_num['OPLMNwAcT']],
# Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
'HPLMNAcT': DF['GSM']+[EF_num['HPLMNAcT']],
'HPLMNwAcT': DF['GSM']+[EF_num['HPLMNAcT']],
'CPBCCH': DF['GSM']+[EF_num['CPBCCH']],
'INVSCAN': DF['GSM']+[EF_num['INVSCAN']],
'PNN': DF['GSM']+[EF_num['PNN']],
'OPL': DF['GSM']+[EF_num['OPL']],
'MBDN': DF['GSM']+[EF_num['MBDN']],
'EXT6': DF['GSM']+[EF_num['EXT6']],
'MBI': DF['GSM']+[EF_num['MBI']],
'MWIS': DF['GSM']+[EF_num['MWIS']],
'CFIS': DF['GSM']+[EF_num['CFIS']],
'EXT7': DF['GSM']+[EF_num['EXT7']],
'SPDI': DF['GSM']+[EF_num['SPDI']],
'MMSN': DF['GSM']+[EF_num['MMSN']],
'EXT8': DF['GSM']+[EF_num['EXT8']],
'MMSICP': DF['GSM']+[EF_num['MMSICP']],
'MMSUP': DF['GSM']+[EF_num['MMSUP']],
'MMSUCP': DF['GSM']+[EF_num['MMSUCP']],
}
# Mapping between SIM Service Number and its description
EF_SST_map = {
@@ -340,23 +111,54 @@ EF_SST_map = {
# DF.TELECOM
######################################################################
# TS 51.011 Section 10.5.1 / Table 12
class ExtendedBcdAdapter(Adapter):
"""Replace some hex-characters with other ASCII characters"""
# we only translate a=* / b=# as they habe a clear representation
# in terms of USSD / SS service codes
def _decode(self, obj, context, path):
if not isinstance(obj, str):
return obj
return obj.lower().replace("a","*").replace("b","#")
def _encode(self, obj, context, path):
if not isinstance(obj, str):
return obj
return obj.replace("*","a").replace("#","b")
# TS 51.011 Section 10.5.1
class EF_ADN(LinFixedEF):
def __init__(self, fid='6f3a', sfid=None, name='EF.ADN', desc='Abbreviated Dialing Numbers', **kwargs):
_test_decode = [
( '42204841203120536963ffffffff06810628560810ffffffffffffff',
{ "alpha_id": "B HA 1 Sic", "len_of_bcd": 6, "ton_npi": { "ext": True, "type_of_number":
"unknown", "numbering_plan_id":
"isdn_e164" }, "dialing_nr":
"6082658001", "cap_conf_id": 255, "ext1_record_id": 255 }),
( '4B756E64656E626574726575756E67FFFFFF0791947112122721ffffffffffff',
{"alpha_id": "Kundenbetreuung", "len_of_bcd": 7, "ton_npi": {"ext": True, "type_of_number":
"international",
"numbering_plan_id": "isdn_e164"},
"dialing_nr": "491721217212", "cap_conf_id": 255, "ext1_record_id": 255} )
]
_test_no_pad = True
def __init__(self, fid='6f3a', sfid=None, name='EF.ADN', desc='Abbreviated Dialing Numbers', ext=1, **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(14, 30), **kwargs)
self._construct = Struct('alpha_id'/COptional(GsmStringAdapter(Rpad(Bytes(this._.total_len-14)), codec='ascii')),
ext_name = 'ext%u_record_id' % ext
self._construct = Struct('alpha_id'/COptional(GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-14)))),
'len_of_bcd'/Int8ub,
'ton_npi'/TonNpi,
'dialing_nr'/BcdAdapter(Rpad(Bytes(10))),
'dialing_nr'/ExtendedBcdAdapter(BcdAdapter(Rpad(Bytes(10)))),
'cap_conf_id'/Int8ub,
'ext1_record_id'/Int8ub)
ext_name/Int8ub)
# TS 51.011 Section 10.5.5
class EF_SMS(LinFixedEF):
def __init__(self, fid='6f3c', sfid=None, name='EF.SMS', desc='Short messages', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(176, 176), **kwargs)
def _decode_record_bin(self, raw_bin_data):
def _decode_record_bin(self, raw_bin_data, **kwargs):
def decode_status(status):
if status & 0x01 == 0x00:
return (None, 'free_space')
@@ -385,23 +187,36 @@ class EF_SMS(LinFixedEF):
# TS 51.011 Section 10.5.5
class EF_MSISDN(LinFixedEF):
def __init__(self, fid='6f40', sfid=None, name='EF.MSISDN', desc='MSISDN', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(15, 34), **kwargs)
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(15, 34), leftpad=True, **kwargs)
def _decode_record_hex(self, raw_hex_data):
def _decode_record_hex(self, raw_hex_data, **kwargs):
return {'msisdn': dec_msisdn(raw_hex_data)}
def _encode_record_hex(self, abstract):
def _encode_record_hex(self, abstract, **kwargs):
msisdn = abstract['msisdn']
if type(msisdn) == str:
encoded_msisdn = enc_msisdn(msisdn)
else:
encoded_msisdn = enc_msisdn(msisdn[2], msisdn[0], msisdn[1])
alpha_identifier = (list(self.rec_len)[
0] - len(encoded_msisdn) // 2) * "ff"
alpha_identifier = (list(self.rec_len)[0] - len(encoded_msisdn) // 2) * "ff"
return alpha_identifier + encoded_msisdn
# TS 51.011 Section 10.5.6
class EF_SMSP(LinFixedEF):
# FIXME: re-encode fails / missing alpha_id at start of output
_test_decode = [
( '454e6574776f726b73fffffffffffffff1ffffffffffffffffffffffffffffffffffffffffffffffff0000a7',
{ "alpha_id": "ENetworks", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
"tp_pid": True, "tp_dcs": True, "tp_vp": True },
"tp_dest_addr": { "length": 255, "ton_npi": { "ext": True, "type_of_number": "reserved_for_extension",
"numbering_plan_id": "reserved_for_extension" },
"call_number": "" },
"tp_sc_addr": { "length": 255, "ton_npi": { "ext": True, "type_of_number": "reserved_for_extension",
"numbering_plan_id": "reserved_for_extension" },
"call_number": "" },
"tp_pid": "00", "tp_dcs": "00", "tp_vp_minutes": 1440 } ),
]
_test_no_pad = True
class ValidityPeriodAdapter(Adapter):
def _decode(self, obj, context, path):
if obj <= 143:
@@ -418,11 +233,11 @@ class EF_SMSP(LinFixedEF):
if obj <= 12*60:
return obj/5 - 1
elif obj <= 24*60:
return 143 + ((obj - (12 * 60)) / 30)
return 143 + ((obj - (12 * 60)) // 30)
elif obj <= 30 * 24 * 60:
return 166 + (obj / (24 * 60))
elif obj <= 63 * 7 * 24 * 60:
return 192 + (obj / (7 * 24 * 60))
return 192 + (obj // (7 * 24 * 60))
else:
raise ValueError
@@ -481,7 +296,7 @@ class DF_TELECOM(CardDF):
super().__init__(fid=fid, name=name, desc=desc, **kwargs)
files = [
EF_ADN(),
EF_ADN(fid='6f3b', name='EF.FDN', desc='Fixed dialling numbers'),
EF_ADN(fid='6f3b', name='EF.FDN', desc='Fixed dialling numbers', ext=2),
EF_SMS(),
LinFixedEF(fid='6f3d', name='EF.CCP',
desc='Capability Configuration Parameters', rec_len=(14, 14)),
@@ -490,7 +305,8 @@ class DF_TELECOM(CardDF):
EF_MSISDN(),
EF_SMSP(),
EF_SMSS(),
# LND, SDN
EF_ADN('6f44', None, 'EF.LND', 'Last Number Dialled', ext=1),
EF_ADN('6f49', None, 'EF.SDN', 'Service Dialling Numbers', ext=3),
EF_EXT('6f4a', None, 'EF.EXT1', 'Extension1 (ADN/SSC)'),
EF_EXT('6f4b', None, 'EF.EXT2', 'Extension2 (FDN/SSC)'),
EF_EXT('6f4c', None, 'EF.EXT3', 'Extension3 (SDN)'),
@@ -512,18 +328,25 @@ class DF_TELECOM(CardDF):
# TS 51.011 Section 10.3.1
class EF_LP(TransRecEF):
_test_de_encode = [
( "24", "24"),
]
def __init__(self, fid='6f05', sfid=None, name='EF.LP', size=(1, None), rec_len=1,
desc='Language Preference'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len)
def _decode_record_bin(self, in_bin):
def _decode_record_bin(self, in_bin, **kwargs):
return b2h(in_bin)
def _encode_record_bin(self, in_json):
def _encode_record_bin(self, in_json, **kwargs):
return h2b(in_json)
# TS 51.011 Section 10.3.2
class EF_IMSI(TransparentEF):
_test_de_encode = [
( "082982608200002080", { "imsi": "228062800000208" } ),
( "082926101160845740", { "imsi": "262011106487504" } ),
]
def __init__(self, fid='6f07', sfid=None, name='EF.IMSI', desc='IMSI', size=(9, 9)):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
# add those commands to the general commands of a TransparentEF
@@ -562,17 +385,21 @@ class EF_IMSI(TransparentEF):
# TS 51.011 Section 10.3.4
class EF_PLMNsel(TransRecEF):
_test_de_encode = [
( "22F860", { "mcc": "228", "mnc": "06" } ),
( "330420", { "mcc": "334", "mnc": "020" } ),
]
def __init__(self, fid='6f30', sfid=None, name='EF.PLMNsel', desc='PLMN selector',
size=(24, None), rec_len=3, **kwargs):
super().__init__(fid, name=name, sfid=sfid, desc=desc, size=size, rec_len=rec_len, **kwargs)
def _decode_record_hex(self, in_hex):
def _decode_record_hex(self, in_hex, **kwargs):
if in_hex[:6] == "ffffff":
return None
else:
return dec_plmn(in_hex)
def _encode_record_hex(self, in_json):
def _encode_record_hex(self, in_json, **kwargs):
if in_json == None:
return "ffffff"
else:
@@ -580,6 +407,9 @@ class EF_PLMNsel(TransRecEF):
# TS 51.011 Section 10.3.6
class EF_ACMmax(TransparentEF):
_test_de_encode = [
( "000000", { "acm_max": 0 } ),
]
def __init__(self, fid='6f37', sfid=None, name='EF.ACMmax', size=(3, 3),
desc='ACM maximum value', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
@@ -590,6 +420,7 @@ class EF_ServiceTable(TransparentEF):
def __init__(self, fid, sfid, name, desc, size, table):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
self.table = table
self.shell_commands += [self.AddlShellCommands()]
@staticmethod
def _bit_byte_offset_for_service(service: int) -> Tuple[int, int]:
@@ -617,16 +448,14 @@ class EF_ServiceTable(TransparentEF):
bin_len = 0
for srv in in_json.keys():
service_nr = int(srv)
(byte_offset, bit_offset) = EF_ServiceTable._bit_byte_offset_for_service(
service_nr)
(byte_offset, bit_offset) = self._bit_byte_offset_for_service(service_nr)
if byte_offset >= bin_len:
bin_len = byte_offset+1
# encode the actual data
out = bytearray(b'\x00' * bin_len)
for srv in in_json.keys():
service_nr = int(srv)
(byte_offset, bit_offset) = EF_ServiceTable._bit_byte_offset_for_service(
service_nr)
(byte_offset, bit_offset) = self._bit_byte_offset_for_service(service_nr)
bits = 0
if in_json[srv]['allocated'] == True:
bits |= 1
@@ -635,8 +464,49 @@ class EF_ServiceTable(TransparentEF):
out[byte_offset] |= ((bits & 3) << bit_offset)
return out
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
def _adjust_service(self, service_nr: int, allocate: Optional[bool] = None, activate : Optional[bool] = None):
(byte_offset, bit_offset) = EF_ServiceTable._bit_byte_offset_for_service(service_nr)
hex_data, sw = self._cmd.lchan.read_binary(length=1, offset=byte_offset)
data = h2b(hex_data)
if allocate is not None:
if allocate:
data[0] |= (1 << bit_offset)
else:
data[0] &= ~(1 << bit_offset)
if activate is not None:
if activate:
data[0] |= (2 << bit_offset)
else:
data[0] &= ~(2 << bit_offset)
total_data, sw = self._cmd.lchan.update_binary(b2h(data), offset=byte_offset)
return sw
def do_sst_service_allocate(self, arg):
"""Allocate a service within EF.SST"""
self._adjust_service(int(arg), allocate = True)
def do_sst_service_deallocate(self, arg):
"""Deallocate a service within EF.SST"""
self._adjust_service(int(arg), allocate = False)
def do_sst_service_activate(self, arg):
"""Activate a service within EF.SST"""
self._adjust_service(int(arg), activate = True)
def do_sst_service_deactivate(self, arg):
"""Deactivate a service within EF.SST"""
self._adjust_service(int(arg), activate = False)
# TS 51.011 Section 10.3.11
class EF_SPN(TransparentEF):
_test_de_encode = [
( "0147534d2d52204348ffffffffffffffff",
{ "rfu": 0, "hide_in_oplmn": False, "show_in_hplmn": True, "spn": "GSM-R CH" } ),
]
def __init__(self, fid='6f46', sfid=None, name='EF.SPN',
desc='Service Provider Name', size=(17, 17), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
@@ -646,11 +516,12 @@ class EF_SPN(TransparentEF):
'hide_in_oplmn'/Flag,
'show_in_hplmn'/Flag,
# Bytes 2..17
'spn'/Bytewise(GsmString(16))
'spn'/Bytewise(GsmOrUcs2String(16))
)
# TS 51.011 Section 10.3.13
class EF_CBMI(TransRecEF):
# TODO: Test vectors
def __init__(self, fid='6f45', sfid=None, name='EF.CBMI', size=(2, None), rec_len=2,
desc='Cell Broadcast message identifier selection', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
@@ -658,13 +529,32 @@ class EF_CBMI(TransRecEF):
# TS 51.011 Section 10.3.15
class EF_ACC(TransparentEF):
# example: acc_list(15) will produce a dict with only ACC15 being True
# example: acc_list(2, 4) will produce a dict with ACC2 and ACC4 being True
# example: acc_list() will produce a dict with all ACCs being False
acc_list = lambda *active: {'ACC{}'.format(c) : c in active for c in range(16)}
_test_de_encode = [
( "0000", acc_list() ),
( "0001", acc_list(0) ),
( "0002", acc_list(1) ),
( "0100", acc_list(8) ),
( "8000", acc_list(15) ),
( "802b", acc_list(0,1,3,5,15) ),
]
def __init__(self, fid='6f78', sfid=None, name='EF.ACC',
desc='Access Control Class', size=(2, 2), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = HexAdapter(Bytes(2))
# LSB of octet 2 is ACC=0 ... MSB of octet 1 is ACC=15
flags = ['ACC{}'.format(c) / Flag for c in range(16)]
self._construct = ByteSwapped(BitsSwapped(BitStruct(*flags)))
# TS 51.011 Section 10.3.16
class EF_LOCI(TransparentEF):
_test_de_encode = [
( "7802570222f81009780000",
{ "tmsi": "78025702", "lai": "22f8100978", "tmsi_time": 0, "lu_status": "updated" } ),
]
def __init__(self, fid='6f7e', sfid=None, name='EF.LOCI', desc='Location Information', size=(11, 11)):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
self._construct = Struct('tmsi'/HexAdapter(Bytes(4)), 'lai'/HexAdapter(Bytes(5)), 'tmsi_time'/Int8ub,
@@ -673,6 +563,12 @@ class EF_LOCI(TransparentEF):
# TS 51.011 Section 10.3.18
class EF_AD(TransparentEF):
_test_de_encode = [
( "00ffff",
{ "ms_operation_mode": "normal", "rfu1": 255, "rfu2": 127, "ofm": True, "extensions": None } ),
]
_test_no_pad = True
class OP_MODE(enum.IntEnum):
normal = 0x00
type_approval = 0x80
@@ -704,6 +600,9 @@ class EF_AD(TransparentEF):
# TS 51.011 Section 10.3.20 / 10.3.22
class EF_VGCS(TransRecEF):
_test_de_encode = [
( "92f9ffff", "299fffff" ),
]
def __init__(self, fid='6fb1', sfid=None, name='EF.VGCS', size=(4, 200), rec_len=4,
desc='Voice Group Call Service', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
@@ -711,14 +610,27 @@ class EF_VGCS(TransRecEF):
# TS 51.011 Section 10.3.21 / 10.3.23
class EF_VGCSS(TransparentEF):
_test_decode = [
( "010000004540fc",
{ "flags": [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ] }
),
]
def __init__(self, fid='6fb2', sfid=None, name='EF.VGCSS', size=(7, 7),
desc='Voice Group Call Service Status', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = BitStruct(
'flags'/Bit[50], Padding(6, pattern=b'\xff'))
self._construct = BitsSwapped(BitStruct(
'flags'/Bit[50], Padding(6, pattern=b'\xff')))
# TS 51.011 Section 10.3.24
class EF_eMLPP(TransparentEF):
_test_de_encode = [
( "7c04", { "levels": { "A": False, "B": False, "zero": True, "one": True,
"two": True, "three": True, "four": True },
"fast_call_setup_cond": { "A": False, "B": False, "zero": True, "one": False,
"two": False, "three": False, "four": False }
}),
]
def __init__(self, fid='6fb5', sfid=None, name='EF.eMLPP', size=(2, 2),
desc='enhanced Multi Level Pre-emption and Priority', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
@@ -729,6 +641,10 @@ class EF_eMLPP(TransparentEF):
# TS 51.011 Section 10.3.25
class EF_AAeM(TransparentEF):
_test_de_encode = [
( "3c", { "auto_answer_prio_levels": { "A": False, "B": False, "zero": True, "one": True,
"two": True, "three": True, "four": False } } ),
]
def __init__(self, fid='6fb6', sfid=None, name='EF.AAeM', size=(1, 1),
desc='Automatic Answer for eMLPP Service', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
@@ -772,7 +688,7 @@ class EF_CNL(TransRecEF):
desc='Co-operative Network List', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
def _decode_record_hex(self, in_hex):
def _decode_record_hex(self, in_hex, **kwargs):
(in_plmn, sub, svp, corp) = unpack('!3sBBB', h2b(in_hex))
res = dec_plmn(b2h(in_plmn))
res['network_subset'] = sub
@@ -780,7 +696,7 @@ class EF_CNL(TransRecEF):
res['corporate_id'] = corp
return res
def _encode_record_hex(self, in_json):
def _encode_record_hex(self, in_json, **kwargs):
plmn = enc_plmn(in_json['mcc'], in_json['mnc'])
return b2h(pack('!3sBBB',
h2b(plmn),
@@ -798,30 +714,43 @@ class EF_NIA(LinFixedEF):
# TS 51.011 Section 10.3.32
class EF_Kc(TransparentEF):
_test_de_encode = [
( "837d783609a3858f05", { "kc": "837d783609a3858f", "cksn": 5 } ),
]
def __init__(self, fid='6f20', sfid=None, name='EF.Kc', desc='Ciphering key Kc', size=(9, 9), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct('kc'/HexAdapter(Bytes(8)), 'cksn'/Int8ub)
# TS 51.011 Section 10.3.33
class EF_LOCIGPRS(TransparentEF):
_test_de_encode = [
( "ffffffffffffff22f8990000ff01",
{ "ptmsi": "ffffffff", "ptmsi_sig": "ffffff", "rai": "22f8990000ff", "rau_status": "not_updated" } ),
]
def __init__(self, fid='6f53', sfid=None, name='EF.LOCIGPRS', desc='GPRS Location Information', size=(14, 14)):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
self._construct = Struct('ptmsi'/HexAdapter(Bytes(4)), 'ptmsi_sig'/Int8ub, 'rai'/HexAdapter(Bytes(6)),
self._construct = Struct('ptmsi'/HexAdapter(Bytes(4)), 'ptmsi_sig'/HexAdapter(Bytes(3)),
'rai'/HexAdapter(Bytes(6)),
'rau_status'/Enum(Byte, updated=0, not_updated=1, plmn_not_allowed=2,
routing_area_not_allowed=3))
# TS 51.011 Section 10.3.35..37
class EF_xPLMNwAcT(TransRecEF):
def __init__(self, fid, sfid=None, name=None, desc=None, size=(40, None), rec_len=5, **kwargs):
_test_de_encode = [
( '62F2104000', { "mcc": "262", "mnc": "01", "act": [ "E-UTRAN NB-S1", "E-UTRAN WB-S1" ] } ),
( '62F2108000', { "mcc": "262", "mnc": "01", "act": [ "UTRAN" ] } ),
( '62F220488C', { "mcc": "262", "mnc": "02", "act": ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'NG-RAN'] } ),
]
def __init__(self, fid='1234', sfid=None, name=None, desc=None, size=(40, None), rec_len=5, **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
def _decode_record_hex(self, in_hex):
def _decode_record_hex(self, in_hex, **kwargs):
if in_hex[:6] == "ffffff":
return None
else:
return dec_xplmn_w_act(in_hex)
def _encode_record_hex(self, in_json):
def _encode_record_hex(self, in_json, **kwargs):
if in_json == None:
return "ffffff0000"
else:
@@ -844,18 +773,18 @@ class EF_xPLMNwAcT(TransRecEF):
if 'cdma2000 1xRTT' in in_list:
u16 |= 0x0010
# E-UTRAN
if 'E-UTRAN' in in_list:
if 'E-UTRAN WB-S1' in in_list and 'E-UTRAN NB-S1' in in_list:
u16 |= 0x4000
if 'E-UTRAN WB-S1' in in_list:
elif 'E-UTRAN WB-S1' in in_list:
u16 |= 0x6000
if 'E-UTRAN NB-S1' in in_list:
elif 'E-UTRAN NB-S1' in in_list:
u16 |= 0x5000
# GSM mess
if 'GSM' in in_list and 'EC-GSM-IoT' in in_list:
u16 |= 0x008C
elif 'GSM' in in_list:
u16 |= 0x0084
elif 'EC-GSM-IuT' in in_list:
elif 'EC-GSM-IoT' in in_list:
u16 |= 0x0088
return '%04X' % (u16)
@@ -874,8 +803,31 @@ class EF_InvScan(TransparentEF):
self._construct = FlagsEnum(
Byte, in_limited_service_mode=1, after_successful_plmn_selection=2)
# TS 51.011 Section 10.3.46
class EF_CFIS(LinFixedEF):
_test_decode = [
( '0100ffffffffffffffffffffffffffff',
{"msp_number": 1, "cfu_indicator_status": { "voice": False, "fax": False, "data": False, "rfu": 0 },
"len_of_bcd": 255, "ton_npi": {"ext": True,
"type_of_number": "reserved_for_extension",
"numbering_plan_id": "reserved_for_extension"},
"dialing_nr": "", "cap_conf_id": 255, "ext7_record_id": 255} ),
]
def __init__(self, fid='6fcb', sfid=None, name='EF.CFIS', desc='Call Forwarding Indication Status', ext=7, **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(16, 30), **kwargs)
ext_name = 'ext%u_record_id' % ext
self._construct = Struct('msp_number'/Int8ub,
'cfu_indicator_status'/BitStruct('voice'/Flag, 'fax'/Flag, 'data'/Flag, 'rfu'/BitsRFU(5)),
'len_of_bcd'/Int8ub,
'ton_npi'/TonNpi,
'dialing_nr'/ExtendedBcdAdapter(BcdAdapter(Rpad(Bytes(10)))),
'cap_conf_id'/Int8ub,
ext_name/Int8ub)
# TS 51.011 Section 4.2.58
class EF_PNN(LinFixedEF):
# TODO: 430a82d432bbbc7eb75de432450a82d432bbbc7eb75de432ffffffff
# TODO: 430a82c596b34cbfbfe5eb39ffffffffffffffffffffffffffffffffffff
class FullNameForNetwork(BER_TLV_IE, tag=0x43):
# TS 24.008 10.5.3.5a
# TODO: proper decode
@@ -895,13 +847,21 @@ class EF_PNN(LinFixedEF):
# TS 51.011 Section 10.3.42
class EF_OPL(LinFixedEF):
_test_de_encode = [
( '62f2100000fffe01',
{ "lai": { "mcc_mnc": "262-01", "lac_min": "0000", "lac_max": "fffe" }, "pnn_record_id": 1 } ),
]
def __init__(self, fid='6fc6', sfid=None, name='EF.OPL', rec_len=(8, 8), desc='Operator PLMN List', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
self._construct = Struct('lai'/Struct('mcc_mnc'/BcdAdapter(Bytes(3)),
self._construct = Struct('lai'/Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
'lac_min'/HexAdapter(Bytes(2)), 'lac_max'/HexAdapter(Bytes(2))), 'pnn_record_id'/Int8ub)
# TS 51.011 Section 10.3.44 + TS 31.102 4.2.62
class EF_MBI(LinFixedEF):
_test_de_encode = [
( '0100000000',
{ "mbi_voicemail": 1, "mbi_fax": 0, "mbi_email": 0, "mbi_other": 0, "mbi_videocall": 0 } ),
]
def __init__(self, fid='6fc9', sfid=None, name='EF.MBI', rec_len=(4, 5), desc='Mailbox Identifier', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
self._construct = Struct('mbi_voicemail'/Int8ub, 'mbi_fax'/Int8ub, 'mbi_email'/Int8ub,
@@ -909,6 +869,14 @@ class EF_MBI(LinFixedEF):
# TS 51.011 Section 10.3.45 + TS 31.102 4.2.63
class EF_MWIS(LinFixedEF):
_test_de_encode = [
( '0000000000',
{"mwi_status": {"voicemail": False, "fax": False, "email": False, "other": False, "videomail":
False}, "num_waiting_voicemail": 0, "num_waiting_fax": 0, "num_waiting_email": 0,
"num_waiting_other": 0, "num_waiting_videomail": None} ),
]
_test_no_pad = True
def __init__(self, fid='6fca', sfid=None, name='EF.MWIS', rec_len=(5, 6),
desc='Message Waiting Indication Status', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
@@ -919,9 +887,10 @@ class EF_MWIS(LinFixedEF):
# TS 51.011 Section 10.3.66
class EF_SPDI(TransparentEF):
# TODO: a305800337f800ffffffffffffffffffffffffffffffffffffffffffffff
class ServiceProviderPLMN(BER_TLV_IE, tag=0x80):
# flexible numbers of 3-byte PLMN records
_construct = GreedyRange(BcdAdapter(Bytes(3)))
_construct = GreedyRange(PlmnAdapter(Bytes(3)))
class SPDI(BER_TLV_IE, tag=0xA3, nested=[ServiceProviderPLMN]):
pass
@@ -966,7 +935,7 @@ class EF_MMSICP(TransparentEF):
# TS 51.011 Section 10.3.54
class EF_MMSUP(LinFixedEF):
class MMS_UserPref_ProfileName(BER_TLV_IE, tag=0x81):
pass
_construct = GsmOrUcs2Adapter(GreedyBytes)
class MMS_UserPref_Info(BER_TLV_IE, tag=0x82):
pass
@@ -989,38 +958,40 @@ class EF_MMSUCP(TransparentEF):
class DF_GSM(CardDF):
def __init__(self, fid='7f20', name='DF.GSM', desc='GSM Network related files'):
super().__init__(fid=fid, name=name, desc=desc)
self.shell_commands += [self.AddlShellCommands()]
files = [
EF_LP(),
EF_IMSI(),
EF_Kc(),
EF_PLMNsel(),
TransparentEF('6f31', None, 'EF.HPPLMN',
'Higher Priority PLMN search period'),
desc='Higher Priority PLMN search period'),
EF_ACMmax(),
EF_ServiceTable('6f38', None, 'EF.SST',
'SIM service table', table=EF_SST_map, size=(2, 16)),
desc='SIM service table', table=EF_SST_map, size=(2, 16)),
CyclicEF('6f39', None, 'EF.ACM',
'Accumulated call meter', rec_len=(3, 3)),
TransparentEF('6f3e', None, 'EF.GID1', 'Group Identifier Level 1'),
TransparentEF('6f3f', None, 'EF.GID2', 'Group Identifier Level 2'),
desc='Accumulated call meter', rec_len=(3, 3)),
TransparentEF('6f3e', None, 'EF.GID1', desc='Group Identifier Level 1'),
TransparentEF('6f3f', None, 'EF.GID2', desc='Group Identifier Level 2'),
EF_SPN(),
TransparentEF('6f41', None, 'EF.PUCT',
'Price per unit and currency table', size=(5, 5)),
desc='Price per unit and currency table', size=(5, 5)),
EF_CBMI(),
TransparentEF('6f7f', None, 'EF.BCCH',
'Broadcast control channels', size=(16, 16)),
TransparentEF('6f74', None, 'EF.BCCH',
desc='Broadcast control channels', size=(16, 16)),
EF_ACC(),
EF_PLMNsel('6f7b', None, 'EF.FPLMN',
'Forbidden PLMNs', size=(12, 12)),
desc='Forbidden PLMNs', size=(12, 12)),
EF_LOCI(),
EF_AD(),
TransparentEF('6fa3', None, 'EF.Phase',
'Phase identification', size=(1, 1)),
TransparentEF('6fae', None, 'EF.Phase',
desc='Phase identification', size=(1, 1)),
EF_VGCS(),
EF_VGCSS(),
EF_VGCS('6fb3', None, 'EF.VBS', 'Voice Broadcast Service'),
EF_VGCS('6fb3', None, 'EF.VBS', desc='Voice Broadcast Service'),
EF_VGCSS('6fb4', None, 'EF.VBSS',
'Voice Broadcast Service Status'),
desc='Voice Broadcast Service Status'),
EF_eMLPP(),
EF_AAeM(),
EF_CBMID(),
@@ -1029,39 +1000,52 @@ class DF_GSM(CardDF):
EF_DCK(),
EF_CNL(),
EF_NIA(),
EF_Kc('6f52', None, 'EF.KcGPRS', 'GPRS Ciphering key KcGPRS'),
EF_Kc('6f52', None, 'EF.KcGPRS', desc='GPRS Ciphering key KcGPRS'),
EF_LOCIGPRS(),
TransparentEF('6f54', None, 'EF.SUME', 'SetUpMenu Elements'),
TransparentEF('6f54', None, 'EF.SUME', desc='SetUpMenu Elements'),
EF_xPLMNwAcT('6f60', None, 'EF.PLMNwAcT',
'User controlled PLMN Selector with Access Technology'),
desc='User controlled PLMN Selector with Access Technology'),
EF_xPLMNwAcT('6f61', None, 'EF.OPLMNwAcT',
'Operator controlled PLMN Selector with Access Technology'),
desc='Operator controlled PLMN Selector with Access Technology'),
EF_xPLMNwAcT('6f62', None, 'EF.HPLMNwAcT',
'HPLMN Selector with Access Technology'),
desc='HPLMN Selector with Access Technology'),
EF_CPBCCH(),
EF_InvScan(),
EF_PNN(),
EF_OPL(),
EF_ADN('6fc7', None, 'EF.MBDN', 'Mailbox Dialling Numbers'),
EF_ADN('6fc7', None, 'EF.MBDN', desc='Mailbox Dialling Numbers'),
EF_MBI(),
EF_MWIS(),
EF_ADN('6fcb', None, 'EF.CFIS',
'Call Forwarding Indication Status'),
EF_EXT('6fc8', None, 'EF.EXT6', 'Externsion6 (MBDN)'),
EF_EXT('6fcc', None, 'EF.EXT7', 'Externsion7 (CFIS)'),
EF_CFIS(),
EF_EXT('6fc8', None, 'EF.EXT6', desc='Externsion6 (MBDN)'),
EF_EXT('6fcc', None, 'EF.EXT7', desc='Externsion7 (CFIS)'),
EF_SPDI(),
EF_MMSN(),
EF_EXT('6fcf', None, 'EF.EXT8', 'Extension8 (MMSN)'),
EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'),
EF_MMSICP(),
EF_MMSUP(),
EF_MMSUCP(),
]
self.add_files(files)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
def __init__(self):
super().__init__()
authenticate_parser = argparse.ArgumentParser()
authenticate_parser.add_argument('rand', type=is_hexstr, help='Random challenge')
@cmd2.with_argparser(authenticate_parser)
def do_authenticate(self, opts):
"""Perform GSM Authentication."""
(data, sw) = self._cmd.lchan.scc.run_gsm(opts.rand)
self._cmd.poutput_json(data)
class CardProfileSIM(CardProfile):
ORDER = 2
ORDER = 3
def __init__(self):
sw = {
@@ -1101,11 +1085,18 @@ class CardProfileSIM(CardProfile):
},
}
addons = [
AddonGSMR,
]
super().__init__('SIM', desc='GSM SIM Card', cla="a0",
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw)
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw, addons = addons)
@staticmethod
def decode_select_response(resp_hex: str) -> object:
# we try to build something that resembles a dict resulting from the TLV decoder
# of TS 102.221 (FcpTemplate), so that higher-level code only has to deal with one
# format of SELECT response
resp_bin = h2b(resp_hex)
struct_of_file_map = {
0: 'transparent',
@@ -1124,21 +1115,25 @@ class CardProfileSIM(CardProfile):
'proprietary_info': {},
}
ret['file_id'] = b2h(resp_bin[4:6])
ret['proprietary_info']['available_memory'] = int.from_bytes(
resp_bin[2:4], 'big')
file_type = type_of_file_map[resp_bin[6]
] if resp_bin[6] in type_of_file_map else resp_bin[6]
ret['file_descriptor']['file_descriptor_byte']['file_type'] = file_type
if file_type in ['mf', 'df']:
ret['proprietary_info']['available_memory'] = int.from_bytes(resp_bin[2:4], 'big')
ret['file_characteristics'] = b2h(resp_bin[13:14])
ret['num_direct_child_df'] = resp_bin[14]
ret['num_direct_child_ef'] = resp_bin[15]
ret['num_chv_unblock_adm_codes'] = int(resp_bin[16])
# CHV / UNBLOCK CHV stats
elif file_type in ['working_ef']:
ret['file_size'] = int.from_bytes(resp_bin[2:4], 'big')
file_struct = struct_of_file_map[resp_bin[13]
] if resp_bin[13] in struct_of_file_map else resp_bin[13]
ret['file_descriptor']['file_descriptor_byte']['structure'] = file_struct
if file_struct != 'transparent':
record_len = resp_bin[14]
ret['file_descriptor']['record_len'] = record_len
ret['file_descriptor']['num_of_rec'] = ret['file_size'] // record_len
ret['access_conditions'] = b2h(resp_bin[8:10])
if resp_bin[11] & 0x01 == 0:
ret['life_cycle_status_int'] = 'operational_activated'
@@ -1151,3 +1146,17 @@ class CardProfileSIM(CardProfile):
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_sim(scc)
class AddonSIM(CardProfileAddon):
"""An add-on that can be found on a UICC in order to support classic GSM SIM."""
def __init__(self):
files = [
DF_GSM(),
DF_TELECOM(),
]
super().__init__('SIM', desc='GSM SIM', files_in_mf=files)
def probe(self, card:'CardBase') -> bool:
# we assume the add-on to be present in case DF.GSM is found on the card
return card.file_exists(self.files_in_mf[0].fid)

View File

@@ -6,8 +6,10 @@
import json
import abc
import string
import datetime
import argparse
from io import BytesIO
from typing import Optional, List, Dict, Any, Tuple
from typing import Optional, List, Dict, Any, Tuple, NewType, Union
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
@@ -27,8 +29,10 @@ from typing import Optional, List, Dict, Any, Tuple
#
# just to differentiate strings of hex nibbles from everything else
Hexstr = str
Hexstr = NewType('Hexstr', str)
SwHexstr = NewType('SwHexstr', str)
SwMatchstr = NewType('SwMatchstr', str)
ResTuple = Tuple[Hexstr, SwHexstr]
def h2b(s: Hexstr) -> bytearray:
"""convert from a string of hex nibbles to a sequence of bytes"""
@@ -146,12 +150,12 @@ def comprehensiontlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
# three-byte tag
tag = (binary[1] & 0x7f) << 8
tag |= binary[2]
compr = True if binary[1] & 0x80 else False
compr = bool(binary[1] & 0x80)
return ({'comprehension': compr, 'tag': tag}, binary[3:])
else:
# single byte tag
tag = binary[0] & 0x7f
compr = True if binary[0] & 0x80 else False
compr = bool(binary[0] & 0x80)
return ({'comprehension': compr, 'tag': tag}, binary[1:])
@@ -159,7 +163,7 @@ def comprehensiontlv_encode_tag(tag) -> bytes:
"""Encode a single Tag according to ETSI TS 101 220 Section 7.1.1"""
# permit caller to specify tag also as integer value
if isinstance(tag, int):
compr = True if tag < 0xff and tag & 0x80 else False
compr = bool(tag < 0xff and tag & 0x80)
tag = {'tag': tag, 'comprehension': compr}
compr = tag.get('comprehension', False)
if tag['tag'] in [0x00, 0x80, 0xff] or tag['tag'] > 0xff:
@@ -215,7 +219,7 @@ def bertlv_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
i = 1
last = False
while not last:
last = False if binary[i] & 0x80 else True
last = not bool(binary[i] & 0x80)
tag <<= 8
tag |= binary[i]
i += 1
@@ -230,7 +234,7 @@ def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
Tuple of ({class:int, constructed:bool, tag:int}, remainder:bytes)
"""
cls = binary[0] >> 6
constructed = True if binary[0] & 0x20 else False
constructed = bool(binary[0] & 0x20)
tag = binary[0] & 0x1f
if tag <= 30:
return ({'class': cls, 'constructed': constructed, 'tag': tag}, binary[1:])
@@ -239,7 +243,7 @@ def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
i = 1
last = False
while not last:
last = False if binary[i] & 0x80 else True
last = not bool(binary[i] & 0x80)
tag <<= 7
tag |= binary[i] & 0x7f
i += 1
@@ -260,15 +264,22 @@ def bertlv_encode_tag(t) -> bytes:
remainder = inp & ~ (inp << (remain_bits - bitcnt))
return outp, remainder
def count_int_bytes(inp: int) -> int:
"""count the number of bytes require to represent the given integer."""
i = 1
inp = inp >> 8
while inp:
i += 1
inp = inp >> 8
return i
if isinstance(t, int):
# FIXME: multiple byte tags
tag = t & 0x1f
constructed = True if t & 0x20 else False
cls = t >> 6
else:
tag = t['tag']
constructed = t['constructed']
cls = t['class']
# first convert to a dict representation
tag_size = count_int_bytes(t)
t, _remainder = bertlv_parse_tag(t.to_bytes(tag_size, 'big'))
tag = t['tag']
constructed = t['constructed']
cls = t['class']
if tag <= 30:
t = tag & 0x1f
if constructed:
@@ -305,6 +316,8 @@ def bertlv_parse_len(binary: bytes) -> Tuple[int, bytes]:
else:
num_len_oct = binary[0] & 0x7f
length = 0
if len(binary) < num_len_oct + 1:
return (0, b'')
for i in range(1, 1+num_len_oct):
length <<= 8
length |= binary[i]
@@ -347,6 +360,42 @@ def bertlv_parse_one(binary: bytes) -> Tuple[dict, int, bytes, bytes]:
return (tagdict, length, value, remainder)
def dgi_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
# In absence of any clear spec guidance we assume it's always 16 bit
return int.from_bytes(binary[:2], 'big'), binary[2:]
def dgi_encode_tag(t: int) -> bytes:
return t.to_bytes(2, 'big')
def dgi_encode_len(length: int) -> bytes:
"""Encode a single Length value according to GlobalPlatform Systems Scripting Language
Specification v1.1.0 Annex B.
Args:
length : length value to be encoded
Returns:
binary output data of encoded length field
"""
if length < 255:
return length.to_bytes(1, 'big')
elif length <= 0xffff:
return b'\xff' + length.to_bytes(2, 'big')
else:
raise ValueError("Length > 32bits not supported")
def dgi_parse_len(binary: bytes) -> Tuple[int, bytes]:
"""Parse a single Length value according to GlobalPlatform Systems Scripting Language
Specification v1.1.0 Annex B.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (length, remainder)
"""
if binary[0] == 255:
assert len(binary) >= 3
return ((binary[1] << 8) | binary[2]), binary[3:]
else:
return binary[0], binary[1:]
# IMSI encoded format:
# For IMSI 0123456789ABCDE:
#
@@ -397,6 +446,30 @@ def dec_iccid(ef: Hexstr) -> str:
def enc_iccid(iccid: str) -> Hexstr:
return swap_nibbles(rpad(iccid, 20))
def sanitize_iccid(iccid: Union[int, str]) -> str:
iccid = str(iccid)
if len(iccid) < 18:
raise ValueError('ICCID input value must be at least 18 digits')
if len(iccid) > 20:
raise ValueError('ICCID input value must be at most 20 digits')
if len(iccid) == 18:
# 18 digits means we must add a luhn check digit to reach 19 digits
iccid += str(calculate_luhn(iccid))
if len(iccid) == 20:
# 20 digits means we're actually exceeding E.118 by one digit, and
# the luhn check digit must already be included
verify_luhn(iccid)
if len(iccid) == 19:
# 19 digits means that it's either an in-spec 19-digits ICCID with
# its luhn check digit already present, or it's an out-of-spec 20-digit
# ICCID without that check digit...
try:
verify_luhn(iccid)
except ValueError:
# 19th digit was not luhn check digit; we must add it
iccid += str(calculate_luhn(iccid))
return iccid
def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
"""Converts integer MCC/MNC into 3 bytes for EF"""
@@ -411,7 +484,7 @@ def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
if len(mnc) == 0:
mnc = "FFF"
elif len(mnc) == 1:
mnc = "F0" + mnc
mnc = "0" + mnc + "F"
elif len(mnc) == 2:
mnc += "F"
@@ -433,30 +506,6 @@ def dec_plmn(threehexbytes: Hexstr) -> dict:
return res
def dec_spn(ef):
"""Obsolete, kept for API compatibility"""
from ts_51_011 import EF_SPN
abstract_data = EF_SPN().decode_hex(ef)
show_in_hplmn = abstract_data['show_in_hplmn']
hide_in_oplmn = abstract_data['hide_in_oplmn']
name = abstract_data['spn']
return (name, show_in_hplmn, hide_in_oplmn)
def enc_spn(name: str, show_in_hplmn=False, hide_in_oplmn=False):
"""Obsolete, kept for API compatibility"""
from ts_51_011 import EF_SPN
abstract_data = {
'hide_in_oplmn': hide_in_oplmn,
'show_in_hplmn': show_in_hplmn,
'spn': name,
}
return EF_SPN().encode_hex(abstract_data)
def hexstr_to_Nbytearr(s, nbytes):
return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
# Accepts hex string representing three bytes
@@ -499,33 +548,37 @@ def dec_mnc_from_plmn_str(plmn: Hexstr) -> str:
def dec_act(twohexbytes: Hexstr) -> List[str]:
act_list = [
{'bit': 15, 'name': "UTRAN"},
{'bit': 14, 'name': "E-UTRAN"},
{'bit': 11, 'name': "NG-RAN"},
{'bit': 7, 'name': "GSM"},
{'bit': 6, 'name': "GSM COMPACT"},
{'bit': 5, 'name': "cdma2000 HRPD"},
{'bit': 4, 'name': "cdma2000 1xRTT"},
]
ia = h2i(twohexbytes)
u16t = (ia[0] << 8) | ia[1]
sel = []
sel = set()
# only the simple single-bit ones
for a in act_list:
if u16t & (1 << a['bit']):
if a['name'] == "E-UTRAN":
# The Access technology identifier of E-UTRAN
# allows a more detailed specification:
if u16t & (1 << 13) and u16t & (1 << 12):
sel.append("E-UTRAN WB-S1")
sel.append("E-UTRAN NB-S1")
elif u16t & (1 << 13):
sel.append("E-UTRAN WB-S1")
elif u16t & (1 << 12):
sel.append("E-UTRAN NB-S1")
else:
sel.append("E-UTRAN")
else:
sel.append(a['name'])
return sel
sel.add(a['name'])
# TS 31.102 Section 4.2.5 Table 4.2.5.1
eutran_bits = u16t & 0x7000
if eutran_bits in [0x4000, 0x7000]:
sel.add("E-UTRAN WB-S1")
sel.add("E-UTRAN NB-S1")
elif eutran_bits == 0x5000:
sel.add("E-UTRAN NB-S1")
elif eutran_bits == 0x6000:
sel.add("E-UTRAN WB-S1")
# TS 31.102 Section 4.2.5 Table 4.2.5.2
gsm_bits = u16t & 0x008C
if gsm_bits in [0x0080, 0x008C]:
sel.add("GSM")
sel.add("EC-GSM-IoT")
elif u16t & 0x008C == 0x0084:
sel.add("GSM")
elif u16t & 0x008C == 0x0086:
sel.add("EC-GSM-IoT")
return sorted(list(sel))
def dec_xplmn_w_act(fivehexbytes: Hexstr) -> Dict[str, Any]:
@@ -542,84 +595,23 @@ def dec_xplmn_w_act(fivehexbytes: Hexstr) -> Dict[str, Any]:
return res
def format_xplmn_w_act(hexstr):
s = ""
for rec_data in hexstr_to_Nbytearr(hexstr, 5):
rec_info = dec_xplmn_w_act(rec_data)
if rec_info['mcc'] == "" and rec_info['mnc'] == "":
rec_str = "unused"
else:
rec_str = "MCC: %s MNC: %s AcT: %s" % (
rec_info['mcc'], rec_info['mnc'], ", ".join(rec_info['act']))
s += "\t%s # %s\n" % (rec_data, rec_str)
return s
def dec_loci(hexstr):
res = {'tmsi': '', 'mcc': 0, 'mnc': 0, 'lac': '', 'status': 0}
res['tmsi'] = hexstr[:8]
res['mcc'] = dec_mcc_from_plmn(hexstr[8:14])
res['mnc'] = dec_mnc_from_plmn(hexstr[8:14])
res['lac'] = hexstr[14:18]
res['status'] = h2i(hexstr[20:22])
return res
def dec_psloci(hexstr):
res = {'p-tmsi': '', 'p-tmsi-sig': '', 'mcc': 0,
'mnc': 0, 'lac': '', 'rac': '', 'status': 0}
res['p-tmsi'] = hexstr[:8]
res['p-tmsi-sig'] = hexstr[8:14]
res['mcc'] = dec_mcc_from_plmn(hexstr[14:20])
res['mnc'] = dec_mnc_from_plmn(hexstr[14:20])
res['lac'] = hexstr[20:24]
res['rac'] = hexstr[24:26]
res['status'] = h2i(hexstr[26:28])
return res
def dec_epsloci(hexstr):
res = {'guti': '', 'mcc': 0, 'mnc': 0, 'tac': '', 'status': 0}
res['guti'] = hexstr[:24]
res['tai'] = hexstr[24:34]
res['mcc'] = dec_mcc_from_plmn(hexstr[24:30])
res['mnc'] = dec_mnc_from_plmn(hexstr[24:30])
res['tac'] = hexstr[30:34]
res['status'] = h2i(hexstr[34:36])
return res
def dec_xplmn(threehexbytes: Hexstr) -> dict:
res = {'mcc': 0, 'mnc': 0, 'act': []}
plmn_chars = 6
# first three bytes (six ascii hex chars)
plmn_str = threehexbytes[:plmn_chars]
res['mcc'] = dec_mcc_from_plmn(plmn_str)
res['mnc'] = dec_mnc_from_plmn(plmn_str)
res['mcc'] = dec_mcc_from_plmn_str(plmn_str)
res['mnc'] = dec_mnc_from_plmn_str(plmn_str)
return res
def format_xplmn(hexstr: Hexstr) -> str:
s = ""
for rec_data in hexstr_to_Nbytearr(hexstr, 3):
rec_info = dec_xplmn(rec_data)
if rec_info['mcc'] == 0xFFF and rec_info['mnc'] == 0xFFF:
rec_str = "unused"
else:
rec_str = "MCC: %03d MNC: %03d" % (
rec_info['mcc'], rec_info['mnc'])
s += "\t%s # %s\n" % (rec_data, rec_str)
return s
def derive_milenage_opc(ki_hex: Hexstr, op_hex: Hexstr) -> Hexstr:
"""
Run the milenage algorithm to calculate OPC from Ki and OP
"""
from Crypto.Cipher import AES
from Cryptodome.Cipher import AES
# pylint: disable=no-name-in-module
from Crypto.Util.strxor import strxor
from pySim.utils import b2h
from Cryptodome.Util.strxor import strxor
# We pass in hex string and now need to work on bytes
ki_bytes = bytes(h2b(ki_hex))
@@ -638,12 +630,17 @@ def calculate_luhn(cc) -> int:
for d in num[::-2]]) % 10
return 0 if check_digit == 10 else check_digit
def verify_luhn(digits: str):
"""Verify the Luhn check digit; raises ValueError if it is incorrect."""
cd = calculate_luhn(digits[:-1])
if str(cd) != digits[-1]:
raise ValueError('Luhn check digit mismatch: should be %s but is %s' % (str(cd), digits[-1]))
def mcc_from_imsi(imsi: str) -> Optional[str]:
"""
Derive the MCC (Mobile Country Code) from the first three digits of an IMSI
"""
if imsi == None:
if imsi is None:
return None
if len(imsi) > 3:
@@ -656,7 +653,7 @@ def mnc_from_imsi(imsi: str, long: bool = False) -> Optional[str]:
"""
Derive the MNC (Mobile Country Code) from the 4th to 6th digit of an IMSI
"""
if imsi == None:
if imsi is None:
return None
if len(imsi) > 3:
@@ -762,7 +759,7 @@ def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
"""
# If no MSISDN is supplied then encode the file contents as all "ff"
if msisdn == "" or msisdn == "+":
if msisdn in ["", "+"]:
return "ff" * 14
# Leading '+' indicates International Number
@@ -786,202 +783,6 @@ def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)
def dec_st(st, table="sim") -> str:
"""
Parses the EF S/U/IST and prints the list of available services in EF S/U/IST
"""
if table == "isim":
from pySim.ts_31_103 import EF_IST_map
lookup_map = EF_IST_map
elif table == "usim":
from pySim.ts_31_102 import EF_UST_map
lookup_map = EF_UST_map
else:
from pySim.ts_51_011 import EF_SST_map
lookup_map = EF_SST_map
st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
avail_st = ""
# Get each byte and check for available services
for i in range(0, len(st_bytes)):
# Byte i contains info about Services num (8i+1) to num (8i+8)
byte = int(st_bytes[i], 16)
# Services in each byte are in order MSB to LSB
# MSB - Service (8i+8)
# LSB - Service (8i+1)
for j in range(1, 9):
if byte & 0x01 == 0x01 and ((8*i) + j in lookup_map):
# Byte X contains info about Services num (8X-7) to num (8X)
# bit = 1: service available
# bit = 0: service not available
avail_st += '\tService %d - %s\n' % (
(8*i) + j, lookup_map[(8*i) + j])
byte = byte >> 1
return avail_st
def first_TLV_parser(bytelist):
'''
first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205])
parses first TLV format record in a list of bytelist
returns a 3-Tuple: Tag, Length, Value
Value is a list of bytes
parsing of length is ETSI'style 101.220
'''
Tag = bytelist[0]
if bytelist[1] == 0xFF:
Len = bytelist[2]*256 + bytelist[3]
Val = bytelist[4:4+Len]
else:
Len = bytelist[1]
Val = bytelist[2:2+Len]
return (Tag, Len, Val)
def TLV_parser(bytelist):
'''
TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...]
loops on the input list of bytes with the "first_TLV_parser()" function
returns a list of 3-Tuples
'''
ret = []
while len(bytelist) > 0:
T, L, V = first_TLV_parser(bytelist)
if T == 0xFF:
# padding bytes
break
ret.append((T, L, V))
# need to manage length of L
if L > 0xFE:
bytelist = bytelist[L+4:]
else:
bytelist = bytelist[L+2:]
return ret
def enc_st(st, service, state=1):
"""
Encodes the EF S/U/IST/EST and returns the updated Service Table
Parameters:
st - Current value of SIM/USIM/ISIM Service Table
service - Service Number to encode as activated/de-activated
state - 1 mean activate, 0 means de-activate
Returns:
s - Modified value of SIM/USIM/ISIM Service Table
Default values:
- state: 1 - Sets the particular Service bit to 1
"""
st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
s = ""
# Check whether the requested service is present in each byte
for i in range(0, len(st_bytes)):
# Byte i contains info about Services num (8i+1) to num (8i+8)
if service in range((8*i) + 1, (8*i) + 9):
byte = int(st_bytes[i], 16)
# Services in each byte are in order MSB to LSB
# MSB - Service (8i+8)
# LSB - Service (8i+1)
mod_byte = 0x00
# Copy bit by bit contents of byte to mod_byte with modified bit
# for requested service
for j in range(1, 9):
mod_byte = mod_byte >> 1
if service == (8*i) + j:
mod_byte = state == 1 and mod_byte | 0x80 or mod_byte & 0x7f
else:
mod_byte = byte & 0x01 == 0x01 and mod_byte | 0x80 or mod_byte & 0x7f
byte = byte >> 1
s += ('%02x' % (mod_byte))
else:
s += st_bytes[i]
return s
def dec_addr_tlv(hexstr):
"""
Decode hex string to get EF.P-CSCF Address or EF.ePDGId or EF.ePDGIdEm.
See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
"""
# Convert from hex str to int bytes list
addr_tlv_bytes = h2i(hexstr)
# Get list of tuples containing parsed TLVs
tlvs = TLV_parser(addr_tlv_bytes)
for tlv in tlvs:
# tlv = (T, L, [V])
# T = Tag
# L = Length
# [V] = List of value
# Invalid Tag value scenario
if tlv[0] != 0x80:
continue
# Empty field - Zero length
if tlv[1] == 0:
continue
# First byte in the value has the address type
addr_type = tlv[2][0]
# TODO: Support parsing of IPv6
# Address Type: 0x00 (FQDN), 0x01 (IPv4), 0x02 (IPv6), other (Reserved)
if addr_type == 0x00: # FQDN
# Skip address tye byte i.e. first byte in value list
content = tlv[2][1:]
return (i2s(content), '00')
elif addr_type == 0x01: # IPv4
# Skip address tye byte i.e. first byte in value list
# Skip the unused byte in Octect 4 after address type byte as per 3GPP TS 31.102
ipv4 = tlv[2][2:]
content = '.'.join(str(x) for x in ipv4)
return (content, '01')
else:
raise ValueError("Invalid address type")
return (None, None)
def enc_addr_tlv(addr, addr_type='00'):
"""
Encode address TLV object used in EF.P-CSCF Address, EF.ePDGId and EF.ePDGIdEm.
See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
Default values:
- addr_type: 00 - FQDN format of Address
"""
s = ""
# TODO: Encoding of IPv6 address
if addr_type == '00': # FQDN
hex_str = s2h(addr)
s += '80' + ('%02x' % ((len(hex_str)//2)+1)) + '00' + hex_str
elif addr_type == '01': # IPv4
ipv4_list = addr.split('.')
ipv4_str = ""
for i in ipv4_list:
ipv4_str += ('%02x' % (int(i)))
# Unused bytes shall be set to 'ff'. i.e 4th Octet after Address Type is not used
# IPv4 Address is in octet 5 to octet 8 of the TLV data object
s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
return s
def is_hex(string: str, minlen: int = 2, maxlen: Optional[int] = None) -> bool:
"""
Check if a string is a valid hexstring
@@ -999,7 +800,7 @@ def is_hex(string: str, minlen: int = 2, maxlen: Optional[int] = None) -> bool:
# Try actual encoding to be sure
try:
try_encode = h2b(string)
_try_encode = h2b(string)
return True
except:
return False
@@ -1027,98 +828,14 @@ def sanitize_pin_adm(pin_adm, pin_adm_hex=None) -> Hexstr:
# Ensure that it's hex-encoded
try:
try_encode = h2b(pin_adm)
except ValueError:
raise ValueError(
"PIN-ADM needs to be hex encoded using this option")
except ValueError as exc:
raise ValueError("PIN-ADM needs to be hex encoded using this option") from exc
else:
raise ValueError(
"PIN-ADM needs to be exactly 16 digits (hex encoded)")
raise ValueError("PIN-ADM needs to be exactly 16 digits (hex encoded)")
return pin_adm
def enc_ePDGSelection(hexstr, mcc, mnc, epdg_priority='0001', epdg_fqdn_format='00'):
"""
Encode ePDGSelection so it can be stored at EF.ePDGSelection or EF.ePDGSelectionEm.
See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
Default values:
- epdg_priority: '0001' - 1st Priority
- epdg_fqdn_format: '00' - Operator Identifier FQDN
"""
plmn1 = enc_plmn(mcc, mnc) + epdg_priority + epdg_fqdn_format
# TODO: Handle encoding of Length field for length more than 127 Bytes
content = '80' + ('%02x' % (len(plmn1)//2)) + plmn1
content = rpad(content, len(hexstr))
return content
def dec_ePDGSelection(sixhexbytes):
"""
Decode ePDGSelection to get EF.ePDGSelection or EF.ePDGSelectionEm.
See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
"""
res = {'mcc': 0, 'mnc': 0, 'epdg_priority': 0, 'epdg_fqdn_format': ''}
plmn_chars = 6
epdg_priority_chars = 4
epdg_fqdn_format_chars = 2
# first three bytes (six ascii hex chars)
plmn_str = sixhexbytes[:plmn_chars]
# two bytes after first three bytes
epdg_priority_str = sixhexbytes[plmn_chars:plmn_chars +
epdg_priority_chars]
# one byte after first five bytes
epdg_fqdn_format_str = sixhexbytes[plmn_chars +
epdg_priority_chars:plmn_chars + epdg_priority_chars + epdg_fqdn_format_chars]
res['mcc'] = dec_mcc_from_plmn(plmn_str)
res['mnc'] = dec_mnc_from_plmn(plmn_str)
res['epdg_priority'] = epdg_priority_str
res['epdg_fqdn_format'] = epdg_fqdn_format_str == '00' and 'Operator Identifier FQDN' or 'Location based FQDN'
return res
def format_ePDGSelection(hexstr):
ePDGSelection_info_tag_chars = 2
ePDGSelection_info_tag_str = hexstr[:2]
s = ""
# Minimum length
len_chars = 2
# TODO: Need to determine length properly - definite length support only
# Inconsistency in spec: 3GPP TS 31.102 version 15.2.0 Release 15, 4.2.104
# As per spec, length is 5n, n - number of PLMNs
# But, each PLMN entry is made of PLMN (3 Bytes) + ePDG Priority (2 Bytes) + ePDG FQDN format (1 Byte)
# Totalling to 6 Bytes, maybe length should be 6n
len_str = hexstr[ePDGSelection_info_tag_chars:ePDGSelection_info_tag_chars+len_chars]
# Not programmed scenario
if int(len_str, 16) == 255 or int(ePDGSelection_info_tag_str, 16) == 255:
len_chars = 0
ePDGSelection_info_tag_chars = 0
if len_str[0] == '8':
# The bits 7 to 1 denotes the number of length octets if length > 127
if int(len_str[1]) > 0:
# Update number of length octets
len_chars = len_chars * int(len_str[1])
len_str = hexstr[ePDGSelection_info_tag_chars:len_chars]
content_str = hexstr[ePDGSelection_info_tag_chars+len_chars:]
# Right pad to prevent index out of range - multiple of 6 bytes
content_str = rpad(content_str, len(content_str) +
(12 - (len(content_str) % 12)))
for rec_data in hexstr_to_Nbytearr(content_str, 6):
rec_info = dec_ePDGSelection(rec_data)
if rec_info['mcc'] == 0xFFF and rec_info['mnc'] == 0xFFF:
rec_str = "unused"
else:
rec_str = "MCC: %03d MNC: %03d ePDG Priority: %s ePDG FQDN format: %s" % \
(rec_info['mcc'], rec_info['mnc'],
rec_info['epdg_priority'], rec_info['epdg_fqdn_format'])
s += "\t%s # %s\n" % (rec_data, rec_str)
return s
def get_addr_type(addr):
"""
Validates the given address and returns it's type (FQDN or IPv4 or IPv6)
@@ -1128,7 +845,7 @@ def get_addr_type(addr):
"""
# Empty address string
if not len(addr):
if len(addr) == 0:
return None
addr_list = addr.split('.')
@@ -1143,7 +860,7 @@ def get_addr_type(addr):
return 0x01
elif ipa.version == 6:
return 0x02
except Exception as e:
except Exception:
invalid_ipv4 = True
for i in addr_list:
# Invalid IPv4 may qualify for a valid FQDN, so make check here
@@ -1199,7 +916,7 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
Returns:
multi-line string containing formatted table
"""
if str_list == None:
if str_list is None:
return ""
if len(str_list) <= 0:
return ""
@@ -1210,7 +927,7 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
table = []
for i in iter(range(rows)):
str_list_row = str_list[i::rows]
if (align_left):
if align_left:
format_str_cell = '%%-%ds'
else:
format_str_cell = '%%%ds'
@@ -1224,6 +941,21 @@ def auto_int(x):
"""Helper function for argparse to accept hexadecimal integers."""
return int(x, 0)
def _auto_uint(x, max_val: int):
"""Helper function for argparse to accept hexadecimal or decimal integers."""
ret = int(x, 0)
if ret < 0 or ret > max_val:
raise argparse.ArgumentTypeError('Number exceeds permited value range (0, %u)' % max_val)
return ret
def auto_uint7(x):
return _auto_uint(x, 127)
def auto_uint8(x):
return _auto_uint(x, 255)
def auto_uint16(x):
return _auto_uint(x, 65535)
def expand_hex(hexstring, length):
"""Expand a given hexstring to a specified length by replacing "." or ".."
@@ -1283,8 +1015,10 @@ class JsonEncoder(json.JSONEncoder):
"""Extend the standard library JSONEncoder with support for more types."""
def default(self, o):
if isinstance(o, BytesIO) or isinstance(o, bytes) or isinstance(o, bytearray):
if isinstance(o, (BytesIO, bytes, bytearray)):
return b2h(o)
elif isinstance(o, datetime.datetime):
return o.isoformat()
return json.JSONEncoder.default(self, o)
@@ -1701,3 +1435,30 @@ class CardCommandSet:
def all_subclasses(cls) -> set:
"""Recursively get all subclasses of a specified class"""
return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])
def is_hexstr_or_decimal(instr: str) -> str:
"""Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
[hexa]decimal digits only."""
if instr.isdecimal():
return instr
if not all(c in string.hexdigits for c in instr):
raise ValueError('Input must be [hexa]decimal')
if len(instr) & 1:
raise ValueError('Input has un-even number of hex digits')
return instr
def is_hexstr(instr: str) -> str:
"""Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
an even sequence of hexadecimal digits only."""
if not all(c in string.hexdigits for c in instr):
raise ValueError('Input must be hexadecimal')
if len(instr) & 1:
raise ValueError('Input has un-even number of hex digits')
return instr
def is_decimal(instr: str) -> str:
"""Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
an even sequence of decimal digits only."""
if not instr.isdecimal():
raise ValueError('Input must decimal')
return instr

View File

@@ -31,7 +31,7 @@ OPLMNwAcT:
ffffff0000 # unused
HPLMNAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -88,6 +88,18 @@ SIM Service Table: ff3cc3ff030fff0f000fff03f0c0
Service 58 - Extension 8
Service 59 - MMS User Connectivity Parameters
FPLMN:
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
USIM Service Table: 01ea1ffc21360480010000
Service 1 - Local Phone Book
Service 10 - Short Message Storage (SMS)

View File

@@ -11,7 +11,7 @@ Show in HPLMN: False
Hide in OPLMN: False
PLMNsel: 00f110ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PLMNwAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -29,7 +29,7 @@ PLMNwAcT:
ffffff0000 # unused
OPLMNwAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -93,6 +93,12 @@ SIM Service Table: ff33ff0f3c00ff0f000cf0c0f0030000
Service 58 - Extension 8
Service 59 - MMS User Connectivity Parameters
FPLMN:
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused
USIM Service Table: 9eff1b3c37fe5900000000
Service 2 - Fixed Dialling Numbers (FDN)
Service 3 - Extension 2

View File

@@ -1,7 +1,6 @@
Using PC/SC reader interface
Reading ...
Autodetected card type: fakemagicsim
Can't read AIDs from SIM -- SW match failed! Expected 9000 and got 9404.
ICCID: 1122334455667788990
IMSI: 001010000000102
GID1: Can't read file -- SW match failed! Expected 9000 and got 9404.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@ Show in HPLMN: True
Hide in OPLMN: True
PLMNsel: 00f110ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PLMNwAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -25,7 +25,7 @@ PLMNwAcT:
ffffff0000 # unused
OPLMNwAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -39,7 +39,7 @@ OPLMNwAcT:
ffffff0000 # unused
HPLMNAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -103,7 +103,13 @@ SIM Service Table: ff33ffff3f003f0f300cf0c3f00000
Service 59 - MMS User Connectivity Parameters
EHPLMN:
00f110 # MCC: 001 MNC: 001
00f110 # MCC: 001 MNC: 01
ffffff # unused
ffffff # unused
ffffff # unused
FPLMN:
ffffff # unused
ffffff # unused
ffffff # unused
ffffff # unused

View File

@@ -11,7 +11,7 @@ Show in HPLMN: True
Hide in OPLMN: True
PLMNsel: 00f110ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PLMNwAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -25,7 +25,7 @@ PLMNwAcT:
ffffff0000 # unused
OPLMNwAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -39,7 +39,7 @@ OPLMNwAcT:
ffffff0000 # unused
HPLMNAcT:
00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -104,6 +104,12 @@ SIM Service Table: ff3fffff3f003f1ff00c00c0f00000
Service 58 - Extension 8
Service 59 - MMS User Connectivity Parameters
FPLMN:
62f201 # MCC: 262 MNC: 10
62f202 # MCC: 262 MNC: 20
62f203 # MCC: 262 MNC: 30
62f207 # MCC: 262 MNC: 70
USIM Service Table: 9e6b1dfc67f6580000
Service 2 - Fixed Dialling Numbers (FDN)
Service 3 - Extension 2

View File

@@ -1,7 +1,6 @@
Using PC/SC reader interface
Reading ...
Autodetected card type: sysmosim-gr1
Can't read AIDs from SIM -- SW match failed! Expected 9000 and got 9404.
ICCID: 1122334455667788990
IMSI: 001010000000102
GID1: Can't read file -- SW match failed! Expected 9000 and got 9404.

View File

@@ -1,7 +1,7 @@
pyscard
pyserial
pytlv
cmd2==1.5
cmd2>=1.5
jsonpath-ng
construct>=2.9.51
bidict
@@ -9,3 +9,8 @@ gsm0338
pyyaml>=5.1
termcolor
colorlog
pycryptodomex
cryptography
git+https://github.com/osmocom/asn1tools
packaging
git+https://github.com/hologram-io/smpp.pdu

100
saip-test.py Executable file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
from pySim.utils import b2h, h2b
from pySim.esim.saip import *
from pySim.esim.saip.validation import *
from pySim.pprint import HexBytesPrettyPrinter
pp = HexBytesPrettyPrinter(indent=4,width=500)
import abc
with open('smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der', 'rb') as f:
pes = ProfileElementSequence.from_der(f.read())
if False:
# iterate over each pe in the pes.pe_list
for pe in pes.pe_list:
print("="*70 + " " + pe.type)
pp.pprint(pe.decoded)
if False:
# sort by PE type and show all PE within that type
for pe_type in pes.pe_by_type.keys():
print("="*70 + " " + pe_type)
for pe in pes.pe_by_type[pe_type]:
pp.pprint(pe)
pp.pprint(pe.decoded)
checker = CheckBasicStructure()
checker.check(pes)
if False:
for naa in pes.pes_by_naa:
i = 0
for naa_instance in pes.pes_by_naa[naa]:
print("="*70 + " " + naa + str(i))
i += 1
for pe in naa_instance:
pp.pprint(pe.type)
for d in pe.decoded:
print(" %s" % d)
#pp.pprint(pe.decoded[d])
#if pe.type in ['akaParameter', 'pinCodes', 'pukCodes']:
# pp.pprint(pe.decoded)
from pySim.esim.saip.personalization import *
params = [Iccid('984944000000000000'), Imsi('901990123456789'),
Puk1(value='01234567'), Puk2(value='98765432'), Pin1('1111'), Pin2('2222'), Adm1('11111111'),
K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f')),
SdKeyScp80_01Kic(h2b('000102030405060708090a0b0c0d0e0f'))]
from pySim.esim.saip.templates import *
for p in params:
p.apply(pes)
if False:
for pe in pes:
pp.pprint(pe.decoded)
pass
if True:
naas = pes.pes_by_naa.keys()
for naa in naas:
for pe in pes.pes_by_naa[naa][0]:
print(pe)
#pp.pprint(pe.decoded)
#print(pe.header)
tpl_id = pe.templateID
if tpl_id:
prof = ProfileTemplateRegistry.get_by_oid(tpl_id)
print(prof)
#pp.pprint(pe.decoded)
for fname, fdata in pe.files.items():
print()
print("============== %s" % fname)
ftempl = None
if prof:
ftempl = prof.files_by_pename[fname]
print("Template: %s" % repr(ftempl))
print("Data: %s" % fdata)
file = File(fname, fdata, ftempl)
print(repr(file))
#pp.pprint(pe.files)
if True:
# iterate over each pe in the pes (using its __iter__ method)
for pe in pes:
print("="*70 + " " + pe.type)
pp.pprint(pe.decoded)
#print(ProfileTemplateRegistry.by_oid)

Some files were not shown because too many files have changed in this diff Show More