mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-16 18:38:32 +03:00
Compare commits
429 Commits
laforge/sm
...
laforge/ws
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9bb63a2db | ||
|
|
a01e87da77 | ||
|
|
53e840ad86 | ||
|
|
d15c3d1319 | ||
|
|
671b0f19b6 | ||
|
|
de8cc322f1 | ||
|
|
385d4407da | ||
|
|
852eff54df | ||
|
|
f951c56449 | ||
|
|
90881a2fff | ||
|
|
4aaccf8751 | ||
|
|
3ef2c40951 | ||
|
|
b845aab473 | ||
|
|
30c59fce42 | ||
|
|
ec30022b1a | ||
|
|
daa1c74047 | ||
|
|
5887fb70fb | ||
|
|
882e24677f | ||
|
|
f4c156ae57 | ||
|
|
59593e0f28 | ||
|
|
35b9b3c542 | ||
|
|
464d1ac2be | ||
|
|
909b8c1611 | ||
|
|
5d54f3b8d8 | ||
|
|
98f4ea1447 | ||
|
|
32d6a9ab5f | ||
|
|
d8d52bdf77 | ||
|
|
12328c090d | ||
|
|
ba22e238f3 | ||
|
|
f9631fb361 | ||
|
|
f4dd9b5ceb | ||
|
|
82b0f1b39a | ||
|
|
3a905d637c | ||
|
|
a8cfeb0111 | ||
|
|
7c62fc5ec4 | ||
|
|
7429bc0ca0 | ||
|
|
93c89856c8 | ||
|
|
1f45799188 | ||
|
|
10ea4a0714 | ||
|
|
dc2ca5d6be | ||
|
|
39552464d8 | ||
|
|
4045146f62 | ||
|
|
efddffe015 | ||
|
|
78c22a7d63 | ||
|
|
d96d04718e | ||
|
|
7b95fac022 | ||
|
|
c09d4cc6b8 | ||
|
|
f87a00c04f | ||
|
|
d7032955c5 | ||
|
|
26ee39bebf | ||
|
|
01a96cd8e4 | ||
|
|
dca641aaa2 | ||
|
|
154e29c89a | ||
|
|
d5ddd04f33 | ||
|
|
6942a40909 | ||
|
|
9a6425b6f2 | ||
|
|
94ecf9a929 | ||
|
|
3eb74829df | ||
|
|
3dc0496913 | ||
|
|
39e4a4b7c5 | ||
|
|
87e1ba6c18 | ||
|
|
ad3d73e734 | ||
|
|
8e42a12048 | ||
|
|
84857accf3 | ||
|
|
72186cce84 | ||
|
|
5f2dfc28ff | ||
|
|
bd762c77ae | ||
|
|
492379e61a | ||
|
|
7633a11239 | ||
|
|
07b67439f8 | ||
|
|
c3fe111c0e | ||
|
|
2fe9b6a3e9 | ||
|
|
241d65db12 | ||
|
|
bf0689a48e | ||
|
|
726097e51f | ||
|
|
ee9ac2f7ff | ||
|
|
398fdd7e8c | ||
|
|
639806cc5a | ||
|
|
471162dc76 | ||
|
|
f81331808f | ||
|
|
bd7c21257c | ||
|
|
6aabb92c38 | ||
|
|
b22bab0b20 | ||
|
|
981220641d | ||
|
|
73dd3d0637 | ||
|
|
65cbe48953 | ||
|
|
52735f3685 | ||
|
|
9036d6d3fb | ||
|
|
a3962b2076 | ||
|
|
a437d11135 | ||
|
|
aa182e9815 | ||
|
|
4d1f4fde4f | ||
|
|
33256ddfed | ||
|
|
f0034e4fe8 | ||
|
|
df08441472 | ||
|
|
4d99c2b204 | ||
|
|
eb4ca1189c | ||
|
|
8ac2647004 | ||
|
|
e0241037e7 | ||
|
|
8680698f97 | ||
|
|
a90bf12ea1 | ||
|
|
c595221bc3 | ||
|
|
d8637f3a70 | ||
|
|
caabee4ccb | ||
|
|
cc4c021bb1 | ||
|
|
1034a9749f | ||
|
|
f807983a98 | ||
|
|
8c1a1c5cc5 | ||
|
|
d5943934a5 | ||
|
|
edf266726d | ||
|
|
d20be98ed1 | ||
|
|
585e16a923 | ||
|
|
1f92031079 | ||
|
|
89dbdbdccc | ||
|
|
a5e2a8dbfd | ||
|
|
a86b1abc03 | ||
|
|
6d4c566fd7 | ||
|
|
c6f8457ff1 | ||
|
|
5e2b93eb55 | ||
|
|
cd22b9aee3 | ||
|
|
39613da6a7 | ||
|
|
ab3e04fdb1 | ||
|
|
3a95fa12f6 | ||
|
|
b349149a88 | ||
|
|
3b30994ff0 | ||
|
|
6a1e5eb4ee | ||
|
|
31c3c9a1e3 | ||
|
|
6d495fb24d | ||
|
|
01ddec2fdc | ||
|
|
b2970d4bbe | ||
|
|
1f477495ec | ||
|
|
97dfcaa9c7 | ||
|
|
022d562ae1 | ||
|
|
89dff98fb6 | ||
|
|
526fdae6e5 | ||
|
|
c421645ba6 | ||
|
|
12cc6821c4 | ||
|
|
8597b64ee6 | ||
|
|
2d235f8143 | ||
|
|
b92f4f52cc | ||
|
|
03901cc9ce | ||
|
|
b4530e71b7 | ||
|
|
7d9c6583ef | ||
|
|
4515f1cf87 | ||
|
|
10e9e97724 | ||
|
|
8f5fd37b4a | ||
|
|
ca1b00f99e | ||
|
|
465d1a07e0 | ||
|
|
44d51a7b16 | ||
|
|
5b513a543f | ||
|
|
46bc37fa65 | ||
|
|
19328e3bbd | ||
|
|
d2254377b6 | ||
|
|
041a1b33fc | ||
|
|
d3a6bbc215 | ||
|
|
08d7c10211 | ||
|
|
fdae0ff90d | ||
|
|
7c06bcdd57 | ||
|
|
d81c2086c8 | ||
|
|
d3fb38965b | ||
|
|
4fd3fa445c | ||
|
|
4f9ee0fa75 | ||
|
|
6b1c6a986c | ||
|
|
3d6a712e8c | ||
|
|
8b1060a30e | ||
|
|
e354ef7d05 | ||
|
|
e3e964589f | ||
|
|
cf65d92039 | ||
|
|
f3b3ba15b8 | ||
|
|
bff8902ce1 | ||
|
|
de5de0e9db | ||
|
|
d29f244aad | ||
|
|
eda408fba3 | ||
|
|
2a963a7ac0 | ||
|
|
75a109419c | ||
|
|
d25ea35e7e | ||
|
|
6d2e385acf | ||
|
|
e931966a06 | ||
|
|
2c0e3358a7 | ||
|
|
43fc875168 | ||
|
|
dff7bb0687 | ||
|
|
4fefac78b8 | ||
|
|
7858f591fe | ||
|
|
d29bdbc2c8 | ||
|
|
34dce409b9 | ||
|
|
c60944a7de | ||
|
|
0c022944ff | ||
|
|
4f2a6ebf1f | ||
|
|
f26042f92d | ||
|
|
9aeadea4c3 | ||
|
|
c78ea1ffa6 | ||
|
|
2cca36e8fd | ||
|
|
87b4f99a90 | ||
|
|
c800f2a716 | ||
|
|
699b49ef1b | ||
|
|
d93d774dcc | ||
|
|
289d2343fa | ||
|
|
03eae595a3 | ||
|
|
f174ad6885 | ||
|
|
6f5a0498bf | ||
|
|
fb56f35546 | ||
|
|
282aeadcc4 | ||
|
|
92bae20b49 | ||
|
|
e18586ddf0 | ||
|
|
03194c0877 | ||
|
|
84077f239f | ||
|
|
5370178ca2 | ||
|
|
3ad3da8995 | ||
|
|
9d0c2947f1 | ||
|
|
0519e2b7e1 | ||
|
|
96e2a521e9 | ||
|
|
23dd13542e | ||
|
|
5fdfa1463e | ||
|
|
c805f00bff | ||
|
|
12902730bf | ||
|
|
0c40a2245b | ||
|
|
dacacd206d | ||
|
|
b865d383aa | ||
|
|
1c2ec93164 | ||
|
|
76b3488829 | ||
|
|
37320da4ab | ||
|
|
b5679386d7 | ||
|
|
03aebf5b43 | ||
|
|
5f9b8a8fc1 | ||
|
|
3b7e2ae2c1 | ||
|
|
2668eb6148 | ||
|
|
3c530c3c1a | ||
|
|
992e60902a | ||
|
|
292191d67a | ||
|
|
c0ea149555 | ||
|
|
200bf6eb8b | ||
|
|
698886247f | ||
|
|
b6532b56d2 | ||
|
|
3d70f659f3 | ||
|
|
ecb65bc2f2 | ||
|
|
f36e9fd39f | ||
|
|
36276e7b2a | ||
|
|
5341bf902f | ||
|
|
5964bdd5a4 | ||
|
|
1aa77c5d74 | ||
|
|
32401a54e6 | ||
|
|
8bd551af32 | ||
|
|
1a9cabbbf0 | ||
|
|
4a191089dc | ||
|
|
3b4a673de4 | ||
|
|
a5634c248b | ||
|
|
cdf661b24c | ||
|
|
05349a0c65 | ||
|
|
144bae3f37 | ||
|
|
4680503acc | ||
|
|
0cb0e02c5c | ||
|
|
50d9e2a6d8 | ||
|
|
888c6e5647 | ||
|
|
f07161d396 | ||
|
|
0d1dea01df | ||
|
|
f1495c1e4e | ||
|
|
7b3d4b805c | ||
|
|
2c39d81b4b | ||
|
|
2eea70f6bc | ||
|
|
f22637f151 | ||
|
|
5529a41a63 | ||
|
|
33a6daee6d | ||
|
|
16749075f9 | ||
|
|
add30ecbff | ||
|
|
1aaf978d9f | ||
|
|
a3d41a147f | ||
|
|
0251367ddb | ||
|
|
bc949649da | ||
|
|
4d5d2f5849 | ||
|
|
77256d0c48 | ||
|
|
80976b65e5 | ||
|
|
fe28a1d87d | ||
|
|
ee7be44528 | ||
|
|
2755b54ded | ||
|
|
ddbfc043ac | ||
|
|
64a5901c4c | ||
|
|
56912caac7 | ||
|
|
3dabbafdba | ||
|
|
e4450afb4e | ||
|
|
7f6102365c | ||
|
|
f47433863e | ||
|
|
3ba10b61e1 | ||
|
|
a823ce89f6 | ||
|
|
8844603941 | ||
|
|
6add18ea08 | ||
|
|
56264669a7 | ||
|
|
172c9f7ca6 | ||
|
|
daeba3c1fb | ||
|
|
91ec099680 | ||
|
|
568d8cf5db | ||
|
|
a3f22ea259 | ||
|
|
81bc26cc31 | ||
|
|
c3d04ab193 | ||
|
|
bb2cba83c5 | ||
|
|
45b7d0126b | ||
|
|
73a5c74114 | ||
|
|
a644fecc01 | ||
|
|
900b04559b | ||
|
|
57df6f6e68 | ||
|
|
1d1ba8e4cc | ||
|
|
b2b29cfed1 | ||
|
|
7aeeb4f475 | ||
|
|
f3432eef4c | ||
|
|
60eef0264a | ||
|
|
0c5dfd9d23 | ||
|
|
a412c436b4 | ||
|
|
479aeb0b00 | ||
|
|
24a7f168bd | ||
|
|
3aa0b41f39 | ||
|
|
7b524fa079 | ||
|
|
ee4db7010b | ||
|
|
2c219cd706 | ||
|
|
decb468092 | ||
|
|
b18c7d9be0 | ||
|
|
6d63712b51 | ||
|
|
2de552e712 | ||
|
|
19fa98e7d0 | ||
|
|
318faef583 | ||
|
|
aa76546d16 | ||
|
|
8449b14d08 | ||
|
|
922b8a279c | ||
|
|
7d88b076ad | ||
|
|
5ff0bafcda | ||
|
|
d16a20ccc3 | ||
|
|
54b4f0ccbd | ||
|
|
efdf423a7f | ||
|
|
979c837286 | ||
|
|
1432af5150 | ||
|
|
eac459fe24 | ||
|
|
95873a964e | ||
|
|
e1c0b626d8 | ||
|
|
d6ecf272f5 | ||
|
|
9d1487af6d | ||
|
|
908634396f | ||
|
|
55be7d48ee | ||
|
|
6db681924c | ||
|
|
f2b20bf6ca | ||
|
|
472165f20f | ||
|
|
f2322774c7 | ||
|
|
8829f8e690 | ||
|
|
09f9663005 | ||
|
|
356a6c0f99 | ||
|
|
f01c4b2c98 | ||
|
|
a5fafe8b48 | ||
|
|
a5630dc45c | ||
|
|
4b56c6cd3e | ||
|
|
0d9c8f73a8 | ||
|
|
4f3976d77f | ||
|
|
4c0b80415e | ||
|
|
530bf73cbc | ||
|
|
5f6b64dc25 | ||
|
|
1258e464a1 | ||
|
|
2b84644c08 | ||
|
|
f235b799e9 | ||
|
|
e6e74229c9 | ||
|
|
9ef65099d2 | ||
|
|
0930bcbbb7 | ||
|
|
295d4a4907 | ||
|
|
9bc016e777 | ||
|
|
528d922510 | ||
|
|
c5ff0a6ab5 | ||
|
|
49d69335b2 | ||
|
|
fdaefd9a8a | ||
|
|
7781c70c09 | ||
|
|
181becb676 | ||
|
|
c4d80870e8 | ||
|
|
f5a8e70f44 | ||
|
|
fd9188d306 | ||
|
|
6088b554ef | ||
|
|
eb18ed08b0 | ||
|
|
33cd964c1a | ||
|
|
e8439d9639 | ||
|
|
8e7d28cad7 | ||
|
|
cb4c0cf1e8 | ||
|
|
c5c9728127 | ||
|
|
f57912ea15 | ||
|
|
0f2ac70397 | ||
|
|
62bd7d3df2 | ||
|
|
2bb2ff4aeb | ||
|
|
7156a40187 | ||
|
|
cd8e16fdfe | ||
|
|
e55fcf66bf | ||
|
|
bc8e2e1664 | ||
|
|
57f73f8de7 | ||
|
|
af8826a02b | ||
|
|
13a1723c2e | ||
|
|
afd89ca36d | ||
|
|
a30ee17246 | ||
|
|
bdf8419966 | ||
|
|
a7eaefc8d9 | ||
|
|
4d5fd25f31 | ||
|
|
321973ad20 | ||
|
|
41a7379a4f | ||
|
|
762a72b308 | ||
|
|
a2f1654051 | ||
|
|
eecef54eee | ||
|
|
5918345c78 | ||
|
|
93bdf00967 | ||
|
|
d7715043a3 | ||
|
|
8a39d00cc3 | ||
|
|
3f3fd1a841 | ||
|
|
263e3094ba | ||
|
|
e815e79db9 | ||
|
|
9f55da998f | ||
|
|
488427993d | ||
|
|
0bce94996f | ||
|
|
3d6df6ce13 | ||
|
|
7f2263b4a0 | ||
|
|
9b1a9d9b2e | ||
|
|
5e0439f881 | ||
|
|
9fd4bbe42e | ||
|
|
18d0a7de96 | ||
|
|
280a9a3408 | ||
|
|
e6124b0aba | ||
|
|
6dadb6c215 | ||
|
|
af87cd544f | ||
|
|
45b7dc9466 | ||
|
|
c83a963877 | ||
|
|
667d589f20 | ||
|
|
ebb6f7f938 | ||
|
|
0311c92e96 | ||
|
|
66b337079a | ||
|
|
4f3d11b378 | ||
|
|
cd18ed0a82 | ||
|
|
ecfb09037e | ||
|
|
1f7a9bd5b4 | ||
|
|
d5be46ae7e | ||
|
|
7ba09f9392 | ||
|
|
91842b471d |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
open_collective: osmocom
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,3 +3,7 @@
|
||||
|
||||
/docs/_*
|
||||
/docs/generated
|
||||
/.cache
|
||||
/.local
|
||||
/build
|
||||
/pySim.egg-info
|
||||
|
||||
23
README.md
23
README.md
@@ -77,7 +77,7 @@ Please install the following dependencies:
|
||||
- cmd2 >= 1.5.0
|
||||
- colorlog
|
||||
- construct >= 2.9.51
|
||||
- gsm0338
|
||||
- pyosmocom
|
||||
- jsonpath-ng
|
||||
- packaging
|
||||
- pycryptodomex
|
||||
@@ -123,19 +123,34 @@ sudo pacman -Rs python-pysim-git
|
||||
```
|
||||
|
||||
|
||||
Forum
|
||||
-----
|
||||
|
||||
We welcome any pySim related discussions in the
|
||||
[SIM Card Technology](https://discourse.osmocom.org/c/sim-card-technology/)
|
||||
section of the osmocom discourse (web based Forum).
|
||||
|
||||
|
||||
Mailing List
|
||||
------------
|
||||
|
||||
There is no separate mailing list for this project. However,
|
||||
discussions related to pysim-prog are happening on the
|
||||
<openbsc@lists.osmocom.org> mailing list, please see
|
||||
<https://lists.osmocom.org/mailman/listinfo/openbsc> for subscription
|
||||
discussions related to pySim are happening on the simtrace
|
||||
<simtrace@lists.osmocom.org> mailing list, please see
|
||||
<https://lists.osmocom.org/mailman/listinfo/simtrace> for subscription
|
||||
options and the list archive.
|
||||
|
||||
Please observe the [Osmocom Mailing List
|
||||
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
|
||||
when posting.
|
||||
|
||||
Issue Tracker
|
||||
-------------
|
||||
|
||||
We use the [issue tracker of the pysim project on osmocom.org](https://osmocom.org/projects/pysim/issues) for
|
||||
tracking the state of bug reports and feature requests. Feel free to submit any issues you may find, or help
|
||||
us out by resolving existing issues.
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
79
contrib/csv-encrypt-columns.py
Executable file
79
contrib/csv-encrypt-columns.py
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Utility program to perform column-based encryption of a CSV file holding SIM/UICC
|
||||
# related key materials.
|
||||
#
|
||||
# (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 csv
|
||||
import argparse
|
||||
from Cryptodome.Cipher import AES
|
||||
from osmocom.utils import h2b, b2h, Hexstr
|
||||
|
||||
from pySim.card_key_provider import CardKeyProviderCsv
|
||||
|
||||
def dict_keys_to_upper(d: dict) -> dict:
|
||||
return {k.upper():v for k,v in d.items()}
|
||||
|
||||
class CsvColumnEncryptor:
|
||||
def __init__(self, filename: str, transport_keys: dict):
|
||||
self.filename = filename
|
||||
self.transport_keys = dict_keys_to_upper(transport_keys)
|
||||
|
||||
def encrypt_col(self, colname:str, value: str) -> Hexstr:
|
||||
key = self.transport_keys[colname]
|
||||
cipher = AES.new(h2b(key), AES.MODE_CBC, CardKeyProviderCsv.IV)
|
||||
return b2h(cipher.encrypt(h2b(value)))
|
||||
|
||||
def encrypt(self) -> None:
|
||||
with open(self.filename, 'r') as infile:
|
||||
cr = csv.DictReader(infile)
|
||||
cr.fieldnames = [field.upper() for field in cr.fieldnames]
|
||||
|
||||
with open(self.filename + '.encr', 'w') as outfile:
|
||||
cw = csv.DictWriter(outfile, dialect=csv.unix_dialect, fieldnames=cr.fieldnames)
|
||||
cw.writeheader()
|
||||
|
||||
for row in cr:
|
||||
for key_colname in self.transport_keys:
|
||||
if key_colname in row:
|
||||
row[key_colname] = self.encrypt_col(key_colname, row[key_colname])
|
||||
cw.writerow(row)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('CSVFILE', help="CSV file name")
|
||||
parser.add_argument('--csv-column-key', action='append', required=True,
|
||||
help='per-CSV-column AES transport key')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
csv_column_keys = {}
|
||||
for par in opts.csv_column_key:
|
||||
name, key = par.split(':')
|
||||
csv_column_keys[name] = key
|
||||
|
||||
if len(csv_column_keys) == 0:
|
||||
print("You must specify at least one key!")
|
||||
sys.exit(1)
|
||||
|
||||
csv_column_keys = CardKeyProviderCsv.process_transport_keys(csv_column_keys)
|
||||
for name, key in csv_column_keys.items():
|
||||
print("Encrypting column %s using AES key %s" % (name, key))
|
||||
|
||||
cce = CsvColumnEncryptor(opts.CSVFILE, csv_column_keys)
|
||||
cce.encrypt()
|
||||
73
contrib/eidtool.py
Executable file
73
contrib/eidtool.py
Executable 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
79
contrib/es2p_client.py
Executable 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)
|
||||
318
contrib/es9p_client.py
Executable file
318
contrib/es9p_client.py
Executable file
@@ -0,0 +1,318 @@
|
||||
#!/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 os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
from osmocom.utils import h2b, b2h, swap_nibbles, is_hexstr
|
||||
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim import es9p, PMO
|
||||
from pySim.esim.x509_cert import CertAndPrivkey
|
||||
from pySim.esim.es8p import BoundProfilePackage
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
parser = argparse.ArgumentParser(description="""
|
||||
Utility to manually issue requests against the ES9+ API of an SM-DP+ according to GSMA SGP.22.""")
|
||||
parser.add_argument('--url', required=True, help='Base URL of ES9+ API endpoint')
|
||||
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.""")
|
||||
parser.add_argument('--certificate-path', default='.',
|
||||
help="Path in which to look for certificate and key files.")
|
||||
parser.add_argument('--euicc-certificate', default='CERT_EUICC_ECDSA_NIST.der',
|
||||
help="File name of DER-encoded eUICC certificate file.")
|
||||
parser.add_argument('--euicc-private-key', default='SK_EUICC_ECDSA_NIST.pem',
|
||||
help="File name of PEM-format eUICC secret key file.")
|
||||
parser.add_argument('--eum-certificate', default='CERT_EUM_ECDSA_NIST.der',
|
||||
help="File name of DER-encoded EUM certificate file.")
|
||||
parser.add_argument('--ci-certificate', default='CERT_CI_ECDSA_NIST.der',
|
||||
help="File name of DER-encoded CI certificate file.")
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command',help="The command (API function) to call", required=True)
|
||||
|
||||
# download
|
||||
parser_dl = subparsers.add_parser('download', help="ES9+ download")
|
||||
parser_dl.add_argument('--matchingId', required=True,
|
||||
help='MatchingID that shall be used by profile download')
|
||||
parser_dl.add_argument('--output-path', default='.',
|
||||
help="Path to which the output files will be written.")
|
||||
parser_dl.add_argument('--confirmation-code',
|
||||
help="Confirmation Code for the eSIM download")
|
||||
|
||||
# notification
|
||||
parser_ntf = subparsers.add_parser('notification', help='ES9+ (other) notification')
|
||||
parser_ntf.add_argument('operation', choices=['enable','disable','delete'],
|
||||
help='Profile Management Opreation whoise occurrence shall be notififed')
|
||||
parser_ntf.add_argument('--sequence-nr', type=int, required=True,
|
||||
help='eUICC global notification sequence number')
|
||||
parser_ntf.add_argument('--notification-address', help='notificationAddress, if different from URL')
|
||||
parser_ntf.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
|
||||
|
||||
# notification-install
|
||||
parser_ntfi = subparsers.add_parser('notification-install', help='ES9+ installation notification')
|
||||
parser_ntfi.add_argument('--sequence-nr', type=int, required=True,
|
||||
help='eUICC global notification sequence number')
|
||||
parser_ntfi.add_argument('--transaction-id', required=True,
|
||||
help='transactionId of previous ES9+ download')
|
||||
parser_ntfi.add_argument('--notification-address', help='notificationAddress, if different from URL')
|
||||
parser_ntfi.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
|
||||
parser_ntfi.add_argument('--smdpp-oid', required=True, help='SM-DP+ OID (as in CERT.DPpb.ECDSA)')
|
||||
parser_ntfi.add_argument('--isdp-aid', type=is_hexstr, required=True,
|
||||
help='AID of the ISD-P of the installed profile')
|
||||
parser_ntfi.add_argument('--sima-response', type=is_hexstr, required=True,
|
||||
help='hex digits of BER-encoded SAIP EUICCResponse')
|
||||
|
||||
class Es9pClient:
|
||||
def __init__(self, opts):
|
||||
self.opts = opts
|
||||
self.cert_and_key = CertAndPrivkey()
|
||||
self.cert_and_key.cert_from_der_file(os.path.join(opts.certificate_path, opts.euicc_certificate))
|
||||
self.cert_and_key.privkey_from_pem_file(os.path.join(opts.certificate_path, opts.euicc_private_key))
|
||||
|
||||
with open(os.path.join(opts.certificate_path, opts.eum_certificate), 'rb') as f:
|
||||
self.eum_cert = x509.load_der_x509_certificate(f.read())
|
||||
|
||||
with open(os.path.join(opts.certificate_path, opts.ci_certificate), 'rb') as f:
|
||||
self.ci_cert = x509.load_der_x509_certificate(f.read())
|
||||
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), self.ci_cert.extensions))
|
||||
subject_pkid = subject_exts[0].value
|
||||
self.ci_pkid = subject_pkid.key_identifier
|
||||
|
||||
print("EUICC: %s" % self.cert_and_key.cert.subject)
|
||||
print("EUM: %s" % self.eum_cert.subject)
|
||||
print("CI: %s" % self.ci_cert.subject)
|
||||
|
||||
self.eid = self.cert_and_key.cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
|
||||
print("EID: %s" % self.eid)
|
||||
print("CI PKID: %s" % b2h(self.ci_pkid))
|
||||
print()
|
||||
|
||||
self.peer = es9p.Es9pApiClient(opts.url, server_cert_verify=opts.server_ca_cert)
|
||||
|
||||
|
||||
def do_notification(self):
|
||||
|
||||
ntf_metadata = {
|
||||
'seqNumber': self.opts.sequence_nr,
|
||||
'profileManagementOperation': PMO(self.opts.operation).to_bitstring(),
|
||||
'notificationAddress': self.opts.notification_address or urlparse(self.opts.url).netloc,
|
||||
}
|
||||
if opts.iccid:
|
||||
ntf_metadata['iccid'] = h2b(swap_nibbles(opts.iccid))
|
||||
|
||||
if self.opts.operation == 'download':
|
||||
pird = {
|
||||
'transactionId': self.opts.transaction_id,
|
||||
'notificationMetadata': ntf_metadata,
|
||||
'smdpOid': self.opts.smdpp_oid,
|
||||
'finalResult': ('successResult', {
|
||||
'aid': self.opts.isdp_aid,
|
||||
'simaResponse': self.opts.sima_response,
|
||||
}),
|
||||
}
|
||||
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
|
||||
signature = self.cert_and_key.ecdsa_sign(pird_bin)
|
||||
pn_dict = ('profileInstallationResult', {
|
||||
'profileInstallationResultData': pird,
|
||||
'euiccSignPIR': signature,
|
||||
})
|
||||
else:
|
||||
ntf_bin = rsp.asn1.encode('NotificationMetadata', ntf_metadata)
|
||||
signature = self.cert_and_key.ecdsa_sign(ntf_bin)
|
||||
pn_dict = ('otherSignedNotification', {
|
||||
'tbsOtherNotification': ntf_metadata,
|
||||
'euiccNotificationSignature': signature,
|
||||
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
|
||||
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER)),
|
||||
})
|
||||
|
||||
data = {
|
||||
'pendingNotification': pn_dict,
|
||||
}
|
||||
#print(data)
|
||||
res = self.peer.call_handleNotification(data)
|
||||
|
||||
|
||||
def do_download(self):
|
||||
|
||||
print("Step 1: InitiateAuthentication...")
|
||||
|
||||
euiccInfo1 = {
|
||||
'svn': b'\x02\x04\x00',
|
||||
'euiccCiPKIdListForVerification': [
|
||||
self.ci_pkid,
|
||||
],
|
||||
'euiccCiPKIdListForSigning': [
|
||||
self.ci_pkid,
|
||||
],
|
||||
}
|
||||
|
||||
data = {
|
||||
'euiccChallenge': os.urandom(16),
|
||||
'euiccInfo1': euiccInfo1,
|
||||
'smdpAddress': urlparse(self.opts.url).netloc,
|
||||
}
|
||||
init_auth_res = self.peer.call_initiateAuthentication(data)
|
||||
print(init_auth_res)
|
||||
|
||||
print("Step 2: AuthenticateClient...")
|
||||
|
||||
#res['serverSigned1']
|
||||
#res['serverSignature1']
|
||||
print("TODO: verify serverSignature1 over serverSigned1")
|
||||
#res['transactionId']
|
||||
print("TODO: verify transactionId matches the signed one in serverSigned1")
|
||||
#res['euiccCiPKIdToBeUsed']
|
||||
# TODO: select eUICC certificate based on CI
|
||||
#res['serverCertificate']
|
||||
# TODO: verify server certificate against CI
|
||||
|
||||
euiccInfo2 = {
|
||||
'profileVersion': b'\x02\x03\x01',
|
||||
'svn': euiccInfo1['svn'],
|
||||
'euiccFirmwareVer': b'\x23\x42\x00',
|
||||
'extCardResource': b'\x81\x01\x00\x82\x04\x00\x04\x9ch\x83\x02"#',
|
||||
'uiccCapability': (b'k6\xd3\xc3', 32),
|
||||
'javacardVersion': b'\x11\x02\x00',
|
||||
'globalplatformVersion': b'\x02\x03\x00',
|
||||
'rspCapability': (b'\x9c', 6),
|
||||
'euiccCiPKIdListForVerification': euiccInfo1['euiccCiPKIdListForVerification'],
|
||||
'euiccCiPKIdListForSigning': euiccInfo1['euiccCiPKIdListForSigning'],
|
||||
#'euiccCategory':
|
||||
#'forbiddenProfilePolicyRules':
|
||||
'ppVersion': b'\x01\x00\x00',
|
||||
'sasAcreditationNumber': 'OSMOCOM-TEST-1', #TODO: make configurable
|
||||
#'certificationDataObject':
|
||||
}
|
||||
|
||||
euiccSigned1 = {
|
||||
'transactionId': h2b(init_auth_res['transactionId']),
|
||||
'serverAddress': init_auth_res['serverSigned1']['serverAddress'],
|
||||
'serverChallenge': init_auth_res['serverSigned1']['serverChallenge'],
|
||||
'euiccInfo2': euiccInfo2,
|
||||
'ctxParams1':
|
||||
('ctxParamsForCommonAuthentication', {
|
||||
'matchingId': self.opts.matchingId,
|
||||
'deviceInfo': {
|
||||
'tac': b'\x35\x23\x01\x45', # same as lpac
|
||||
'deviceCapabilities': {},
|
||||
#imei:
|
||||
}
|
||||
}),
|
||||
}
|
||||
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
|
||||
euiccSignature1 = self.cert_and_key.ecdsa_sign(euiccSigned1_bin)
|
||||
auth_clnt_req = {
|
||||
'transactionId': init_auth_res['transactionId'],
|
||||
'authenticateServerResponse':
|
||||
('authenticateResponseOk', {
|
||||
'euiccSigned1': euiccSigned1,
|
||||
'euiccSignature1': euiccSignature1,
|
||||
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
|
||||
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER))
|
||||
})
|
||||
}
|
||||
auth_clnt_res = self.peer.call_authenticateClient(auth_clnt_req)
|
||||
print(auth_clnt_res)
|
||||
#auth_clnt_res['transactionId']
|
||||
print("TODO: verify transactionId matches previous ones")
|
||||
#auth_clnt_res['profileMetadata']
|
||||
# TODO: what's in here?
|
||||
#auth_clnt_res['smdpSigned2']['bppEuiccOtpk']
|
||||
#auth_clnt_res['smdpSignature2']
|
||||
print("TODO: verify serverSignature2 over smdpSigned2")
|
||||
|
||||
smdp_cert = x509.load_der_x509_certificate(auth_clnt_res['smdpCertificate'])
|
||||
|
||||
print("Step 3: GetBoundProfilePackage...")
|
||||
# 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.ECDSA
|
||||
euicc_ot = ec.generate_private_key(smdp_cert.public_key().public_numbers().curve)
|
||||
|
||||
# extract the public key in (hopefully) the right format for the ES8+ interface
|
||||
euicc_otpk = euicc_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
||||
|
||||
euiccSigned2 = {
|
||||
'transactionId': h2b(auth_clnt_res['transactionId']),
|
||||
'euiccOtpk': euicc_otpk,
|
||||
#hashCC
|
||||
}
|
||||
# check for smdpSigned2 ccRequiredFlag, and send it in PrepareDownloadRequest hashCc
|
||||
if auth_clnt_res['smdpSigned2']['ccRequiredFlag']:
|
||||
if not self.opts.confirmation_code:
|
||||
raise ValueError('Confirmation Code required but not provided')
|
||||
cc_hash = hashlib.sha256(self.opts.confirmation_code.encode('ascii')).digest()
|
||||
euiccSigned2['hashCc'] = hashlib.sha256(cc_hash + euiccSigned2['transactionId']).digest()
|
||||
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
|
||||
euiccSignature2 = self.cert_and_key.ecdsa_sign(euiccSigned2_bin + auth_clnt_res['smdpSignature2'])
|
||||
gbp_req = {
|
||||
'transactionId': auth_clnt_res['transactionId'],
|
||||
'prepareDownloadResponse':
|
||||
('downloadResponseOk', {
|
||||
'euiccSigned2': euiccSigned2,
|
||||
'euiccSignature2': euiccSignature2,
|
||||
})
|
||||
}
|
||||
gbp_res = self.peer.call_getBoundProfilePackage(gbp_req)
|
||||
print(gbp_res)
|
||||
#gbp_res['transactionId']
|
||||
# TODO: verify transactionId
|
||||
print("TODO: verify transactionId matches previous ones")
|
||||
bpp_bin = gbp_res['boundProfilePackage']
|
||||
print("TODO: verify boundProfilePackage smdpSignature")
|
||||
|
||||
bpp = BoundProfilePackage()
|
||||
upp_bin = bpp.decode(euicc_ot, self.eid, bpp_bin)
|
||||
|
||||
iccid = swap_nibbles(b2h(bpp.storeMetadataRequest['iccid']))
|
||||
base_name = os.path.join(self.opts.output_path, '%s' % iccid)
|
||||
|
||||
print("SUCCESS: Storing files as %s.*.der" % base_name)
|
||||
|
||||
# write various output files
|
||||
with open(base_name+'.upp.der', 'wb') as f:
|
||||
f.write(bpp.upp)
|
||||
with open(base_name+'.isdp.der', 'wb') as f:
|
||||
f.write(bpp.encoded_configureISDPRequest)
|
||||
with open(base_name+'.smr.der', 'wb') as f:
|
||||
f.write(bpp.encoded_storeMetadataRequest)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
c = Es9pClient(opts)
|
||||
|
||||
if opts.command == 'download':
|
||||
c.do_download()
|
||||
elif opts.command == 'notification':
|
||||
c.do_notification()
|
||||
elif opts.command == 'notification-install':
|
||||
opts.operation = 'install'
|
||||
c.do_notification()
|
||||
169
contrib/fsdump-diff-apply.py
Executable file
169
contrib/fsdump-diff-apply.py
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# The purpose of this script is to
|
||||
# * load two SIM card 'fsdump' files
|
||||
# * determine which file contents in "B" differs from that of "A"
|
||||
# * create a pySim-shell script to update the contents of "A" to match that of "B"
|
||||
|
||||
# (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 json
|
||||
import argparse
|
||||
|
||||
# Files that we should not update
|
||||
FILES_TO_SKIP = [
|
||||
"MF/EF.ICCID",
|
||||
#"MF/DF.GSM/EF.IMSI",
|
||||
#"MF/ADF.USIM/EF.IMSI",
|
||||
]
|
||||
|
||||
# Files that need zero-padding at the end, not ff-padding
|
||||
FILES_PAD_ZERO = [
|
||||
"DF.GSM/EF.SST",
|
||||
"MF/ADF.USIM/EF.UST",
|
||||
"MF/ADF.USIM/EF.EST",
|
||||
"MF/ADF.ISIM/EF.IST",
|
||||
]
|
||||
|
||||
def pad_file(path, instr, byte_len):
|
||||
if path in FILES_PAD_ZERO:
|
||||
pad = '0'
|
||||
else:
|
||||
pad = 'f'
|
||||
return pad_hexstr(instr, byte_len, pad)
|
||||
|
||||
def pad_hexstr(instr, byte_len:int, pad='f'):
|
||||
"""Pad given hex-string to the number of bytes given in byte_len, using ff as padding."""
|
||||
if len(instr) == byte_len*2:
|
||||
return instr
|
||||
elif len(instr) > byte_len*2:
|
||||
raise ValueError('Cannot pad string of length %u to smaller length %u' % (len(instr)/2, byte_len))
|
||||
else:
|
||||
return instr + pad * (byte_len*2 - len(instr))
|
||||
|
||||
def is_all_ff(instr):
|
||||
"""Determine if the entire input hex-string consists of f-digits."""
|
||||
if all([x == 'f' for x in instr.lower()]):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('file_a')
|
||||
parser.add_argument('file_b')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
with open(opts.file_a, 'r') as file_a:
|
||||
json_a = json.loads(file_a.read())
|
||||
with open(opts.file_b, 'r') as file_b:
|
||||
json_b = json.loads(file_b.read())
|
||||
|
||||
for path in json_b.keys():
|
||||
print()
|
||||
print("# %s" % path)
|
||||
|
||||
if not path in json_a:
|
||||
raise ValueError("%s doesn't exist in file_a!" % path)
|
||||
|
||||
if path in FILES_TO_SKIP:
|
||||
print("# skipped explicitly as it is in FILES_TO_SKIP")
|
||||
continue
|
||||
|
||||
if not 'body' in json_b[path]:
|
||||
print("# file doesn't exist in B so we cannot possibly need to modify A")
|
||||
continue
|
||||
|
||||
if not 'body' in json_a[path]:
|
||||
# file was not readable in original (permissions? deactivated?)
|
||||
print("# ERROR: %s not readable in A; please fix that" % path)
|
||||
continue
|
||||
|
||||
body_a = json_a[path]['body']
|
||||
body_b = json_b[path]['body']
|
||||
if body_a == body_b:
|
||||
print("# file body is identical")
|
||||
continue
|
||||
|
||||
file_size_a = json_a[path]['fcp']['file_size']
|
||||
file_size_b = json_b[path]['fcp']['file_size']
|
||||
|
||||
cmds = []
|
||||
structure = json_b[path]['fcp']['file_descriptor']['file_descriptor_byte']['structure']
|
||||
if structure == 'transparent':
|
||||
val_a = body_a
|
||||
val_b = body_b
|
||||
if file_size_a < file_size_b:
|
||||
if not is_all_ff(val_b[2*file_size_a:]):
|
||||
print("# ERROR: file_size_a (%u) < file_size_b (%u); please fix!" % (file_size_a, file_size_b))
|
||||
continue
|
||||
else:
|
||||
print("# WARN: file_size_a (%u) < file_size_b (%u); please fix!" % (file_size_a, file_size_b))
|
||||
# truncate val_b to fit in A
|
||||
val_b = val_b[:file_size_a*2]
|
||||
|
||||
elif file_size_a != file_size_b:
|
||||
print("# NOTE: file_size_a (%u) != file_size_b (%u)" % (file_size_a, file_size_b))
|
||||
|
||||
# Pad to file_size_a
|
||||
val_b = pad_file(path, val_b, file_size_a)
|
||||
if val_b != val_a:
|
||||
cmds.append("update_binary %s" % val_b)
|
||||
else:
|
||||
print("# padded file body is identical")
|
||||
elif structure in ['linear_fixed', 'cyclic']:
|
||||
record_len_a = json_a[path]['fcp']['file_descriptor']['record_len']
|
||||
record_len_b = json_b[path]['fcp']['file_descriptor']['record_len']
|
||||
if record_len_a < record_len_b:
|
||||
print("# ERROR: record_len_a (%u) < record_len_b (%u); please fix!" % (file_size_a, file_size_b))
|
||||
continue
|
||||
elif record_len_a != record_len_b:
|
||||
print("# NOTE: record_len_a (%u) != record_len_b (%u)" % (record_len_a, record_len_b))
|
||||
|
||||
num_rec_a = file_size_a // record_len_a
|
||||
num_rec_b = file_size_b // record_len_b
|
||||
if num_rec_a < num_rec_b:
|
||||
if not all([is_all_ff(x) for x in body_b[num_rec_a:]]):
|
||||
print("# ERROR: num_rec_a (%u) < num_rec_b (%u); please fix!" % (num_rec_a, num_rec_b))
|
||||
continue
|
||||
else:
|
||||
print("# WARN: num_rec_a (%u) < num_rec_b (%u); but they're empty" % (num_rec_a, num_rec_b))
|
||||
elif num_rec_a != num_rec_b:
|
||||
print("# NOTE: num_rec_a (%u) != num_rec_b (%u)" % (num_rec_a, num_rec_b))
|
||||
|
||||
i = 0
|
||||
for r in body_b:
|
||||
if i < len(body_a):
|
||||
break
|
||||
val_a = body_a[i]
|
||||
# Pad to record_len_a
|
||||
val_b = pad_file(path, body_b[i], record_len_a)
|
||||
if val_a != val_b:
|
||||
cmds.append("update_record %u %s" % (i+1, val_b))
|
||||
i = i + 1
|
||||
if len(cmds) == 0:
|
||||
print("# padded file body is identical")
|
||||
elif structure == 'ber_tlv':
|
||||
print("# FIXME: Implement BER-TLV")
|
||||
else:
|
||||
raise ValueError('Unsupported structure %s' % structure)
|
||||
|
||||
if len(cmds):
|
||||
print("select %s" % path)
|
||||
for cmd in cmds:
|
||||
print(cmd)
|
||||
@@ -4,18 +4,23 @@
|
||||
# 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'
|
||||
# * JOB_TYPE: one of 'test', 'distcheck', 'pylint', 'docs'
|
||||
# * SKIP_CLEAN_WORKSPACE: don't run osmo-clean-workspace.sh (for pyosmocom CI)
|
||||
#
|
||||
|
||||
export PYTHONUNBUFFERED=1
|
||||
|
||||
if [ ! -d "./pysim-testdata/" ] ; then
|
||||
if [ ! -d "./tests/" ] ; then
|
||||
echo "###############################################"
|
||||
echo "Please call from pySim-prog top directory"
|
||||
echo "###############################################"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SKIP_CLEAN_WORKSPACE" ]; then
|
||||
osmo-clean-workspace.sh
|
||||
fi
|
||||
|
||||
case "$JOB_TYPE" in
|
||||
"test")
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
@@ -25,16 +30,39 @@ case "$JOB_TYPE" in
|
||||
pip install pyshark
|
||||
|
||||
# Execute automatically discovered unit tests first
|
||||
python -m unittest discover -v -s tests/
|
||||
python -m unittest discover -v -s tests/unittests
|
||||
|
||||
# Run the test with physical cards
|
||||
cd pysim-testdata
|
||||
../tests/pySim-prog_test.sh
|
||||
../tests/pySim-trace_test.sh
|
||||
# Run pySim-prog integration tests (requires physical cards)
|
||||
cd tests/pySim-prog_test/
|
||||
./pySim-prog_test.sh
|
||||
cd ../../
|
||||
|
||||
# Run pySim-trace test
|
||||
tests/pySim-trace_test/pySim-trace_test.sh
|
||||
|
||||
# Run pySim-shell integration tests (requires physical cards)
|
||||
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
|
||||
;;
|
||||
"distcheck")
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
|
||||
pip install .
|
||||
pip install pyshark
|
||||
|
||||
for prog in venv/bin/pySim-*.py; do
|
||||
$prog --help > /dev/null
|
||||
done
|
||||
;;
|
||||
"pylint")
|
||||
# Print pylint version
|
||||
pip3 freeze | grep pylint
|
||||
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
|
||||
pip install .
|
||||
|
||||
# Run pylint to find potential errors
|
||||
# Ignore E1102: not-callable
|
||||
# pySim/filesystem.py: E1102: method is not callable (not-callable)
|
||||
@@ -45,9 +73,15 @@ case "$JOB_TYPE" in
|
||||
--disable E1102 \
|
||||
--disable E0401 \
|
||||
--enable W0301 \
|
||||
pySim *.py
|
||||
pySim tests/unittests/*.py *.py \
|
||||
contrib/*.py
|
||||
;;
|
||||
"docs")
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
rm -rf docs/_build
|
||||
make -C "docs" html latexpdf
|
||||
|
||||
@@ -60,3 +94,5 @@ case "$JOB_TYPE" in
|
||||
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
|
||||
exit 1
|
||||
esac
|
||||
|
||||
osmo-clean-workspace.sh
|
||||
|
||||
258
contrib/saip-tool.py
Executable file
258
contrib/saip-tool.py
Executable file
@@ -0,0 +1,258 @@
|
||||
#!/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 os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import zipfile
|
||||
from pathlib import Path as PlPath
|
||||
from typing import List
|
||||
from osmocom.utils import h2b, b2h, swap_nibbles
|
||||
|
||||
from pySim.esim.saip import *
|
||||
from pySim.esim.saip.validation import CheckBasicStructure
|
||||
from pySim import javacard
|
||||
from pySim.pprint import HexBytesPrettyPrinter
|
||||
|
||||
pp = HexBytesPrettyPrinter(indent=4,width=500)
|
||||
|
||||
parser = argparse.ArgumentParser(description="""
|
||||
Utility program to work with eSIM SAIP (SimAlliance Interoperable Profile) files.""")
|
||||
parser.add_argument('INPUT_UPP', help='Unprotected Profile Package Input file')
|
||||
parser.add_argument("--loglevel", dest="loglevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
default='INFO', help="Set the logging level")
|
||||
parser.add_argument('--debug', action='store_true', help='Enable DEBUG logging')
|
||||
subparsers = parser.add_subparsers(dest='command', help="The command to perform", required=True)
|
||||
|
||||
parser_split = subparsers.add_parser('split', help='Split PE-Sequence into individual PEs')
|
||||
parser_split.add_argument('--output-prefix', default='.', help='Prefix path/filename for output files')
|
||||
|
||||
parser_dump = subparsers.add_parser('dump', help='Dump information on PE-Sequence')
|
||||
parser_dump.add_argument('mode', choices=['all_pe', 'all_pe_by_type', 'all_pe_by_naa'])
|
||||
parser_dump.add_argument('--dump-decoded', action='store_true', help='Dump decoded PEs')
|
||||
|
||||
parser_check = subparsers.add_parser('check', help='Run constraint checkers on PE-Sequence')
|
||||
|
||||
parser_rpe = subparsers.add_parser('remove-pe', help='Remove specified PEs from PE-Sequence')
|
||||
parser_rpe.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_rpe.add_argument('--identification', type=int, action='append', help='Remove PEs matching specified identification')
|
||||
|
||||
parser_rn = subparsers.add_parser('remove-naa', help='Remove speciifed NAAs from PE-Sequence')
|
||||
parser_rn.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='Network Access Application type to remove')
|
||||
# TODO: add an --naa-index or the like, so only one given instance can be removed
|
||||
|
||||
parser_info = subparsers.add_parser('info', help='Display information about the profile')
|
||||
|
||||
parser_eapp = subparsers.add_parser('extract-apps', help='Extract applications as loadblock file')
|
||||
parser_eapp.add_argument('--output-dir', default='.', help='Output directory (where to store files)')
|
||||
parser_eapp.add_argument('--format', default='cap', choices=['ijc', 'cap'], help='Data format of output files')
|
||||
|
||||
parser_info = subparsers.add_parser('tree', help='Display the filesystem tree')
|
||||
|
||||
def do_split(pes: ProfileElementSequence, opts):
|
||||
i = 0
|
||||
for pe in pes.pe_list:
|
||||
basename = PlPath(opts.INPUT_UPP).stem
|
||||
if not pe.identification:
|
||||
fname = '%s-%02u-%s.der' % (basename, i, pe.type)
|
||||
else:
|
||||
fname = '%s-%02u-%05u-%s.der' % (basename, i, pe.identification, pe.type)
|
||||
print("writing single PE to file '%s'" % fname)
|
||||
with open(os.path.join(opts.output_prefix, fname), 'wb') as outf:
|
||||
outf.write(pe.to_der())
|
||||
i += 1
|
||||
|
||||
def do_dump(pes: ProfileElementSequence, opts):
|
||||
def print_all_pe(pes: ProfileElementSequence, dump_decoded:bool = False):
|
||||
# iterate over each pe in the pes (using its __iter__ method)
|
||||
for pe in pes:
|
||||
print("="*70 + " " + pe.type)
|
||||
if dump_decoded:
|
||||
pp.pprint(pe.decoded)
|
||||
|
||||
def print_all_pe_by_type(pes: ProfileElementSequence, dump_decoded:bool = 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)
|
||||
if dump_decoded:
|
||||
pp.pprint(pe.decoded)
|
||||
|
||||
def print_all_pe_by_naa(pes: ProfileElementSequence, dump_decoded:bool = 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)
|
||||
if dump_decoded:
|
||||
for d in pe.decoded:
|
||||
print(" %s" % d)
|
||||
|
||||
if opts.mode == 'all_pe':
|
||||
print_all_pe(pes, opts.dump_decoded)
|
||||
elif opts.mode == 'all_pe_by_type':
|
||||
print_all_pe_by_type(pes, opts.dump_decoded)
|
||||
elif opts.mode == 'all_pe_by_naa':
|
||||
print_all_pe_by_naa(pes, opts.dump_decoded)
|
||||
|
||||
def do_check(pes: ProfileElementSequence, opts):
|
||||
print("Checking PE-Sequence structure...")
|
||||
checker = CheckBasicStructure()
|
||||
checker.check(pes)
|
||||
print("All good!")
|
||||
|
||||
def do_remove_pe(pes: ProfileElementSequence, opts):
|
||||
new_pe_list = []
|
||||
for pe in pes.pe_list:
|
||||
identification = pe.identification
|
||||
if identification:
|
||||
if identification in opts.identification:
|
||||
print("Removing PE %s (id=%u) from Sequence..." % (pe, identification))
|
||||
continue
|
||||
new_pe_list.append(pe)
|
||||
|
||||
pes.pe_list = new_pe_list
|
||||
pes._process_pelist()
|
||||
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
|
||||
with open(opts.output_file, 'wb') as f:
|
||||
f.write(pes.to_der())
|
||||
|
||||
def do_remove_naa(pes: ProfileElementSequence, opts):
|
||||
if not opts.naa_type in NAAs:
|
||||
raise ValueError('unsupported NAA type %s' % opts.naa_type)
|
||||
naa = NAAs[opts.naa_type]
|
||||
print("Removing NAAs of type '%s' from Sequence..." % opts.naa_type)
|
||||
pes.remove_naas_of_type(naa)
|
||||
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
|
||||
with open(opts.output_file, 'wb') as f:
|
||||
f.write(pes.to_der())
|
||||
|
||||
def do_info(pes: ProfileElementSequence, opts):
|
||||
def get_naa_count(pes: ProfileElementSequence) -> dict:
|
||||
"""return a dict with naa-type (usim, isim) as key and the count of NAA instances as value."""
|
||||
ret = {}
|
||||
for naa_type in pes.pes_by_naa:
|
||||
ret[naa_type] = len(pes.pes_by_naa[naa_type])
|
||||
return ret
|
||||
|
||||
pe_hdr_dec = pes.pe_by_type['header'][0].decoded
|
||||
print()
|
||||
print("SAIP Profile Version: %u.%u" % (pe_hdr_dec['major-version'], pe_hdr_dec['minor-version']))
|
||||
print("Profile Type: '%s'" % pe_hdr_dec['profileType'])
|
||||
print("ICCID: %s" % b2h(pe_hdr_dec['iccid']))
|
||||
print("Mandatory Services: %s" % ', '.join(pe_hdr_dec['eUICC-Mandatory-services'].keys()))
|
||||
print()
|
||||
naa_strs = ["%s[%u]" % (k, v) for k, v in get_naa_count(pes).items()]
|
||||
print("NAAs: %s" % ', '.join(naa_strs))
|
||||
for naa_type in pes.pes_by_naa:
|
||||
for naa_inst in pes.pes_by_naa[naa_type]:
|
||||
first_pe = naa_inst[0]
|
||||
adf_name = ''
|
||||
if hasattr(first_pe, 'adf_name'):
|
||||
adf_name = '(' + first_pe.adf_name + ')'
|
||||
print("NAA %s %s" % (first_pe.type, adf_name))
|
||||
if hasattr(first_pe, 'imsi'):
|
||||
print("\tIMSI: %s" % first_pe.imsi)
|
||||
|
||||
# applications
|
||||
print()
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
print("Number of applications: %u" % len(apps))
|
||||
for app_pe in apps:
|
||||
print("App Load Package AID: %s" % b2h(app_pe.decoded['loadBlock']['loadPackageAID']))
|
||||
print("\tMandated: %s" % ('mandated' in app_pe.decoded['app-Header']))
|
||||
print("\tLoad Block Size: %s" % len(app_pe.decoded['loadBlock']['loadBlockObject']))
|
||||
for inst in app_pe.decoded.get('instanceList', []):
|
||||
print("\tInstance AID: %s" % b2h(inst['instanceAID']))
|
||||
|
||||
# security domains
|
||||
print()
|
||||
sds = pes.pe_by_type.get('securityDomain', [])
|
||||
print("Number of security domains: %u" % len(sds))
|
||||
for sd in sds:
|
||||
print("Security domain Instance AID: %s" % b2h(sd.decoded['instance']['instanceAID']))
|
||||
# FIXME: 'applicationSpecificParametersC9' parsing to figure out enabled SCP
|
||||
for key in sd.keys:
|
||||
print("\tKVN=0x%02x, KID=0x%02x, %s" % (key.key_version_number, key.key_identifier, key.key_components))
|
||||
|
||||
# RFM
|
||||
print()
|
||||
rfms = pes.pe_by_type.get('rfm', [])
|
||||
print("Number of RFM instances: %u" % len(rfms))
|
||||
for rfm in rfms:
|
||||
inst_aid = rfm.decoded['instanceAID']
|
||||
print("RFM instanceAID: %s" % b2h(inst_aid))
|
||||
print("\tMSL: 0x%02x" % rfm.decoded['minimumSecurityLevel'][0])
|
||||
adf = rfm.decoded.get('adfRFMAccess', None)
|
||||
if adf:
|
||||
print("\tADF AID: %s" % b2h(adf['adfAID']))
|
||||
tar_list = rfm.decoded.get('tarList', [inst_aid[-3:]])
|
||||
for tar in tar_list:
|
||||
print("\tTAR: %s" % b2h(tar))
|
||||
|
||||
def do_extract_apps(pes:ProfileElementSequence, opts):
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
for app_pe in apps:
|
||||
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
|
||||
|
||||
fname = os.path.join(opts.output_dir, '%s-%s.%s' % (pes.iccid, package_aid, opts.format))
|
||||
load_block_obj = app_pe.decoded['loadBlock']['loadBlockObject']
|
||||
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
|
||||
if opts.format == 'ijc':
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(load_block_obj)
|
||||
else:
|
||||
with io.BytesIO(load_block_obj) as f, zipfile.ZipFile(fname, 'w') as z:
|
||||
javacard.ijc_to_cap(f, z, package_aid)
|
||||
|
||||
def do_tree(pes:ProfileElementSequence, opts):
|
||||
pes.mf.print_tree()
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
if opts.debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.getLevelName(opts.loglevel))
|
||||
|
||||
with open(opts.INPUT_UPP, 'rb') as f:
|
||||
pes = ProfileElementSequence.from_der(f.read())
|
||||
|
||||
print("Read %u PEs from file '%s'" % (len(pes.pe_list), opts.INPUT_UPP))
|
||||
|
||||
if opts.command == 'split':
|
||||
do_split(pes, opts)
|
||||
elif opts.command == 'dump':
|
||||
do_dump(pes, opts)
|
||||
elif opts.command == 'check':
|
||||
do_check(pes, opts)
|
||||
elif opts.command == 'remove-pe':
|
||||
do_remove_pe(pes, opts)
|
||||
elif opts.command == 'remove-naa':
|
||||
do_remove_naa(pes, opts)
|
||||
elif opts.command == 'info':
|
||||
do_info(pes, opts)
|
||||
elif opts.command == 'extract-apps':
|
||||
do_extract_apps(pes, opts)
|
||||
elif opts.command == 'tree':
|
||||
do_tree(pes, opts)
|
||||
@@ -162,6 +162,7 @@ def main(argv):
|
||||
parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
|
||||
parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0)
|
||||
subp = parser.add_subparsers()
|
||||
subp.required = True
|
||||
|
||||
auth_p = subp.add_parser('auth', help='UMTS AKA Authentication')
|
||||
auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10)
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.tlv import bertlv_parse_one, bertlv_encode_tag
|
||||
|
||||
def process_one_level(content: bytes, indent: int):
|
||||
remainder = content
|
||||
@@ -35,5 +36,8 @@ if __name__ == '__main__':
|
||||
content = f.read()
|
||||
elif opts.hex:
|
||||
content = h2b(opts.hex)
|
||||
else:
|
||||
# avoid pylint "(possibly-used-before-assignment)" below
|
||||
sys.exit(2)
|
||||
|
||||
process_one_level(content, 0)
|
||||
|
||||
112
contrib/wsrc_card_client.py
Executable file
112
contrib/wsrc_card_client.py
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Connect smartcard to a remote server, so the remote server can take control and
|
||||
perform commands on it."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from osmocom.utils import b2h
|
||||
import websockets
|
||||
|
||||
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.wsrc.client_blocking import WsClientBlocking
|
||||
from pySim.exceptions import NoCardError
|
||||
from pySim.wsrc import WSRC_DEFAULT_PORT_CARD
|
||||
|
||||
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CardWsClientBlocking(WsClientBlocking):
|
||||
"""Implementation of the card (reader) client of the WSRC (WebSocket Remote Card) protocol"""
|
||||
|
||||
def __init__(self, ws_uri, tp: LinkBase):
|
||||
super().__init__('card', ws_uri)
|
||||
self.tp = tp
|
||||
|
||||
def perform_outbound_hello(self):
|
||||
hello_data = {
|
||||
'atr': b2h(self.tp.get_atr()),
|
||||
# TODO: include various card information in the HELLO message
|
||||
}
|
||||
super().perform_outbound_hello(hello_data)
|
||||
|
||||
def handle_rx_c_apdu(self, rx: dict):
|
||||
"""handle an inbound APDU transceive command"""
|
||||
data, sw = self.tp.send_apdu(rx['command'])
|
||||
tx = {
|
||||
'response': data,
|
||||
'sw': sw,
|
||||
}
|
||||
self.tx_json('r_apdu', tx)
|
||||
|
||||
def handle_rx_disconnect(self, rx: dict):
|
||||
"""server tells us to disconnect"""
|
||||
self.tx_json('disconnect_ack')
|
||||
# FIXME: tear down connection and/or terminate entire program
|
||||
|
||||
def handle_rx_state_notification(self, rx: dict):
|
||||
logger.info("State Notification: %s" % rx['new_state'])
|
||||
|
||||
def handle_rx_print(self, rx: dict):
|
||||
"""print a message (text) given by server to the local console/log"""
|
||||
logger.info("SERVER MSG: %s" % rx['message'])
|
||||
# no response
|
||||
|
||||
def handle_rx_reset_req(self, rx: dict):
|
||||
"""server tells us to reset the card"""
|
||||
self.tp.reset_card()
|
||||
self.tx_json('reset_resp', {'atr': b2h(self.tp.get_atr())})
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
argparse_add_reader_args(parser)
|
||||
parser.add_argument("--uri", default="ws://localhost:%u/" % (WSRC_DEFAULT_PORT_CARD),
|
||||
help="URI of the sever to which to connect")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
# open the card reader / slot
|
||||
logger.info("Initializing Card Reader...")
|
||||
try:
|
||||
tp = init_reader(opts)
|
||||
except Exception as e:
|
||||
logger.fatal("Error opening reader: %s" % e)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Connecting to Card...")
|
||||
try:
|
||||
tp.connect()
|
||||
except NoCardError as e:
|
||||
logger.fatal("Error opening card! Is a card inserted in the reader?")
|
||||
sys.exit(1)
|
||||
|
||||
scc = SimCardCommands(transport=tp)
|
||||
logger.info("Detected Card with ATR: %s" % b2h(tp.get_atr()))
|
||||
|
||||
# TODO: gather various information about the card; print it
|
||||
|
||||
# create + connect the client to the server
|
||||
cl = CardWsClientBlocking(opts.uri, tp)
|
||||
logger.info("Connecting to remote server...")
|
||||
try:
|
||||
cl.connect()
|
||||
logger.info("Successfully connected to Server")
|
||||
except ConnectionRefusedError as e:
|
||||
logger.fatal(e)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# endless loop: wait for inbound command from server + execute it
|
||||
cl.rx_and_execute_cmd()
|
||||
except websockets.exceptions.ConnectionClosedOK as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt as e:
|
||||
print(e.__class__.__name__)
|
||||
sys.exit(2)
|
||||
354
contrib/wsrc_server.py
Executable file
354
contrib/wsrc_server.py
Executable file
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import argparse
|
||||
from typing import Optional, Tuple
|
||||
from websockets.asyncio.server import serve
|
||||
from websockets.exceptions import ConnectionClosedError
|
||||
from osmocom.utils import Hexstr, swap_nibbles
|
||||
|
||||
from pySim.utils import SwMatchstr, ResTuple, sw_match, dec_iccid
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.wsrc import WSRC_DEFAULT_PORT_USER, WSRC_DEFAULT_PORT_CARD
|
||||
|
||||
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
card_clients = set()
|
||||
user_clients = set()
|
||||
|
||||
class WsClientLogAdapter(logging.LoggerAdapter):
|
||||
"""LoggerAdapter adding context (for now: remote IP/Port of client) to log"""
|
||||
def process(self, msg, kwargs):
|
||||
return '%s([%s]:%u) %s' % (self.extra['type'], self.extra['remote_addr'][0],
|
||||
self.extra['remote_addr'][1], msg), kwargs
|
||||
|
||||
class WsClient:
|
||||
def __init__(self, websocket, hello: dict):
|
||||
self.websocket = websocket
|
||||
self.hello = hello
|
||||
self.identity = {}
|
||||
self.logger = WsClientLogAdapter(logger, {'type': self.__class__.__name__,
|
||||
'remote_addr': websocket.remote_address})
|
||||
|
||||
def __str__(self):
|
||||
return '%s([%s]:%u)' % (self.__class__.__name__, self.websocket.remote_address[0],
|
||||
self.websocket.remote_address[1])
|
||||
|
||||
async def rx_json(self):
|
||||
rx = await self.websocket.recv()
|
||||
rx_js = json.loads(rx)
|
||||
self.logger.debug("Rx: %s", rx_js)
|
||||
assert 'msg_type' in rx
|
||||
return rx_js
|
||||
|
||||
async def tx_json(self, msg_type:str, d: dict = {}):
|
||||
"""Transmit a json-serializable dict to the peer"""
|
||||
d['msg_type'] = msg_type
|
||||
d_js = json.dumps(d)
|
||||
self.logger.debug("Tx: %s", d_js)
|
||||
await self.websocket.send(d_js)
|
||||
|
||||
async def tx_hello_ack(self):
|
||||
await self.tx_json('hello_ack')
|
||||
|
||||
async def xceive_json(self, msg_type:str, d:dict = {}, exp_msg_type:Optional[str] = None) -> dict:
|
||||
await self.tx_json(msg_type, d)
|
||||
rx = await self.rx_json()
|
||||
if exp_msg_type:
|
||||
assert rx['msg_type'] == exp_msg_type
|
||||
return rx;
|
||||
|
||||
async def tx_error(self, message: str):
|
||||
"""Transmit an error message to the peer"""
|
||||
event = {
|
||||
"message": message,
|
||||
}
|
||||
self.logger.error("Transmitting error message: '%s'" % message)
|
||||
await self.tx_json('error', event)
|
||||
|
||||
# async def ws_hdlr(self):
|
||||
# """kind of a 'main' function for the websocket client: wait for incoming message,
|
||||
# and handle it."""
|
||||
# try:
|
||||
# async for message in self.websocket:
|
||||
# method = getattr(self, 'handle_rx_%s' % message['msg_type'], None)
|
||||
# if not method:
|
||||
# await self.tx_error("Unknonw msg_type: %s" % message['msg_type'])
|
||||
# else:
|
||||
# method(message)
|
||||
# except ConnectionClosedError:
|
||||
# # we handle this in the outer loop
|
||||
# pass
|
||||
|
||||
class CardClient(WsClient):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.identity['ATR'] = self.hello['atr']
|
||||
self.state = 'init'
|
||||
|
||||
def __str__(self):
|
||||
eid = self.identity.get('EID', None)
|
||||
if eid:
|
||||
return '%s(EID=%s)' % (self.__class__.__name__, eid)
|
||||
iccid = self.identity.get('ICCID', None)
|
||||
if iccid:
|
||||
return '%s(ICCID=%s)' % (self.__class__.__name__, iccid)
|
||||
return super().__str__()
|
||||
|
||||
def listing_entry(self) -> dict:
|
||||
return {'remote_addr': self.websocket.remote_address,
|
||||
'identities': self.identity,
|
||||
'state': self.state}
|
||||
|
||||
"""A websocket client that represents a reader/card. This is what we use to talk to a card"""
|
||||
async def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
|
||||
"""transceive a single APDU with the card"""
|
||||
message = await self.xceive_json('c_apdu', {'command': cmd}, 'r_apdu')
|
||||
return message['response'], message['sw']
|
||||
|
||||
async def xceive_apdu(self, pdu: Hexstr) -> ResTuple:
|
||||
"""transceive an APDU with the card, handling T=0 GET_RESPONSE cases"""
|
||||
prev_pdu = pdu
|
||||
data, sw = await self.xceive_apdu_raw(pdu)
|
||||
|
||||
if sw is not None:
|
||||
while (sw[0:2] in ['9f', '61', '62', '63']):
|
||||
# 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
|
||||
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
|
||||
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
|
||||
prev_pdu = pdu_gr
|
||||
d, sw = await self.xceive_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 = prev_pdu[0:8] + sw[2:4]
|
||||
data, sw = await self.xceive_apdu_raw(pdu_gr)
|
||||
|
||||
return data, sw
|
||||
|
||||
async def xceive_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
|
||||
"""like xceive_apdu, but checking the status word matches the expected pattern"""
|
||||
rv = await self.xceive_apdu(pdu)
|
||||
last_sw = rv[1]
|
||||
|
||||
if not sw_match(rv[1], sw):
|
||||
raise SwMatchError(rv[1], sw.lower())
|
||||
return rv
|
||||
|
||||
async def card_reset(self):
|
||||
"""reset the card"""
|
||||
rx = await self.xceive_json('reset_req', exp_msg_type='reset_resp')
|
||||
self.identity['ATR'] = rx['atr']
|
||||
|
||||
async def notify_state(self):
|
||||
"""notify the card client of a state [change]"""
|
||||
await self.tx_json('state_notification', {'new_state': self.state})
|
||||
|
||||
async def get_iccid(self):
|
||||
"""high-level method to obtain the ICCID of the card"""
|
||||
await self.xceive_apdu_checksw('00a40000023f00') # SELECT MF
|
||||
await self.xceive_apdu_checksw('00a40000022fe2') # SELECT EF.ICCID
|
||||
res, sw = await self.xceive_apdu_checksw('00b0000000') # READ BINARY
|
||||
return dec_iccid(res)
|
||||
|
||||
async def get_eid_sgp22(self):
|
||||
"""high-level method to obtain the EID of a SGP.22 consumer eUICC"""
|
||||
await self.xceive_apdu_checksw('00a4040410a0000005591010ffffffff8900000100')
|
||||
res, sw = await self.xceive_apdu_checksw('80e2910006bf3e035c015a')
|
||||
return res[-32:]
|
||||
|
||||
async def set_state(self, new_state:str):
|
||||
self.logger.info("Card now in '%s' state" % new_state)
|
||||
self.state = new_state
|
||||
await self.notify_state()
|
||||
|
||||
async def identify(self):
|
||||
# identify the card by asking for its EID and/or ICCID
|
||||
try:
|
||||
eid = await self.get_eid_sgp22()
|
||||
self.logger.debug("EID: %s", eid)
|
||||
self.identity['EID'] = eid
|
||||
except SwMatchError:
|
||||
pass
|
||||
try:
|
||||
iccid = await self.get_iccid()
|
||||
self.logger.debug("ICCID: %s", iccid)
|
||||
self.identity['ICCID'] = iccid
|
||||
except SwMatchError:
|
||||
pass
|
||||
await self.set_state('ready')
|
||||
|
||||
async def associate_user(self, user):
|
||||
assert self.state == 'ready'
|
||||
self.user = user
|
||||
await self.set_state('associated')
|
||||
|
||||
async def disassociate_user(self):
|
||||
assert self.user
|
||||
assert self.state == 'associated'
|
||||
await self.set_state('ready')
|
||||
|
||||
@staticmethod
|
||||
def find_client_for_id(id_type: str, id_str: str) -> Optional['CardClient']:
|
||||
for c in card_clients:
|
||||
if c.state != 'ready':
|
||||
continue
|
||||
c_id = c.identity.get(id_type.upper(), None)
|
||||
if c_id and c_id.lower() == id_str.lower():
|
||||
return c
|
||||
return None
|
||||
|
||||
class UserClient(WsClient):
|
||||
"""A websocket client representing a user application like pySim-shell."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.state = 'init'
|
||||
self.card = None
|
||||
|
||||
async def associate_card(self, card: CardClient):
|
||||
assert self.state == 'init'
|
||||
self.card = card
|
||||
self.state = 'associated'
|
||||
|
||||
async def disassociate_card(self):
|
||||
assert self.state == 'associated'
|
||||
self.card = None
|
||||
self.state = 'init'
|
||||
|
||||
async def state_init(self):
|
||||
"""Wait for incoming 'select_card' and process it."""
|
||||
while True:
|
||||
rx = await self.rx_json()
|
||||
if rx['msg_type'] == 'select_card':
|
||||
# look-up if the card can be found
|
||||
card = CardClient.find_client_for_id(rx['id_type'], rx['id_str'])
|
||||
if not card:
|
||||
await self.tx_error('No CardClient found for %s == %s' % (rx['id_type'], rx['id_str']))
|
||||
continue
|
||||
# transition to next statee
|
||||
await card.associate_user(self)
|
||||
await self.associate_card(card)
|
||||
await self.tx_json('select_card_ack', {'identities': card.identity})
|
||||
break
|
||||
elif rx['msg_type'] == 'list_cards':
|
||||
res = [x.listing_entry() for x in card_clients]
|
||||
await self.tx_json('list_cards_ack', {'cards': res})
|
||||
else:
|
||||
self.tx_error('Unknown message type %s' % rx['msg_type'])
|
||||
|
||||
async def state_selected(self):
|
||||
while True:
|
||||
rx = await self.rx_json()
|
||||
if rx['msg_type'] == 'c_apdu':
|
||||
rsp, sw = await self.card.xceive_apdu_raw(rx['command'])
|
||||
await self.tx_json('r_apdu', {'response': rsp, 'sw': sw})
|
||||
elif rx['msg_type'] == 'reset_req':
|
||||
await self.card.card_reset()
|
||||
await self.tx_json('reset_resp')
|
||||
else:
|
||||
self.logger.warning("Unknown/unsupported command '%s' received" % rx['msg_type'])
|
||||
|
||||
|
||||
|
||||
async def card_conn_hdlr(websocket):
|
||||
"""Handler for incoming connection to 'card' port."""
|
||||
# receive first message, which should be a 'hello'
|
||||
rx_raw = await websocket.recv()
|
||||
rx = json.loads(rx_raw)
|
||||
assert rx['msg_type'] == 'hello'
|
||||
client_type = rx['client_type']
|
||||
|
||||
if client_type != 'card':
|
||||
logger.error("Rejecting client (unknown type %s) connection", client_type)
|
||||
raise ValueError("client_type '%s' != expected 'card'" % client_type)
|
||||
|
||||
card = CardClient(websocket, rx)
|
||||
card.logger.info("connection accepted")
|
||||
# first go through identity phase
|
||||
async with asyncio.timeout(30):
|
||||
await card.tx_hello_ack()
|
||||
card.logger.info("hello-handshake completed")
|
||||
card_clients.add(card)
|
||||
# first obtain the identity of the card
|
||||
await card.identify()
|
||||
# then go into the "main loop"
|
||||
try:
|
||||
# wait 'indefinitely'. We cannot call websocket.recv() here, as we will call another
|
||||
# recv() while waiting for the R-APDU after forwarding one from the user client.
|
||||
await websocket.wait_closed()
|
||||
finally:
|
||||
card.logger.info("connection closed")
|
||||
card_clients.remove(card)
|
||||
|
||||
|
||||
async def user_conn_hdlr(websocket):
|
||||
"""Handler for incoming connection to 'user' port."""
|
||||
# receive first message, which should be a 'hello'
|
||||
rx_raw = await websocket.recv()
|
||||
rx = json.loads(rx_raw)
|
||||
assert rx['msg_type'] == 'hello'
|
||||
client_type = rx['client_type']
|
||||
|
||||
if client_type != 'user':
|
||||
logger.error("Rejecting client (unknown type %s) connection", client_type)
|
||||
raise ValueError("client_type '%s' != expected 'card'" % client_type)
|
||||
|
||||
user = UserClient(websocket, rx)
|
||||
user.logger.info("connection accepted")
|
||||
# first go through hello phase
|
||||
async with asyncio.timeout(10):
|
||||
await user.tx_hello_ack()
|
||||
user.logger.info("hello-handshake completed")
|
||||
user_clients.add(user)
|
||||
# first wait for the user to specify the select the card
|
||||
try:
|
||||
await user.state_init()
|
||||
except ConnectionClosedError:
|
||||
user.logger.info("connection closed")
|
||||
user_clients.remove(user)
|
||||
return
|
||||
except asyncio.exceptions.CancelledError: # direct cause of TimeoutError
|
||||
user.logger.error("User failed to transition to 'associated' state within 10s")
|
||||
user_clients.remove(user)
|
||||
return
|
||||
except Exception as e:
|
||||
user.logger.error("Unknown exception %s", e)
|
||||
raise e
|
||||
user_clients.remove(user)
|
||||
return
|
||||
# then perform APDU exchanges without any time limit
|
||||
try:
|
||||
await user.state_selected()
|
||||
except ConnectionClosedError:
|
||||
pass
|
||||
finally:
|
||||
user.logger.info("connection closed")
|
||||
await user.card.disassociate_user()
|
||||
await user.disassociate_card()
|
||||
user_clients.remove(user)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="""Osmocom WebSocket Remote Card server""")
|
||||
parser.add_argument('--user-bind-port', type=int, default=WSRC_DEFAULT_PORT_USER,
|
||||
help="port number to bind for connections from users")
|
||||
parser.add_argument('--user-bind-host', type=str, default='localhost',
|
||||
help="local Host/IP to bind for connections from users")
|
||||
parser.add_argument('--card-bind-port', type=int, default=WSRC_DEFAULT_PORT_CARD,
|
||||
help="port number to bind for connections from cards")
|
||||
parser.add_argument('--card-bind-host', type=str, default='localhost',
|
||||
help="local Host/IP to bind for connections from cards")
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
async def main():
|
||||
# we use different ports for user + card connections to ensure they can
|
||||
# have different packet filter rules apply to them
|
||||
async with serve(card_conn_hdlr, opts.card_bind_host, opts.card_bind_port), serve(user_conn_hdlr, opts.user_bind_host, opts.user_bind_port):
|
||||
await asyncio.get_running_loop().create_future() # run forever
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -4,7 +4,7 @@
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SPHINXBUILD ?= python3 -m sphinx.cmd.build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
|
||||
126
docs/card-key-provider.rst
Normal file
126
docs/card-key-provider.rst
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
Retrieving card-individual keys via CardKeyProvider
|
||||
==================================================
|
||||
|
||||
When working with a batch of cards, or more than one card in general, it
|
||||
is a lot of effort to manually retrieve the card-specific PIN (like
|
||||
ADM1) or key material (like SCP02/SCP03 keys).
|
||||
|
||||
To increase productivity in that regard, pySim has a concept called the
|
||||
`CardKeyProvider`. This is a generic mechanism by which different parts
|
||||
of the pySim[-shell] code can programmatically request card-specific key material
|
||||
from some data source (*provider*).
|
||||
|
||||
For example, when you want to verify the ADM1 PIN using the `verify_adm`
|
||||
command without providing an ADM1 value yourself, pySim-shell will
|
||||
request the ADM1 value for the ICCID of the card via the
|
||||
CardKeyProvider.
|
||||
|
||||
There can in theory be multiple different CardKeyProviders. You can for
|
||||
example develop your own CardKeyProvider that queries some kind of
|
||||
database for the key material, or that uses a key derivation function to
|
||||
derive card-specific key material from a global master key.
|
||||
|
||||
The only actual CardKeyProvider implementation included in pySim is the
|
||||
`CardKeyProviderCsv` which retrieves the key material from a
|
||||
[potentially encrypted] CSV file.
|
||||
|
||||
|
||||
The CardKeyProviderCsv
|
||||
----------------------
|
||||
|
||||
The `CardKeyProviderCsv` allows you to retrieve card-individual key
|
||||
material from a CSV (comma separated value) file that is accessible to pySim.
|
||||
|
||||
The CSV file must have the expected column names, for example `ICCID`
|
||||
and `ADM1` in case you would like to use that CSV to obtain the
|
||||
card-specific ADM1 PIN when using the `verify_adm` command.
|
||||
|
||||
You can specify the CSV file to use via the `--csv` command-line option
|
||||
of pySim-shell. If you do not specify a CSV file, pySim will attempt to
|
||||
open a CSV file from the default location at
|
||||
`~/.osmocom/pysim/card_data.csv`, and use that, if it exists.
|
||||
|
||||
Column-Level CSV encryption
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
pySim supports column-level CSV encryption. This feature will make sure
|
||||
that your key material is not stored in plaintext in the CSV file.
|
||||
|
||||
The encryption mechanism uses AES in CBC mode. You can use any key
|
||||
length permitted by AES (128/192/256 bit).
|
||||
|
||||
Following GSMA FS.28, the encryption works on column level. This means
|
||||
different columns can be decrypted using different key material. This
|
||||
means that leakage of a column encryption key for one column or set of
|
||||
columns (like a specific security domain) does not compromise various
|
||||
other keys that might be stored in other columns.
|
||||
|
||||
You can specify column-level decryption keys using the
|
||||
`--csv-column-key` command line argument. The syntax is
|
||||
`FIELD:AES_KEY_HEX`, for example:
|
||||
|
||||
`pySim-shell.py --csv-column-key SCP03_ENC_ISDR:000102030405060708090a0b0c0d0e0f`
|
||||
|
||||
In order to avoid having to repeat the column key for each and every
|
||||
column of a group of keys within a keyset, there are pre-defined column
|
||||
group aliases, which will make sure that the specified key will be used
|
||||
by all columns of the set:
|
||||
|
||||
* `UICC_SCP02` is a group alias for `UICC_SCP02_KIC1`, `UICC_SCP02_KID1`, `UICC_SCP02_KIK1`
|
||||
* `UICC_SCP03` is a group alias for `UICC_SCP03_KIC1`, `UICC_SCP03_KID1`, `UICC_SCP03_KIK1`
|
||||
* `SCP03_ECASD` is a group alias for `SCP03_ENC_ECASD`, `SCP03_MAC_ECASD`, `SCP03_DEK_ECASD`
|
||||
* `SCP03_ISDA` is a group alias for `SCP03_ENC_ISDA`, `SCP03_MAC_ISDA`, `SCP03_DEK_ISDA`
|
||||
* `SCP03_ISDR` is a group alias for `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR`, `SCP03_DEK_ISDR`
|
||||
|
||||
|
||||
Field naming
|
||||
------------
|
||||
|
||||
* For look-up of UICC/SIM/USIM/ISIM or eSIM profile specific key
|
||||
material, pySim uses the `ICCID` field as lookup key.
|
||||
|
||||
* For look-up of eUICC specific key material (like SCP03 keys for the
|
||||
ISD-R, ECASD), pySim uses the `EID` field as lookup key.
|
||||
|
||||
As soon as the CardKeyProviderCsv finds a line (row) in your CSV where
|
||||
the ICCID or EID match, it looks for the column containing the requested
|
||||
data.
|
||||
|
||||
|
||||
ADM PIN
|
||||
~~~~~~~
|
||||
|
||||
The `verify_adm` command will attempt to look up the `ADM1` column
|
||||
indexed by the ICCID of the SIM/UICC.
|
||||
|
||||
|
||||
SCP02 / SCP03
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
SCP02 and SCP03 each use key triplets consisting if ENC, MAC and DEK
|
||||
keys. For more details, see the applicable GlobalPlatform
|
||||
specifications.
|
||||
|
||||
If you do not want to manually enter the key material for each specific
|
||||
card as arguments to the `establish_scp02` or `establish_scp03`
|
||||
commands, you can make use of the `--key-provider-suffix` option. pySim
|
||||
uses this suffix to compose the column names for the CardKeyProvider as
|
||||
follows.
|
||||
|
||||
* `SCP02_ENC_` + suffix for the SCP02 ciphering key
|
||||
* `SCP02_MAC_` + suffix for the SCP02 MAC key
|
||||
* `SCP02_DEK_` + suffix for the SCP02 DEK key
|
||||
* `SCP03_ENC_` + suffix for the SCP03 ciphering key
|
||||
* `SCP03_MAC_` + suffix for the SCP03 MAC key
|
||||
* `SCP03_DEK_` + suffix for the SCP03 DEK key
|
||||
|
||||
So for example, if you are using a command like `establish_scp03
|
||||
--key-provider-suffix ISDR`, then the column names for the key material
|
||||
look-up are `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR` and `SCP03_DEK_ISDR`,
|
||||
respectively.
|
||||
|
||||
The identifier used for look-up is determined by the definition of the
|
||||
Security Domain. For example, the eUICC ISD-R and ECASD will use the EID
|
||||
of the eUICC. On the other hand, the ISD-P of an eSIM or the ISD of an
|
||||
UICC will use the ICCID.
|
||||
@@ -43,6 +43,7 @@ pySim consists of several parts:
|
||||
legacy
|
||||
library
|
||||
osmo-smdpp
|
||||
wsrc
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
194
docs/legacy.rst
194
docs/legacy.rst
@@ -1,20 +1,20 @@
|
||||
Legacy tools
|
||||
Legacy tools
|
||||
============
|
||||
|
||||
*legacy tools* are the classic ``pySim-prog`` and ``pySim-read`` programs that
|
||||
existed long before ``pySim-shell``.
|
||||
|
||||
These days, you should primarily use ``pySim-shell`` instead of these
|
||||
These days, it is highly recommended to use ``pySim-shell`` instead of these
|
||||
legacy tools.
|
||||
|
||||
pySim-prog
|
||||
----------
|
||||
|
||||
``pySim-prog`` was the first part of the pySim software suite. It started as
|
||||
a tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and
|
||||
was later extended to a variety of other cards. As the number of features supported
|
||||
became no longer bearable to express with command-line arguments, `pySim-shell` was
|
||||
created.
|
||||
``pySim-prog`` was the first part of the pySim software suite. It started as a
|
||||
tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and was
|
||||
later extended to a variety of other cards. As the number of features supported
|
||||
became no longer bearable to express with command-line arguments, `pySim-shell`
|
||||
was created.
|
||||
|
||||
Basic use cases can still use `pySim-prog`.
|
||||
|
||||
@@ -22,36 +22,180 @@ Program customizable SIMs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Two modes are possible:
|
||||
|
||||
- one where you specify every parameter manually :
|
||||
- one where the user specifies every parameter manually:
|
||||
|
||||
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -i <IMSI> -s <ICCID>``
|
||||
This is the most common way to use ``pySim-prog``. The user will specify all relevant parameters directly via the
|
||||
commandline. A typical commandline would look like this:
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --ki <ki_value> --opc <opc_value> --mcc <mcc_value> --mnc <mnc_value>
|
||||
--country <country_code> --imsi <imsi_value> --iccid <iccid_value> --pin-adm <adm_pin>``
|
||||
|
||||
Please note, that this already lengthy commandline still only contains the most common card parameters. For a full
|
||||
list of all possible parameters, use the ``--help`` option of ``pySim-prog``. It is also important to mention
|
||||
that not all parameters are supported by all card types. In particular, very simple programmable SIM cards will only
|
||||
support a very basic set of parameters, such as MCC, MNC, IMSI and KI values.
|
||||
|
||||
- one where the parameters are generated from a minimal set:
|
||||
|
||||
It is also possible to leave the generation of certain parameters to ``pySim-prog``. This is in particular helpful
|
||||
when a large number of cards should be initialized with randomly generated key material.
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --mcc <mcc_value> --mnc <mnc_value> --secret <random_secret> --num <card_number> --pin-adm <adm_pin>``
|
||||
|
||||
The parameter ``--secret`` specifies a random seed that is used to generate the card individual parameters. (IMSI).
|
||||
The secret should contain enough randomness to avoid conflicts. It is also recommended to store the secret safely,
|
||||
in case cards have to be re-generated or the current card batch has to be extended later. For security reasons, the
|
||||
key material, which is also card individual, will not be derived from the random seed. Instead a new random set of
|
||||
Ki and OPc will be generated during each programming cycle. This means fresh keys are generated, even when the
|
||||
``--num`` remains unchanged.
|
||||
|
||||
The parameter ``--num`` specifies a card individual number. This number will be manged into the random seed so that
|
||||
it serves as an identifier for a particular set of randomly generated parameters.
|
||||
|
||||
In the example above the parameters ``--mcc``, and ``--mnc`` are specified as well, since they identify the GSM
|
||||
network where the cards should operate in, it is absolutely required to keep them static. ``pySim-prog`` will use
|
||||
those parameters to generate a valid IMSI that thas the specified MCC/MNC at the beginning and a random tail.
|
||||
|
||||
Specifying the card type:
|
||||
|
||||
``pySim-prog`` usually autodetects the card type. In case auto detection does not work, it is possible to specify
|
||||
the parameter ``--type``. The following card types are supported:
|
||||
|
||||
* Fairwaves-SIM
|
||||
* fakemagicsim
|
||||
* gialersim
|
||||
* grcardsim
|
||||
* magicsim
|
||||
* OpenCells-SIM
|
||||
* supersim
|
||||
* sysmoISIM-SJA2
|
||||
* sysmoISIM-SJA5
|
||||
* sysmosim-gr1
|
||||
* sysmoSIM-GR2
|
||||
* sysmoUSIM-SJS1
|
||||
* Wavemobile-SIM
|
||||
|
||||
Specifying the card reader:
|
||||
|
||||
It is most common to use ``pySim-prog`` together whith a PCSC reader. The PCSC reader number is specified via the
|
||||
``--pcsc-device`` or ``-p`` option. However, other reader types (such as serial readers and modems) are supported. Use
|
||||
the ``--help`` option of ``pySim-prog`` for more information.
|
||||
|
||||
|
||||
- one where they are generated from some minimal set :
|
||||
Card programming using CSV files
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -z <random_string_of_choice> -j <card_num>``
|
||||
To simplify the card programming process, ``pySim-prog`` also allows to read
|
||||
the card parameters from a CSV file. When a CSV file is used as input, the
|
||||
user does not have to craft an individual commandline for each card. Instead
|
||||
all card related parameters are automatically drawn from the CSV file.
|
||||
|
||||
With <random_string_of_choice> and <card_num>, the soft will generate
|
||||
'predictable' IMSI and ICCID, so make sure you choose them so as not to
|
||||
conflict with anyone. (for eg. your name as <random_string_of_choice> and
|
||||
0 1 2 ... for <card num>).
|
||||
A CSV files may hold rows for multiple (hundreds or even thousands) of
|
||||
cards. ``pySim-prog`` is able to identify the rows either by ICCID
|
||||
(recommended as ICCIDs are normally not changed) or IMSI.
|
||||
|
||||
You also need to enter some parameters to select the device :
|
||||
-t TYPE : type of card (supersim, magicsim, fakemagicsim or try 'auto')
|
||||
-d DEV : Serial port device (default /dev/ttyUSB0)
|
||||
-b BAUD : Baudrate (default 9600)
|
||||
The CSV file format is a flexible format with mandatory and optional columns,
|
||||
here the same rules as for the commandline parameters apply. The column names
|
||||
match the command line options. The CSV file may also contain columns that are
|
||||
unknown to pySim-prog, such as inventory numbers, nicknames or parameters that
|
||||
are unrelated to the card programming process. ``pySim-prog`` will silently
|
||||
ignore all unknown columns.
|
||||
|
||||
A CSV file may contain the following columns:
|
||||
|
||||
* name
|
||||
* iccid (typically used as key)
|
||||
* mcc
|
||||
* mnc
|
||||
* imsi (may be used as key, but not recommended)
|
||||
* smsp
|
||||
* ki
|
||||
* opc
|
||||
* acc
|
||||
* pin_adm, adm1 or pin_adm_hex (must be present)
|
||||
* msisdn
|
||||
* epdgid
|
||||
* epdgSelection
|
||||
* pcscf
|
||||
* ims_hdomain
|
||||
* impi
|
||||
* impu
|
||||
* opmode
|
||||
* fplmn
|
||||
|
||||
Due to historical reasons, and to maintain the compatibility between multiple different CSV file formats, the ADM pin
|
||||
may be stored in three different columns. Only one of the three columns must be available.
|
||||
|
||||
* adm1: This column contains the ADM pin in numeric ASCII digit format. This format is the most common.
|
||||
* pin_adm: Same as adm1, only the column name is different
|
||||
* pin_adm_hex: If the ADM pin consists of raw HEX digits, rather then of numerical ASCII digits, then the ADM pin
|
||||
can also be provided as HEX string using this column.
|
||||
|
||||
The following example shows a typical minimal example
|
||||
::
|
||||
|
||||
"imsi","iccid","acc","ki","opc","adm1"
|
||||
"999700000053010","8988211000000530108","0001","51ACE8BD6313C230F0BFE1A458928DF0","E5A00E8DE427E21B206526B5D1B902DF","65942330"
|
||||
"999700000053011","8988211000000530116","0002","746AAFD7F13CFED3AE626B770E53E860","38F7CE8322D2A7417E0BBD1D7B1190EC","13445792"
|
||||
"999700123053012","8988211000000530124","0004","D0DA4B7B150026ADC966DC637B26429C","144FD3AEAC208DFFF4E2140859BAE8EC","53540383"
|
||||
"999700000053013","8988211000000530132","0008","52E59240ABAC6F53FF5778715C5CE70E","D9C988550DC70B95F40342298EB84C5E","26151368"
|
||||
"999700000053014","8988211000000530140","0010","3B4B83CB9C5F3A0B41EBD17E7D96F324","D61DCC160E3B91F284979552CC5B4D9F","64088605"
|
||||
"999700000053015","8988211000000530157","0020","D673DAB320D81039B025263610C2BBB3","4BCE1458936B338067989A06E5327139","94108841"
|
||||
"999700000053016","8988211000000530165","0040","89DE5ACB76E06D14B0F5D5CD3594E2B1","411C4B8273FD7607E1885E59F0831906","55184287"
|
||||
"999700000053017","8988211000000530173","0080","977852F7CEE83233F02E69E211626DE1","2EC35D48DBF2A99C07D4361F19EF338F","70284674"
|
||||
|
||||
The following commandline will instruct ``pySim-prog`` to use the provided CSV file as parameter source and the
|
||||
ICCID (read from the card before programming) as a key to identify the card. To use the IMSI as a key, the parameter
|
||||
``--read-imsi`` can be used instead of ``--read-iccid``. However, this option is only recommended to be used in very
|
||||
specific corner cases.
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --read-iccid``
|
||||
|
||||
It is also possible to pick a row from the CSV file by manually providing an ICCID (option ``--iccid``) or an IMSI
|
||||
(option ``--imsi``) that is then used as a key to find the matching row in the CSV file.
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --iccid <iccid_value>``
|
||||
|
||||
|
||||
Writing CSV files
|
||||
~~~~~~~~~~~~~~~~~
|
||||
``pySim-prog`` is also able to generate CSV files that contain a subset of the parameters it has generated or received
|
||||
from some other source (commandline, CSV-File). The generated file will be header-less and contain the following
|
||||
columns:
|
||||
|
||||
* name
|
||||
* iccid
|
||||
* mcc
|
||||
* mnc
|
||||
* imsi
|
||||
* smsp
|
||||
* ki
|
||||
* opc
|
||||
|
||||
A commandline that makes use of the CSV write feature would look like this:
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_input_csv_file> --read-iccid --source csv --write-csv <path_to_output_csv_file>``
|
||||
|
||||
|
||||
Batch programming
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
In case larger card batches need to be programmed, it is possible to use the ``--batch`` parameter to run ``pySim-prog`` in batch mode.
|
||||
|
||||
The batch mode will prompt the user to insert a card. Once a card is detected in the reader, the programming is carried out. The user may then remove the card again and the process starts over. This allows for a quick and efficient card programming without permanent commandline interaction.
|
||||
|
||||
|
||||
pySim-read
|
||||
----------
|
||||
|
||||
``pySim-read`` allows you to read some data from a SIM card. It will only some files
|
||||
of the card, and will only read files accessible to a normal user (without any special authentication)
|
||||
``pySim-read`` allows to read some of the most important data items from a SIM
|
||||
card. This means it will only read some files of the card, and will only read
|
||||
files accessible to a normal user (without any special authentication)
|
||||
|
||||
These days, you should use the ``export`` command of ``pySim-shell``
|
||||
instead. It performs a much more comprehensive export of all of the
|
||||
[standard] files that can be found on the card. To get a human-readable
|
||||
decode instead of the raw hex export, you can use ``export --json``.
|
||||
These days, it is recommended to use the ``export`` command of ``pySim-shell``
|
||||
instead. It performs a much more comprehensive export of all of the [standard]
|
||||
files that can be found on the card. To get a human-readable decode instead of
|
||||
the raw hex export, you can use ``export --json``.
|
||||
|
||||
Specifically, pySim-read will dump the following:
|
||||
|
||||
|
||||
@@ -74,18 +74,6 @@ at 9600 bps. These readers are sometimes called `Phoenix`.
|
||||
:members:
|
||||
|
||||
|
||||
pySim construct utilities
|
||||
-------------------------
|
||||
|
||||
.. automodule:: pySim.construct
|
||||
:members:
|
||||
|
||||
pySim TLV utilities
|
||||
-------------------
|
||||
|
||||
.. automodule:: pySim.tlv
|
||||
:members:
|
||||
|
||||
pySim utility functions
|
||||
-----------------------
|
||||
|
||||
|
||||
@@ -19,13 +19,21 @@ support for profile personalization yet.
|
||||
|
||||
osmo-smdpp currently
|
||||
|
||||
* always provides the exact same profile to every request. The profile always has the same IMSI and
|
||||
ICCID.
|
||||
* [by default] uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your
|
||||
osmo-smdpp would be running at the host name `testsmdpplus1.example.com`. You can of course replace those
|
||||
certificates with your own, whether SGP.26 derived or part of a *private root CA* setup with mathcing eUICCs.
|
||||
* 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. This is actually very useful for R&D and testing, as it
|
||||
doesn't require you to generate new profiles all the time. This logic of course is unsuitable for
|
||||
production usage.
|
||||
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical (the ones that are stored in
|
||||
the respective UPP `.der` files)
|
||||
* **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
|
||||
* does not perform all of the mandatory certificate verification (it checks the certificate chain, but not
|
||||
the expiration dates nor any CRL)
|
||||
* does not evaluate/consider any *Confirmation Code*
|
||||
* stores the sessions in an unencrypted *python shelve* and is hence leaking one-time key materials
|
||||
used for profile encryption and signing.
|
||||
|
||||
|
||||
@@ -71,17 +79,22 @@ If you use `nginx` as web server, you can use the following configuration snippe
|
||||
You can of course achieve a similar functionality with apache, lighttpd or many other web server
|
||||
software.
|
||||
|
||||
supplementary files
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
osmo-smdpp
|
||||
~~~~~~~~~~
|
||||
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. You can of course replace them with custom certificates
|
||||
if you're operating eSIM with a *private root CA*.
|
||||
|
||||
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.
|
||||
|
||||
commandline options
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
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.
|
||||
|
||||
|
||||
DNS setup for your LPA
|
||||
@@ -91,3 +104,20 @@ The LPA must resolve `testsmdpplus1.example.com` to the IP address of your TLS p
|
||||
|
||||
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.
|
||||
|
||||
46
docs/remote-access.rst
Normal file
46
docs/remote-access.rst
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
Remote access to an UICC/eUICC
|
||||
==============================
|
||||
|
||||
To access a card with pySim-shell, it is not strictly necessary to have physical
|
||||
access to it. There are solutions that allow remote access to UICC/eUICC cards.
|
||||
In this section we will give a brief overview.
|
||||
|
||||
|
||||
osmo-remsim
|
||||
-----------
|
||||
|
||||
osmo-remsim is a suite of software programs enabling physical/geographic
|
||||
separation of a cellular phone (or modem) on the one hand side and the
|
||||
UICC/eUICC card on the other side.
|
||||
|
||||
Using osmo-remsim, you can operate an entire fleet of modems/phones, as well as
|
||||
banks of SIM cards and dynamically establish or remove the connections between
|
||||
modems/phones and cards.
|
||||
|
||||
To access remote cards with pySim-shell via osmo-remseim (RSPRO), the
|
||||
provided libifd_remsim_client would be used to provide a virtual PC/SC reader
|
||||
on the local machine. pySim-shell can then access this reader like any other
|
||||
PC/SC reader.
|
||||
|
||||
More information on osmo-remsim can be found under:
|
||||
* https://osmocom.org/projects/osmo-remsim/wiki
|
||||
* https://ftp.osmocom.org/docs/osmo-remsim/master/osmo-remsim-usermanual.pdf
|
||||
|
||||
|
||||
Android APDU proxy
|
||||
------------------
|
||||
|
||||
Android APDU proxy is an Android app that provides a bridge between a host
|
||||
computer and the UICC/eUICC slot of an Android smartphone.
|
||||
|
||||
The APDU proxy connects to VPCD server that runs on the remote host (in this
|
||||
case the local machine where pySim-shell is running). The VPCD server then
|
||||
provides a virtual PC/SC reader, that pySim-shell can access like any other
|
||||
PC/SC reader.
|
||||
|
||||
On the Android side the UICC/eUICC is accessed via OMAPI (Open Mobile API),
|
||||
which is available in Android since API level Android 8 (API level 29).
|
||||
|
||||
More information Android APDU proxy can be found under:
|
||||
* https://gitea.osmocom.org/sim-card/android-apdu-proxy
|
||||
156
docs/shell.rst
156
docs/shell.rst
@@ -20,6 +20,9 @@ The pySim-shell interactive shell provides commands for
|
||||
|
||||
* if your card supports it, and you have the related privileges: resizing, creating, enabling and disabling of
|
||||
files
|
||||
* performing GlobalPlatform operations, including establishment of Secure Channel Protocol (SCP), Installing
|
||||
applications, installing key material, etc.
|
||||
* listing/enabling/disabling/deleting eSIM profiles on Consumer eUICC
|
||||
|
||||
By means of using the python ``cmd2`` module, various useful features improve usability:
|
||||
|
||||
@@ -66,6 +69,15 @@ Usage Examples
|
||||
suci-tutorial
|
||||
|
||||
|
||||
Advanced Topics
|
||||
---------------
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Advanced pySIM-shell topics
|
||||
|
||||
card-key-provider
|
||||
remote-access
|
||||
|
||||
cmd2 basics
|
||||
-----------
|
||||
|
||||
@@ -133,6 +145,32 @@ optional files in some later 3GPP release) were not found on the card, or were i
|
||||
trying to SELECT them.
|
||||
|
||||
|
||||
fsdump
|
||||
~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim-shell
|
||||
:func: PySimCommands.fsdump_parser
|
||||
|
||||
Please note that `fsdump` works relative to the current working
|
||||
directory, so if you are in `MF`, then the dump will contain all known
|
||||
files on the card. However, if you are in `ADF.ISIM`, only files below
|
||||
that ADF will be part of the dump.
|
||||
|
||||
Furthermore, it is strongly advised to first enter the ADM1 pin
|
||||
(`verify_adm`) to maximize the chance of having permission to read
|
||||
all/most files.
|
||||
|
||||
One use case for this is to systematically analyze the differences between the contents of two
|
||||
cards. To do this, you can create fsdumps of the two cards, and then use some general-purpose JSON
|
||||
diffing tool like `jycm --show` (see https://github.com/eggachecat/jycm).
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF)> fsdump > /tmp/fsdump.json
|
||||
pySIM-shell (00:MF)>
|
||||
|
||||
|
||||
tree
|
||||
~~~~
|
||||
Display a tree of the card filesystem. It is important to note that this displays a tree
|
||||
@@ -400,7 +438,19 @@ verify_chv
|
||||
|
||||
deactivate_file
|
||||
~~~~~~~~~~~~~~~
|
||||
Deactivate the currently selected file. This used to be called INVALIDATE in TS 11.11.
|
||||
Deactivate the currently selected file. A deactivated file can no longer be accessed
|
||||
for any further operation (such as selecting and subsequently reading or writing).
|
||||
|
||||
Any access to a file that is deactivated will trigger the error
|
||||
*SW 6283 'Selected file invalidated/disabled'*
|
||||
|
||||
In order to re-access a deactivated file, you need to activate it again, see the
|
||||
`activate_file` command below. 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!
|
||||
|
||||
This command sends a DEACTIVATE FILE APDU to
|
||||
the card (used to be called INVALIDATE in TS 11.11 for classic SIM).
|
||||
|
||||
|
||||
activate_file
|
||||
@@ -461,7 +511,18 @@ sequence including the electrical power down.
|
||||
:module: pySim.ts_102_221
|
||||
:func: CardProfileUICC.AddlShellCommands.resume_uicc_parser
|
||||
|
||||
terminal_capability
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
This command allows you to perform the TERMINAL CAPABILITY command towards the card.
|
||||
|
||||
TS 102 221 specifies the 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.
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.ts_102_221
|
||||
:func: CardProfileUICC.AddlShellCommands.term_cap_parser
|
||||
|
||||
|
||||
Linear Fixed EF commands
|
||||
@@ -482,6 +543,9 @@ read_record_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: LinFixedEF.ShellCommands.read_rec_dec_parser
|
||||
|
||||
If this command fails, it means that the record is not decodable, and you should use the :ref:`read_record`
|
||||
command and proceed with manual decoding of the contents.
|
||||
|
||||
|
||||
read_records
|
||||
~~~~~~~~~~~~
|
||||
@@ -496,6 +560,9 @@ read_records_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: LinFixedEF.ShellCommands.read_recs_dec_parser
|
||||
|
||||
If this command fails, it means that the record[s] are not decodable, and you should use the :ref:`read_records`
|
||||
command and proceed with manual decoding of the contents.
|
||||
|
||||
|
||||
update_record
|
||||
~~~~~~~~~~~~~
|
||||
@@ -510,6 +577,9 @@ update_record_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: LinFixedEF.ShellCommands.upd_rec_dec_parser
|
||||
|
||||
If this command fails, it means that the record is not encodable; please check your input and/or use the raw
|
||||
:ref:`update_record` command.
|
||||
|
||||
|
||||
edit_record_decoded
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -528,6 +598,12 @@ back to the record on the SIM card.
|
||||
|
||||
This allows for easy interactive modification of records.
|
||||
|
||||
If this command fails before the editor is spawned, it means that the current record contents is not decodable,
|
||||
and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
|
||||
|
||||
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
|
||||
encodable; please check your input and/or us the raw :ref:`update_record` comamdn.
|
||||
|
||||
|
||||
decode_hex
|
||||
~~~~~~~~~~
|
||||
@@ -556,6 +632,8 @@ read_binary_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: TransparentEF.ShellCommands.read_bin_dec_parser
|
||||
|
||||
If this command fails, it means that the file is not decodable, and you should use the :ref:`read_binary`
|
||||
command and proceed with manual decoding of the contents.
|
||||
|
||||
update_binary
|
||||
~~~~~~~~~~~~~
|
||||
@@ -609,6 +687,10 @@ The below example demonstrates this by modifying the ciphering indicator field w
|
||||
"extensions": "ff"
|
||||
}
|
||||
|
||||
If this command fails, it means that the file is not encodable; please check your input and/or use the raw
|
||||
:ref:`update_binary` command.
|
||||
|
||||
|
||||
edit_binary_decoded
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
This command will read the selected binary EF, decode it to its JSON representation, save
|
||||
@@ -622,6 +704,12 @@ to the SIM card.
|
||||
|
||||
This allows for easy interactive modification of file contents.
|
||||
|
||||
If this command fails before the editor is spawned, it means that the current file contents is not decodable,
|
||||
and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
|
||||
|
||||
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
|
||||
encodable; please check your input and/or us the raw :ref:`update_binary` comamdn.
|
||||
|
||||
|
||||
decode_hex
|
||||
~~~~~~~~~~
|
||||
@@ -914,7 +1002,7 @@ aram_delete_all
|
||||
~~~~~~~~~~~~~~~
|
||||
This command will request deletion of all access rules stored within the
|
||||
ARA-M applet. Use it with caution, there is no undo. Any rules later
|
||||
intended must be manually inserted again using `aram_store_ref_ar_do`
|
||||
intended must be manually inserted again using :ref:`aram_store_ref_ar_do`
|
||||
|
||||
|
||||
GlobalPlatform commands
|
||||
@@ -929,6 +1017,70 @@ get_data
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.get_data_parser
|
||||
|
||||
get_status
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.get_status_parser
|
||||
|
||||
set_status
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.set_status_parser
|
||||
|
||||
store_data
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.store_data_parser
|
||||
|
||||
put_key
|
||||
~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.put_key_parser
|
||||
|
||||
delete_key
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.del_key_parser
|
||||
|
||||
install_for_personalization
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.inst_perso_parser
|
||||
|
||||
install_for_install
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.inst_inst_parser
|
||||
|
||||
delete_card_content
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.del_cc_parser
|
||||
|
||||
establish_scp02
|
||||
~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.est_scp02_parser
|
||||
|
||||
establish_scp03
|
||||
~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.est_scp03_parser
|
||||
|
||||
release_scp
|
||||
~~~~~~~~~~~
|
||||
Release any previously established SCP (Secure Channel Protocol)
|
||||
|
||||
|
||||
eUICC ISD-R commands
|
||||
--------------------
|
||||
|
||||
@@ -1,40 +1,56 @@
|
||||
|
||||
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 *in the UE*, using key 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).
|
||||
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 or any flavor of sysmoISIM-SJA5.
|
||||
|
||||
In short, you can enable SUCI with these steps:
|
||||
There is no 3GPP/ETSI standard method for configuring *SUCI calculation on the card*; pySim currently
|
||||
supports the vendor-specific method for the sysmoISIM-SJA5-S17).
|
||||
|
||||
* activate USIM **Service 124**
|
||||
* make sure USIM **Service 125** is disabled
|
||||
* store the public keys in **SUCI_Calc_Info**
|
||||
* set the **Routing Indicator** (required)
|
||||
This document describes both methods.
|
||||
|
||||
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>`__
|
||||
* USIM files and structure: `3GPP 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): `3GPP TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
|
||||
* Test keys for SUCI calculation: `3GPP TS 33.501 <https://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.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>`__.
|
||||
For specific information on sysmocom SIM cards, refer to
|
||||
|
||||
* the `sysmoISIM-SJA5 User Manual <https://sysmocom.de/manuals/sysmoisim-sja5-manual.pdf>`__ for the curent
|
||||
sysmoISIM-SJA5 product
|
||||
* the `sysmoISIM-SJA2 User Manual <https://sysmocom.de/manuals/sysmousim-manual.pdf>`__ for the older
|
||||
sysmoISIM-SJA2 product
|
||||
|
||||
--------------
|
||||
|
||||
|
||||
Enabling 5G SUCI *calculated in the UE*
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In short, you can enable *SUCI calculation in the UE* with these steps:
|
||||
|
||||
* activate USIM **Service 124**
|
||||
* make sure USIM **Service 125** is disabled
|
||||
* store the public keys in **EF.SUCI_Calc_Info**
|
||||
* set the **Routing Indicator** (required)
|
||||
|
||||
If you want to disable the feature, you can just disable USIM Service 124 (and 125) in `EF.UST`.
|
||||
|
||||
|
||||
Admin PIN
|
||||
---------
|
||||
|
||||
@@ -83,8 +99,8 @@ By default, the file is present but empty:
|
||||
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
|
||||
The following JSON config defines the testfile from 3GPP TS 31.121, Section 4.9.4 with
|
||||
test keys from 3GPP TS 33.501, 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``.
|
||||
|
||||
@@ -97,7 +113,7 @@ with ``hnet_pubkey_identifier: 27``.
|
||||
{"priority": 2, "identifier": 0, "key_index": 0}],
|
||||
"hnet_pubkey_list": [
|
||||
{"hnet_pubkey_identifier": 27,
|
||||
"hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"},
|
||||
"hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
|
||||
{"hnet_pubkey_identifier": 30,
|
||||
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
|
||||
}
|
||||
@@ -106,7 +122,7 @@ 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"}]}'
|
||||
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": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"}, {"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.
|
||||
@@ -150,7 +166,7 @@ First, check out the USIM Service Table (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
|
||||
.. list-table:: From 3GPP TS 31.102
|
||||
:widths: 15 40
|
||||
:header-rows: 1
|
||||
|
||||
@@ -184,7 +200,7 @@ be disabled.
|
||||
USIM Error with 5G and sysmoISIM
|
||||
--------------------------------
|
||||
|
||||
sysmoISIMs come 5GS-enabled. By default however, the configuration stored
|
||||
sysmoISIM-SJA2 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).
|
||||
@@ -193,3 +209,62 @@ At least for Qualcomm’s X55 modem, this results in an USIM error and the
|
||||
whole modem shutting 5G down. If you don’t need SUCI concealment but the
|
||||
smartphone refuses to connect to any 5G network, try to disable the UST
|
||||
service 124.
|
||||
|
||||
sysmoISIM-SJA5 are shipped with a more forgiving default, with valid EF.Routing_Indicator
|
||||
contents and disabled Service 124
|
||||
|
||||
|
||||
SUCI calculation by the USIM
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The SUCI calculation can also be performed by the USIM application on the UICC
|
||||
directly. The UE then uses the GET IDENTITY command (see also 3GPP TS 31.102,
|
||||
section 7.5) to retrieve a SUCI value.
|
||||
|
||||
The sysmoISIM-SJA5-S17 supports *SUCI calculation by the USIM*. The configuration
|
||||
is not much different to the above described configuration of *SUCI calculation
|
||||
in the UE*.
|
||||
|
||||
The main difference is how the key provisioning is done. When the SUCI
|
||||
calculation is done by the USIM, then the key material is not accessed by the
|
||||
UE. The specification (see also 3GPP TS 31.102, section 7.5.1.1), also does not
|
||||
specify any file or file format to store the key material. This means the exact
|
||||
way to perform the key provisioning is an implementation detail of the USIM
|
||||
card application.
|
||||
|
||||
In the case of sysmoISIM-SJA5-S17, the key material for *SUCI calculation by the USIM* is stored in
|
||||
`ADF.USIM/DF.SAIP/EF.SUCI_Calc_Info` (**not** in `ADF.USIM/DF.5GS/EF.SUCI_Calc_Info`!).
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF)> select MF
|
||||
pySIM-shell (00:MF)> select ADF.USIM
|
||||
pySIM-shell (00:MF/ADF.USIM)> select DF.SAIP
|
||||
pySIM-shell (00:MF/ADF.USIM/DF.SAIP)> select EF.SUCI_Calc_Info
|
||||
|
||||
The file format is exactly the same as specified in 3GPP TS 31.102, section
|
||||
4.4.11.8. This means the above described key provisioning procedure can be
|
||||
applied without any changes, except that the file location is different.
|
||||
|
||||
To signal to the UE that the USIM is setup up for SUCI calculation, service
|
||||
125 must be enabled in addition to service 124 (see also 3GPP TS 31.102,
|
||||
section 5.3.48)
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 124
|
||||
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 125
|
||||
|
||||
To verify that the SUCI calculation works as expected, it is possible to issue
|
||||
a GET IDENTITY command using pySim-shell:
|
||||
|
||||
::
|
||||
|
||||
select ADF.USIM
|
||||
get_identity
|
||||
|
||||
The USIM should then return a SUCI TLV Data object that looks like this:
|
||||
|
||||
::
|
||||
|
||||
SUCI TLV Data Object: 0199f90717ff021b027a2c58ce1c6b89df088a9eb4d242596dd75746bb5f3503d2cf58a7461e4fd106e205c86f76544e9d732226a4e1
|
||||
|
||||
57
docs/wsrc.rst
Normal file
57
docs/wsrc.rst
Normal file
@@ -0,0 +1,57 @@
|
||||
WebSocket Remote Card (WSRC)
|
||||
============================
|
||||
|
||||
WSRC (*Web Socket Remote Card*) is a mechanism by which card readers can be made remotely available
|
||||
via a computer network. The transport mechanism is (as the name implies) a WebSocket. This transport
|
||||
method was chosen to be as firewall/NAT friendly as possible.
|
||||
|
||||
WSRC Network Architecture
|
||||
-------------------------
|
||||
|
||||
In a WSRC network, there are three major elements:
|
||||
|
||||
* The **WSRC Card Client** which exposes a locally attached smart card (usually via a Smart Card Reader)
|
||||
to a remote *WSRC Server*
|
||||
* The **WSRC Server** manges incoming connections from both *WSRC Card Clients* as well as *WSRC User Clients*
|
||||
* The **WSRC User Client** is a user application, like for example pySim-shell, which is accessing a remote
|
||||
card by connecting to the *WSRC Server* which relays the information to the selected *WSRC Card Client*
|
||||
|
||||
WSRC Protocol
|
||||
-------------
|
||||
|
||||
The WSRC protocl consits of JSON objects being sent over a websocket. The websocket communication itself
|
||||
is based on HTTP and should usually operate via TLS for security reasons.
|
||||
|
||||
The detailed protocol is currently still WIP. The plan is to document it here.
|
||||
|
||||
|
||||
pySim implementations
|
||||
---------------------
|
||||
|
||||
|
||||
wsrc_server
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:filename: ../contrib/wsrc_server.py
|
||||
:func: parser
|
||||
:prog: contrib/wsrc_server.py
|
||||
|
||||
|
||||
wsrc_card_client
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:filename: ../contrib/wsrc_card_client.py
|
||||
:func: parser
|
||||
:prog: contrib/wsrc_card_client.py
|
||||
|
||||
pySim-shell
|
||||
~~~~~~~~~~~
|
||||
|
||||
pySim-shell can talk to a remote card via WSRC if you use the *wsrc transport*, for example like this:
|
||||
|
||||
::
|
||||
|
||||
./pySim-shell.py --wsrc-eid 89882119900000000000000000007280 --wsrc-serer-url ws://localhost:4220/
|
||||
|
||||
|
||||
You can specify `--wsrc-eid` or `--wsrc-iccid` to identify the remote eUICC or UICC you would like to select.
|
||||
219
osmo-smdpp.py
219
osmo-smdpp.py
@@ -32,10 +32,13 @@ from klein import Klein
|
||||
from twisted.web.iweb import IRequest
|
||||
import asn1tools
|
||||
|
||||
from pySim.utils import h2b, b2h, swap_nibbles
|
||||
from osmocom.utils import h2b, b2h, swap_nibbles
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim import saip, PMO
|
||||
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'
|
||||
@@ -70,18 +73,12 @@ def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_da
|
||||
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 load_pem_private_key, Encoding, PublicFormat, PrivateFormat, NoEncryption
|
||||
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_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')
|
||||
|
||||
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
|
||||
@@ -90,52 +87,6 @@ def ecdsa_tr03111_to_dss(sig: bytes) -> bytes:
|
||||
return encode_dss_signature(r, s)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
def __init__(self, subject_code: str, reason_code: str, message: Optional[str] = None,
|
||||
subject_id: Optional[str] = None):
|
||||
@@ -147,27 +98,6 @@ class ApiError(Exception):
|
||||
build_resp_header(js, 'Failed', self.status_code)
|
||||
return json.dumps(js)
|
||||
|
||||
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 SmDppHttpServer:
|
||||
app = Klein()
|
||||
|
||||
@@ -206,6 +136,7 @@ class SmDppHttpServer:
|
||||
|
||||
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)
|
||||
@@ -252,13 +183,15 @@ class SmDppHttpServer:
|
||||
# TODO: reject any non-JSON Content-type
|
||||
|
||||
content = json.loads(request.content.read())
|
||||
print("Rx JSON: %s" % content)
|
||||
print("Rx JSON: %s" % json.dumps(content))
|
||||
set_headers(request)
|
||||
|
||||
output = func(self, request, content) or {}
|
||||
output = func(self, request, content)
|
||||
if output == None:
|
||||
return ''
|
||||
|
||||
build_resp_header(output)
|
||||
print("Tx JSON: %s" % output)
|
||||
print("Tx JSON: %s" % json.dumps(output))
|
||||
return json.dumps(output)
|
||||
return _api_wrapper
|
||||
|
||||
@@ -286,7 +219,17 @@ class SmDppHttpServer:
|
||||
if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
|
||||
pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
|
||||
# verify it supports one of the keys indicated by euiccCiPKIdListForSigning
|
||||
if not any(self.ci_get_cert_for_pkid(x) for x in pkid_list):
|
||||
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:
|
||||
@@ -298,7 +241,7 @@ class SmDppHttpServer:
|
||||
|
||||
# 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
|
||||
transactionId = uuid.uuid4().hex.upper()
|
||||
assert not transactionId in self.rss
|
||||
|
||||
# Generate a serverChallenge for eUICC authentication attached to the ongoing RSP session.
|
||||
@@ -329,7 +272,8 @@ class SmDppHttpServer:
|
||||
#output['otherCertsInChain'] = b64encode2str()
|
||||
|
||||
# create SessionState and store it in rss
|
||||
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge)
|
||||
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge,
|
||||
cert_get_subject_key_id(ci_cert))
|
||||
|
||||
return output
|
||||
|
||||
@@ -350,8 +294,7 @@ class SmDppHttpServer:
|
||||
|
||||
r_ok = authenticateServerResp[1]
|
||||
euiccSigned1 = r_ok['euiccSigned1']
|
||||
# TODO: use original data, don't re-encode?
|
||||
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
|
||||
euiccSigned1_bin = rsp.extract_euiccSigned1(authenticateServerResp_bin)
|
||||
euiccSignature1_bin = r_ok['euiccSignature1']
|
||||
euiccCertificate_dec = r_ok['euiccCertificate']
|
||||
# TODO: use original data, don't re-encode?
|
||||
@@ -364,29 +307,35 @@ class SmDppHttpServer:
|
||||
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
|
||||
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
|
||||
|
||||
# TODO: Verify the validity of the eUICC certificate chain
|
||||
# raise ApiError('8.1.3', '6.1', 'Verification failed')
|
||||
# raise ApiError('8.1.3', '6.3', 'Expired')
|
||||
|
||||
# TODO: Verify that the Root Certificate of the eUICC certificate chain corresponds to the
|
||||
# euiccCiPKIdToBeUsed or euiccCiPKIdToBeUsedV3
|
||||
# raise ApiError('8.11.1', '3.9', 'Unknown')
|
||||
|
||||
# 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')
|
||||
|
||||
# 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 # do we need this in the state?
|
||||
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 (certificate chain)')
|
||||
# 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 (euiccSignature1 over euiccSigned1)')
|
||||
|
||||
# TODO: verify eUICC cert is signed by EUM cert
|
||||
# TODO: verify EUM cert is signed by CI cert
|
||||
# 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
|
||||
@@ -396,10 +345,40 @@ class SmDppHttpServer:
|
||||
# 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')
|
||||
raise ApiError('8.1', '6.1', 'Verification failed (serverChallenge)')
|
||||
|
||||
# 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'])
|
||||
else:
|
||||
# there's currently no other option in the ctxParams1 choice, so this cannot happen
|
||||
raise ApiError('1.3.1', '2.2', 'ctxParams1 missing mandatory ctxParamsForCommonAuthentication')
|
||||
|
||||
# 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('89000123456789012358')), spn="OsmocomSPN", profile_name="OsmocomProfile")
|
||||
ss.profileMetadata = ProfileMetadata(iccid_bin=h2b(swap_nibbles(iccid_str)), spn="OsmocomSPN", profile_name=matchingId)
|
||||
# enable notifications for all operations
|
||||
for event in ['enable', 'disable', 'delete']:
|
||||
ss.profileMetadata.add_notification(event, self.server_hostname)
|
||||
profileMetadata_bin = ss.profileMetadata.gen_store_metadata_request()
|
||||
|
||||
# Put together smdpSigned2 + _bin
|
||||
@@ -447,8 +426,7 @@ class SmDppHttpServer:
|
||||
|
||||
# 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)
|
||||
euiccSigned2_bin = rsp.extract_euiccSigned2(prepDownloadResp_bin)
|
||||
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')
|
||||
|
||||
@@ -471,7 +449,7 @@ class SmDppHttpServer:
|
||||
|
||||
ss.host_id = b'mahlzeit'
|
||||
|
||||
# Generate Session Keys using the CRT, opPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
|
||||
# Generate Session Keys using the CRT, otPK.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))
|
||||
@@ -479,7 +457,7 @@ class SmDppHttpServer:
|
||||
# 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(DATA_DIR, 'upp', 'TS48 V2 eSIM_GTP_SAIP2.1_NoBERTLV.rename2der'), 'rb') as f:
|
||||
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
|
||||
@@ -503,6 +481,9 @@ class SmDppHttpServer:
|
||||
@rsp_api_wrapper
|
||||
def handleNotification(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ HandleNotification in SGP.22 Section 5.6.4"""
|
||||
# SGP.22 Section 6.3: "A normal notification function execution status (MEP Notification)
|
||||
# SHALL be indicated by the HTTP status code '204' (No Content) with an empty HTTP response body"
|
||||
request.setResponseCode(204)
|
||||
pendingNotification_bin = b64decode(content['pendingNotification'])
|
||||
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
|
||||
print("Rx %s: %s" % pendingNotification)
|
||||
@@ -519,13 +500,33 @@ class SmDppHttpServer:
|
||||
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")
|
||||
raise Exception('ECDSA signature verification failed on notification')
|
||||
print("Profile Installation Final Result: ", pird['finalResult'])
|
||||
# remove session state
|
||||
del self.rss[transactionId]
|
||||
elif pendingNotification[0] == 'otherSignedNotification':
|
||||
# TODO
|
||||
pass
|
||||
otherSignedNotif = pendingNotification[1]
|
||||
# TODO: use some kind of partially-parsed original data, don't re-encode?
|
||||
euiccCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['euiccCertificate'])
|
||||
eumCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['eumCertificate'])
|
||||
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
|
||||
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
|
||||
ci_cert_id = cert_get_auth_key_id(eum_cert)
|
||||
# Verify the validity of the eUICC certificate chain
|
||||
cs = CertificateSet(self.ci_get_cert_for_pkid(ci_cert_id))
|
||||
cs.add_intermediate_cert(eum_cert)
|
||||
# TODO v3: otherCertsInChain
|
||||
cs.verify_cert_chain(euicc_cert)
|
||||
tbs_bin = rsp.asn1.encode('NotificationMetadata', otherSignedNotif['tbsOtherNotification'])
|
||||
if not self._ecdsa_verify(euicc_cert, otherSignedNotif['euiccNotificationSignature'], tbs_bin):
|
||||
raise Exception('ECDSA signature verification failed on notification')
|
||||
other_notif = otherSignedNotif['tbsOtherNotification']
|
||||
pmo = PMO.from_bitstring(other_notif['profileManagementOperation'])
|
||||
eid = euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
|
||||
iccid = other_notif.get('iccid', None)
|
||||
if iccid:
|
||||
iccid = swap_nibbles(b2h(iccid))
|
||||
print("handleNotification: EID %s: %s of %s" % (eid, pmo, iccid))
|
||||
else:
|
||||
raise ValueError(pendingNotification)
|
||||
|
||||
@@ -586,7 +587,7 @@ def main(argv):
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=True)
|
||||
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=False)
|
||||
#hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
|
||||
hs.app.run("localhost", 8000)
|
||||
|
||||
|
||||
156
pySim-prog.py
156
pySim-prog.py
@@ -25,7 +25,7 @@
|
||||
#
|
||||
|
||||
import hashlib
|
||||
from optparse import OptionParser
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
@@ -33,12 +33,13 @@ import sys
|
||||
import traceback
|
||||
import json
|
||||
import csv
|
||||
from osmocom.utils import h2b, swap_nibbles, rpad
|
||||
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.transport import init_reader
|
||||
from pySim.transport import init_reader, argparse_add_reader_args
|
||||
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_AD
|
||||
from pySim.utils import derive_milenage_opc, calculate_luhn, dec_iccid
|
||||
from pySim.profile.ts_51_011 import EF_AD
|
||||
from pySim.legacy.ts_51_011 import EF
|
||||
from pySim.card_handler import *
|
||||
from pySim.utils import *
|
||||
@@ -46,169 +47,146 @@ from pySim.utils import *
|
||||
|
||||
def parse_options():
|
||||
|
||||
parser = OptionParser(usage="usage: %prog [options]")
|
||||
parser = argparse.ArgumentParser()
|
||||
argparse_add_reader_args(parser)
|
||||
|
||||
parser.add_option("-d", "--device", dest="device", metavar="DEV",
|
||||
help="Serial Device for SIM access [default: %default]",
|
||||
default="/dev/ttyUSB0",
|
||||
)
|
||||
parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
|
||||
help="Baudrate used for SIM access [default: %default]",
|
||||
default=9600,
|
||||
)
|
||||
parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
|
||||
help="Which PC/SC reader number for SIM access",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
|
||||
help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
|
||||
help="Baudrate used for modem's port [default: %default]",
|
||||
default=115200,
|
||||
)
|
||||
parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
|
||||
help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option("-t", "--type", dest="type",
|
||||
help="Card type (user -t list to view) [default: %default]",
|
||||
parser.add_argument("-t", "--type", dest="type",
|
||||
help="Card type (user -t list to view) [default: %(default)s]",
|
||||
default="auto",
|
||||
)
|
||||
parser.add_option("-T", "--probe", dest="probe",
|
||||
parser.add_argument("-T", "--probe", dest="probe",
|
||||
help="Determine card type",
|
||||
default=False, action="store_true"
|
||||
)
|
||||
parser.add_option("-a", "--pin-adm", dest="pin_adm",
|
||||
parser.add_argument("-a", "--pin-adm", dest="pin_adm",
|
||||
help="ADM PIN used for provisioning (overwrites default)",
|
||||
)
|
||||
parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
|
||||
parser.add_argument("-A", "--pin-adm-hex", dest="pin_adm_hex",
|
||||
help="ADM PIN used for provisioning, as hex string (16 characters long",
|
||||
)
|
||||
parser.add_option("-e", "--erase", dest="erase", action='store_true',
|
||||
help="Erase beforehand [default: %default]",
|
||||
parser.add_argument("-e", "--erase", dest="erase", action='store_true',
|
||||
help="Erase beforehand [default: %(default)s]",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_option("-S", "--source", dest="source",
|
||||
help="Data Source[default: %default]",
|
||||
parser.add_argument("-S", "--source", dest="source",
|
||||
help="Data Source[default: %(default)s]",
|
||||
default="cmdline",
|
||||
)
|
||||
|
||||
# if mode is "cmdline"
|
||||
parser.add_option("-n", "--name", dest="name",
|
||||
help="Operator name [default: %default]",
|
||||
parser.add_argument("-n", "--name", dest="name",
|
||||
help="Operator name [default: %(default)s]",
|
||||
default="Magic",
|
||||
)
|
||||
parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
|
||||
help="Country code [default: %default]",
|
||||
parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
|
||||
help="Country code [default: %(default)s]",
|
||||
default=1,
|
||||
)
|
||||
parser.add_option("-x", "--mcc", dest="mcc", type="string",
|
||||
help="Mobile Country Code [default: %default]",
|
||||
parser.add_argument("-x", "--mcc", dest="mcc",
|
||||
help="Mobile Country Code [default: %(default)s]",
|
||||
default="901",
|
||||
)
|
||||
parser.add_option("-y", "--mnc", dest="mnc", type="string",
|
||||
help="Mobile Network Code [default: %default]",
|
||||
parser.add_argument("-y", "--mnc", dest="mnc",
|
||||
help="Mobile Network Code [default: %(default)s]",
|
||||
default="55",
|
||||
)
|
||||
parser.add_option("--mnclen", dest="mnclen", type="choice",
|
||||
help="Length of Mobile Network Code [default: %default]",
|
||||
parser.add_argument("--mnclen", dest="mnclen",
|
||||
help="Length of Mobile Network Code [default: %(default)s]",
|
||||
default="auto",
|
||||
choices=["2", "3", "auto"],
|
||||
)
|
||||
parser.add_option("-m", "--smsc", dest="smsc",
|
||||
parser.add_argument("-m", "--smsc", dest="smsc",
|
||||
help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
|
||||
)
|
||||
parser.add_option("-M", "--smsp", dest="smsp",
|
||||
parser.add_argument("-M", "--smsp", dest="smsp",
|
||||
help="Raw SMSP content in hex [default: auto from SMSC]",
|
||||
)
|
||||
|
||||
parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
|
||||
parser.add_argument("-s", "--iccid", dest="iccid", metavar="ID",
|
||||
help="Integrated Circuit Card ID",
|
||||
)
|
||||
parser.add_option("-i", "--imsi", dest="imsi",
|
||||
parser.add_argument("-i", "--imsi", dest="imsi",
|
||||
help="International Mobile Subscriber Identity",
|
||||
)
|
||||
parser.add_option("--msisdn", dest="msisdn",
|
||||
parser.add_argument("--msisdn", dest="msisdn",
|
||||
help="Mobile Subscriber Integrated Services Digital Number",
|
||||
)
|
||||
parser.add_option("-k", "--ki", dest="ki",
|
||||
parser.add_argument("-k", "--ki", dest="ki",
|
||||
help="Ki (default is to randomize)",
|
||||
)
|
||||
parser.add_option("-o", "--opc", dest="opc",
|
||||
parser.add_argument("-o", "--opc", dest="opc",
|
||||
help="OPC (default is to randomize)",
|
||||
)
|
||||
parser.add_option("--op", dest="op",
|
||||
parser.add_argument("--op", dest="op",
|
||||
help="Set OP to derive OPC from OP and KI",
|
||||
)
|
||||
parser.add_option("--acc", dest="acc",
|
||||
parser.add_argument("--acc", dest="acc",
|
||||
help="Set ACC bits (Access Control Code). not all card types are supported",
|
||||
)
|
||||
parser.add_option("--opmode", dest="opmode", type="choice",
|
||||
parser.add_argument("--opmode", dest="opmode",
|
||||
help="Set UE Operation Mode in EF.AD (Administrative Data)",
|
||||
default=None,
|
||||
choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
|
||||
)
|
||||
parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
|
||||
parser.add_argument("-f", "--fplmn", dest="fplmn", action="append",
|
||||
help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
|
||||
)
|
||||
parser.add_option("--epdgid", dest="epdgid",
|
||||
parser.add_argument("--epdgid", dest="epdgid",
|
||||
help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
|
||||
)
|
||||
parser.add_option("--epdgSelection", dest="epdgSelection",
|
||||
parser.add_argument("--epdgSelection", dest="epdgSelection",
|
||||
help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
|
||||
)
|
||||
parser.add_option("--pcscf", dest="pcscf",
|
||||
parser.add_argument("--pcscf", dest="pcscf",
|
||||
help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
|
||||
)
|
||||
parser.add_option("--ims-hdomain", dest="ims_hdomain",
|
||||
parser.add_argument("--ims-hdomain", dest="ims_hdomain",
|
||||
help="Set IMS Home Network Domain Name in FQDN format",
|
||||
)
|
||||
parser.add_option("--impi", dest="impi",
|
||||
parser.add_argument("--impi", dest="impi",
|
||||
help="Set IMS private user identity",
|
||||
)
|
||||
parser.add_option("--impu", dest="impu",
|
||||
parser.add_argument("--impu", dest="impu",
|
||||
help="Set IMS public user identity",
|
||||
)
|
||||
parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
|
||||
parser.add_argument("--read-imsi", dest="read_imsi", action="store_true",
|
||||
help="Read the IMSI from the CARD", default=False
|
||||
)
|
||||
parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
|
||||
parser.add_argument("--read-iccid", dest="read_iccid", action="store_true",
|
||||
help="Read the ICCID from the CARD", default=False
|
||||
)
|
||||
parser.add_option("-z", "--secret", dest="secret", metavar="STR",
|
||||
parser.add_argument("-z", "--secret", dest="secret", metavar="STR",
|
||||
help="Secret used for ICCID/IMSI autogen",
|
||||
)
|
||||
parser.add_option("-j", "--num", dest="num", type=int,
|
||||
parser.add_argument("-j", "--num", dest="num", type=int,
|
||||
help="Card # used for ICCID/IMSI autogen",
|
||||
)
|
||||
parser.add_option("--batch", dest="batch_mode",
|
||||
help="Enable batch mode [default: %default]",
|
||||
parser.add_argument("--batch", dest="batch_mode",
|
||||
help="Enable batch mode [default: %(default)s]",
|
||||
default=False, action='store_true',
|
||||
)
|
||||
parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
|
||||
parser.add_argument("--batch-state", dest="batch_state", metavar="FILE",
|
||||
help="Optional batch state file",
|
||||
)
|
||||
|
||||
# if mode is "csv"
|
||||
parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
|
||||
parser.add_argument("--read-csv", dest="read_csv", metavar="FILE",
|
||||
help="Read parameters from CSV file rather than command line")
|
||||
|
||||
parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
|
||||
parser.add_argument("--write-csv", dest="write_csv", metavar="FILE",
|
||||
help="Append generated parameters in CSV file",
|
||||
)
|
||||
parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
|
||||
parser.add_argument("--write-hlr", dest="write_hlr", metavar="FILE",
|
||||
help="Append generated parameters to OpenBSC HLR sqlite3",
|
||||
)
|
||||
parser.add_option("--dry-run", dest="dry_run",
|
||||
parser.add_argument("--dry-run", dest="dry_run",
|
||||
help="Perform a 'dry run', don't actually program the card",
|
||||
default=False, action="store_true")
|
||||
parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||
help="Use automatic card handling machine")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
options = parser.parse_args()
|
||||
|
||||
if options.type == 'list':
|
||||
for kls in _cards_classes:
|
||||
@@ -219,15 +197,13 @@ def parse_options():
|
||||
return options
|
||||
|
||||
if options.source == 'csv':
|
||||
if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
|
||||
parser.error(
|
||||
"CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
|
||||
if (options.imsi is None) and (options.iccid is None) and (options.read_imsi is False) and (options.read_iccid is False):
|
||||
parser.error("CSV mode requires one additional parameter: --read-iccid, --read-imsi, --iccid or --imsi")
|
||||
if options.read_csv is None:
|
||||
parser.error("CSV mode requires a CSV input file")
|
||||
elif options.source == 'cmdline':
|
||||
if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
|
||||
parser.error(
|
||||
"If either IMSI or ICCID isn't specified, num is required")
|
||||
parser.error("If either IMSI or ICCID isn't specified, num is required")
|
||||
else:
|
||||
parser.error("Only `cmdline' and `csv' sources supported")
|
||||
|
||||
@@ -242,9 +218,6 @@ def parse_options():
|
||||
parser.error(
|
||||
"Can't give ICCID/IMSI for batch mode, need to use automatic parameters ! see --num and --secret for more information")
|
||||
|
||||
if args:
|
||||
parser.error("Extraneous arguments")
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@@ -649,6 +622,9 @@ def read_params_csv(opts, imsi=None, iccid=None):
|
||||
|
||||
def write_params_hlr(opts, params):
|
||||
# SQLite3 OpenBSC HLR
|
||||
# FIXME: The format of the osmo-hlr database has evolved, so that the code below will no longer work.
|
||||
print("Warning: the database format of recent OsmoHLR versions is not compatible with pySim-prog!")
|
||||
|
||||
if opts.write_hlr:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(opts.write_hlr)
|
||||
@@ -749,16 +725,18 @@ def process_card(scc, opts, first, ch):
|
||||
card.erase()
|
||||
card.reset()
|
||||
|
||||
cp = None
|
||||
|
||||
# Generate parameters
|
||||
if opts.source == 'cmdline':
|
||||
cp = gen_parameters(opts)
|
||||
elif opts.source == 'csv':
|
||||
imsi = None
|
||||
iccid = None
|
||||
if opts.read_iccid:
|
||||
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
|
||||
iccid = dec_iccid(res)
|
||||
elif opts.read_imsi:
|
||||
else:
|
||||
iccid = opts.iccid
|
||||
if opts.read_imsi:
|
||||
(res, _) = scc.read_binary(EF['IMSI'])
|
||||
imsi = swap_nibbles(res)[3:]
|
||||
else:
|
||||
|
||||
@@ -29,7 +29,9 @@ import random
|
||||
import re
|
||||
import sys
|
||||
|
||||
from pySim.ts_51_011 import EF_SST_map, EF_AD
|
||||
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
|
||||
|
||||
from pySim.profile.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
|
||||
@@ -40,8 +42,8 @@ from pySim.commands import SimCardCommands
|
||||
from pySim.transport import init_reader, argparse_add_reader_args
|
||||
from pySim.exceptions import SwMatchError
|
||||
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
|
||||
from pySim.utils import dec_imsi, dec_iccid
|
||||
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
|
||||
|
||||
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
@@ -86,7 +88,7 @@ if __name__ == '__main__':
|
||||
scc.sel_ctrl = "0004"
|
||||
|
||||
# Testing for Classic SIM or UICC
|
||||
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00")
|
||||
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00" + "00")
|
||||
if sw == '6e00':
|
||||
# Just a Classic SIM
|
||||
scc.cla_byte = "a0"
|
||||
|
||||
614
pySim-shell.py
614
pySim-shell.py
@@ -21,6 +21,7 @@ from typing import List, Optional
|
||||
|
||||
import json
|
||||
import traceback
|
||||
import re
|
||||
|
||||
import cmd2
|
||||
from packaging import version
|
||||
@@ -47,16 +48,18 @@ from io import StringIO
|
||||
|
||||
from pprint import pprint as pp
|
||||
|
||||
from osmocom.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, is_hexstr, is_decimal
|
||||
from osmocom.utils import is_hexstr_or_decimal, Hexstr
|
||||
from osmocom.tlv import bertlv_parse_one
|
||||
|
||||
from pySim.exceptions import *
|
||||
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
|
||||
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.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, dec_iccid, sw_match
|
||||
from pySim.card_handler import CardHandler, CardHandlerAuto
|
||||
|
||||
from pySim.filesystem import CardMF, CardDF, CardADF
|
||||
from pySim.filesystem import CardMF, CardEF, CardDF, CardADF, LinFixedEF, TransparentEF, BerTlvEF
|
||||
from pySim.ts_102_221 import pin_names
|
||||
from pySim.ts_102_222 import Ts102222Commands
|
||||
from pySim.gsm_r import DF_EIRENE
|
||||
from pySim.cat import ProactiveCommand
|
||||
|
||||
from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
|
||||
@@ -110,6 +113,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.conserve_write = True
|
||||
self.json_pretty_print = True
|
||||
self.apdu_trace = False
|
||||
self.apdu_strict = False
|
||||
|
||||
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
|
||||
onchange_cb=self._onchange_numeric_path))
|
||||
@@ -118,6 +122,9 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
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.add_settable(Settable2Compat('apdu_strict', bool,
|
||||
'Enforce APDU responses according to ISO/IEC 7816-3, table 12', self,
|
||||
onchange_cb=self._onchange_apdu_strict))
|
||||
self.equip(card, rs)
|
||||
|
||||
def equip(self, card, rs):
|
||||
@@ -160,9 +167,9 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
|
||||
try:
|
||||
self.lchan.select('MF/EF.ICCID', self)
|
||||
self.iccid = dec_iccid(self.lchan.read_binary()[0])
|
||||
rs.identity['ICCID'] = dec_iccid(self.lchan.read_binary()[0])
|
||||
except:
|
||||
self.iccid = None
|
||||
rs.identity['ICCID'] = None
|
||||
|
||||
self.lchan.select('MF', self)
|
||||
rc = True
|
||||
@@ -194,6 +201,13 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
else:
|
||||
self.card._scc._tp.apdu_tracer = None
|
||||
|
||||
def _onchange_apdu_strict(self, param_name, old, new):
|
||||
if self.card:
|
||||
if new == True:
|
||||
self.card._scc._tp.apdu_strict = True
|
||||
else:
|
||||
self.card._scc._tp.apdu_strict = False
|
||||
|
||||
class Cmd2ApduTracer(ApduTracer):
|
||||
def __init__(self, cmd2_app):
|
||||
self.cmd2 = cmd2_app
|
||||
@@ -205,7 +219,11 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
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 (%02u:%s)> ' % (self.lchan.lchan_nr, 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)> '
|
||||
@@ -231,8 +249,10 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.equip(card, rs)
|
||||
|
||||
apdu_cmd_parser = argparse.ArgumentParser()
|
||||
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('--expect-response-regex', help='match response against regex', type=str, default=None)
|
||||
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
|
||||
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
|
||||
|
||||
@cmd2.with_argparser(apdu_cmd_parser)
|
||||
def do_apdu(self, opts):
|
||||
@@ -245,7 +265,10 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
# 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).
|
||||
data, sw = self.card._scc._tp.send_apdu(opts.APDU)
|
||||
if opts.raw or self.lchan is None:
|
||||
data, sw = self.card._scc.send_apdu(opts.APDU, apply_lchan = False)
|
||||
else:
|
||||
data, sw = self.lchan.scc.send_apdu(opts.APDU, apply_lchan = False)
|
||||
if data:
|
||||
self.poutput("SW: %s, RESP: %s" % (sw, data))
|
||||
else:
|
||||
@@ -253,6 +276,22 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
if opts.expect_sw:
|
||||
if not sw_match(sw, opts.expect_sw):
|
||||
raise SwMatchError(sw, opts.expect_sw)
|
||||
if opts.expect_response_regex:
|
||||
response_regex_compiled = re.compile(opts.expect_response_regex)
|
||||
if re.match(response_regex_compiled, data) is None:
|
||||
raise ValueError("RESP does not match regex \'%s\'" % opts.expect_response_regex)
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_reset(self, opts):
|
||||
"""Reset the Card."""
|
||||
if self.rs is None:
|
||||
# In case no runtime state is available we go the direct route
|
||||
self.card._scc.reset_card()
|
||||
atr = b2h(self.card._scc.get_atr())
|
||||
else:
|
||||
atr = self.rs.reset(self)
|
||||
self.poutput('Card ATR: %s' % atr)
|
||||
self.update_prompt()
|
||||
|
||||
class InterceptStderr(list):
|
||||
def __init__(self):
|
||||
@@ -330,8 +369,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
return -1
|
||||
|
||||
bulk_script_parser = argparse.ArgumentParser()
|
||||
bulk_script_parser.add_argument(
|
||||
'script_path', help="path to the script file")
|
||||
bulk_script_parser.add_argument('SCRIPT_PATH', help="path to the script file")
|
||||
bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
|
||||
action='store_true')
|
||||
bulk_script_parser.add_argument('--tries', type=int, default=2,
|
||||
@@ -347,7 +385,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
"""Run script on multiple cards (bulk provisioning)"""
|
||||
|
||||
# Make sure that the script file exists and that it is readable.
|
||||
if not os.access(opts.script_path, os.R_OK):
|
||||
if not os.access(opts.SCRIPT_PATH, os.R_OK):
|
||||
self.poutput("Invalid script file!")
|
||||
return
|
||||
|
||||
@@ -357,7 +395,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
first = True
|
||||
while 1:
|
||||
# TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
|
||||
# The ratinale is: There may be a problem with the device, we do want to prevent that
|
||||
# The rationale is: There may be a problem with the device, we do want to prevent that
|
||||
# all remaining cards are fired to the error bin. This is only relevant for situations
|
||||
# with large stacks, probably we do not need this feature right now.
|
||||
|
||||
@@ -372,7 +410,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
os.system(opts.pre_card_action)
|
||||
|
||||
# process the card
|
||||
rc = self._process_card(first, opts.script_path)
|
||||
rc = self._process_card(first, opts.SCRIPT_PATH)
|
||||
if rc == 0:
|
||||
success_count = success_count + 1
|
||||
self._show_success_sign()
|
||||
@@ -424,13 +462,13 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
first = False
|
||||
|
||||
echo_parser = argparse.ArgumentParser()
|
||||
echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
|
||||
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(' '.join(opts.string))
|
||||
self.poutput(' '.join(opts.STRING))
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_version(self, opts):
|
||||
@@ -479,12 +517,25 @@ class PySimCommands(CommandSet):
|
||||
self._cmd.poutput(directory_str)
|
||||
self._cmd.poutput("%d files" % len(selectables))
|
||||
|
||||
def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
|
||||
def __walk_action(self, action, filename, context, **kwargs):
|
||||
# Changing the currently selected file while walking over the filesystem tree would disturb the
|
||||
# walk, so we memorize the currently selected file here so that we can select it again after
|
||||
# we have executed the action callback.
|
||||
selected_file_before_action = self._cmd.lchan.selected_file
|
||||
|
||||
# Perform action
|
||||
action(filename, context, **kwargs)
|
||||
|
||||
# When the action callback is done, make sure the file that was selected before is selected again.
|
||||
if selected_file_before_action != self._cmd.lchan.selected_file:
|
||||
self._cmd.lchan.select_file(selected_file_before_action, self._cmd)
|
||||
|
||||
def __walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
|
||||
"""Recursively walk through the file system, starting at the currently selected DF"""
|
||||
|
||||
if isinstance(self._cmd.lchan.selected_file, CardDF):
|
||||
if action_df:
|
||||
action_df(context, **kwargs)
|
||||
self.__walk_action(action_df, self._cmd.lchan.selected_file.name, context, **kwargs)
|
||||
|
||||
files = self._cmd.lchan.selected_file.get_selectables(
|
||||
flags=['FNAMES', 'ANAMES'])
|
||||
@@ -517,143 +568,45 @@ class PySimCommands(CommandSet):
|
||||
# If the DF was skipped, we never have entered the directory
|
||||
# below, so we must not move up.
|
||||
if skip_df == False:
|
||||
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
|
||||
|
||||
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)
|
||||
self.__walk(indent + 1, action_ef, action_df, context, **kwargs)
|
||||
self._cmd.lchan.select_file(self._cmd.lchan.selected_file.parent, self._cmd)
|
||||
|
||||
elif action_ef:
|
||||
df_before_action = self._cmd.lchan.selected_file
|
||||
action_ef(f, context, **kwargs)
|
||||
# When walking through the file system tree the action must not
|
||||
# always restore the currently selected file to the file that
|
||||
# was selected before executing the action() callback.
|
||||
if df_before_action != self._cmd.lchan.selected_file:
|
||||
raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
|
||||
% (str(self._cmd.lchan.selected_file), str(df_before_action)))
|
||||
self.__walk_action(action_ef, f, context, **kwargs)
|
||||
|
||||
def do_tree(self, opts):
|
||||
"""Display a filesystem-tree with all selectable files"""
|
||||
self.walk()
|
||||
self.__walk()
|
||||
|
||||
def export_ef(self, filename, context, as_json):
|
||||
""" Select and export a single elementary file (EF) """
|
||||
def __export_file(self, filename, context, as_json):
|
||||
""" Select and export a single file (EF, DF or ADF) """
|
||||
context['COUNT'] += 1
|
||||
df = self._cmd.lchan.selected_file
|
||||
|
||||
# The currently selected file (not the file we are going to export)
|
||||
# must always be an ADF or DF. From this starting point we select
|
||||
# the EF we want to export. To maintain consistency we will then
|
||||
# select the current DF again (see comment below).
|
||||
if not isinstance(df, CardDF):
|
||||
raise RuntimeError(
|
||||
"currently selected file %s is not a DF or ADF" % str(df))
|
||||
file = self._cmd.lchan.get_file_by_name(filename)
|
||||
if file:
|
||||
self._cmd.poutput(boxed_heading_str(file.fully_qualified_path_str(True)))
|
||||
self._cmd.poutput("# directory: %s (%s)" % (file.fully_qualified_path_str(True),
|
||||
file.fully_qualified_path_str(False)))
|
||||
else:
|
||||
# If this is called from self.__walk(), then it is ensured that the file exists.
|
||||
raise RuntimeError("cannot export, file %s does not exist in the file system tree" % filename)
|
||||
|
||||
df_path_list = df.fully_qualified_path(True)
|
||||
df_path = df.fully_qualified_path_str(True)
|
||||
df_path_fid = df.fully_qualified_path_str(False)
|
||||
|
||||
file_str = df_path + "/" + str(filename)
|
||||
self._cmd.poutput(boxed_heading_str(file_str))
|
||||
|
||||
self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
|
||||
try:
|
||||
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
|
||||
self._cmd.poutput("# file: %s (%s)" % (
|
||||
self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
|
||||
|
||||
structure = self._cmd.lchan.selected_file_structure()
|
||||
self._cmd.poutput("# structure: %s" % str(structure))
|
||||
fcp_dec = self._cmd.lchan.select_file(file, self._cmd)
|
||||
self._cmd.poutput("# file: %s (%s)" %
|
||||
(self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
|
||||
if isinstance(self._cmd.lchan.selected_file, CardEF):
|
||||
self._cmd.poutput("# structure: %s" % str(self._cmd.lchan.selected_file_structure()))
|
||||
self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
|
||||
self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
|
||||
|
||||
for f in df_path_list:
|
||||
self._cmd.poutput("select " + str(f))
|
||||
self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
|
||||
|
||||
if structure == 'transparent':
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_binary_dec()
|
||||
self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
|
||||
else:
|
||||
result = self._cmd.lchan.read_binary()
|
||||
self._cmd.poutput("update_binary " + str(result[0]))
|
||||
elif structure == 'cyclic' or structure == 'linear_fixed':
|
||||
# Use number of records specified in select response
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
if num_of_rec:
|
||||
for r in range(1, num_of_rec + 1):
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
||||
|
||||
# When the select response does not return the number of records, read until we hit the
|
||||
# first record that cannot be read.
|
||||
else:
|
||||
r = 1
|
||||
while True:
|
||||
try:
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
||||
except SwMatchError as e:
|
||||
# We are past the last valid record - stop
|
||||
if e.sw_actual == "9402":
|
||||
break
|
||||
# Some other problem occurred
|
||||
else:
|
||||
raise e
|
||||
r = r + 1
|
||||
elif structure == 'ber_tlv':
|
||||
tags = self._cmd.lchan.retrieve_tags()
|
||||
for t in tags:
|
||||
result = self._cmd.lchan.retrieve_data(t)
|
||||
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
|
||||
self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Unsupported structure "%s" of file "%s"' % (structure, filename))
|
||||
self._cmd.poutput("select " + self._cmd.lchan.selected_file.fully_qualified_path_str())
|
||||
self._cmd.poutput(self._cmd.lchan.selected_file.export(as_json, self._cmd.lchan))
|
||||
except Exception as e:
|
||||
bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
|
||||
bad_file_str = file.fully_qualified_path_str(True) + "/" + str(file.name) + ", " + str(e)
|
||||
self._cmd.poutput("# bad file: %s" % bad_file_str)
|
||||
context['ERR'] += 1
|
||||
context['BAD'].append(bad_file_str)
|
||||
|
||||
# When reading the file is done, make sure the parent file is
|
||||
# selected again. This will be the usual case, however we need
|
||||
# to check before since we must not select the same DF twice
|
||||
if df != self._cmd.lchan.selected_file:
|
||||
self._cmd.lchan.select(df.fid or df.aid, self._cmd)
|
||||
|
||||
self._cmd.poutput("#")
|
||||
|
||||
export_parser = argparse.ArgumentParser()
|
||||
@@ -671,10 +624,10 @@ class PySimCommands(CommandSet):
|
||||
exception_str_add = ""
|
||||
|
||||
if opts.filename:
|
||||
self.export_ef(opts.filename, context, **kwargs_export)
|
||||
self.__walk_action(self.__export_file, opts.filename, context, **kwargs_export)
|
||||
else:
|
||||
try:
|
||||
self.walk(0, self.export_ef, None, context, **kwargs_export)
|
||||
self.__walk(0, self.__export_file, self.__export_file, context, **kwargs_export)
|
||||
except Exception as e:
|
||||
print("# Stopping early here due to exception: " + str(e))
|
||||
print("#")
|
||||
@@ -702,47 +655,202 @@ 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.card.reset()
|
||||
self._cmd.poutput('Card ATR: %s' % i2h(atr))
|
||||
self._cmd.update_prompt()
|
||||
def __dump_file(self, filename, context, as_json):
|
||||
""" Select and dump a single file (EF, DF or ADF) """
|
||||
file = self._cmd.lchan.get_file_by_name(filename)
|
||||
if file:
|
||||
res = {
|
||||
'path': file.fully_qualified_path(True)
|
||||
}
|
||||
else:
|
||||
# If this is called from self.__walk(), then it is ensured that the file exists.
|
||||
raise RuntimeError("cannot dump, file %s does not exist in the file system tree" % filename)
|
||||
|
||||
try:
|
||||
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
|
||||
|
||||
# File control parameters (common for EF, DF and ADF files)
|
||||
if not self._cmd.lchan.selected_file_fcp_hex:
|
||||
# An application without a real ADF (like ADF.ARA-M) / filesystem
|
||||
return
|
||||
|
||||
res['fcp_raw'] = str(self._cmd.lchan.selected_file_fcp_hex)
|
||||
res['fcp'] = fcp_dec
|
||||
|
||||
# File structure and contents (EF only)
|
||||
if isinstance(self._cmd.lchan.selected_file, CardEF):
|
||||
structure = self._cmd.lchan.selected_file_structure()
|
||||
if structure == 'transparent':
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_binary_dec()
|
||||
body = result[0]
|
||||
else:
|
||||
result = self._cmd.lchan.read_binary()
|
||||
body = str(result[0])
|
||||
elif structure == 'cyclic' or structure == 'linear_fixed':
|
||||
body = []
|
||||
# Use number of records specified in select response
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
if num_of_rec:
|
||||
for r in range(1, num_of_rec + 1):
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
body.append(result[0])
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
body.append(str(result[0]))
|
||||
|
||||
# When the select response does not return the number of records, read until we hit the
|
||||
# first record that cannot be read.
|
||||
else:
|
||||
r = 1
|
||||
while True:
|
||||
try:
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
body.append(result[0])
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
body.append(str(result[0]))
|
||||
except SwMatchError as e:
|
||||
# We are past the last valid record - stop
|
||||
if e.sw_actual == "9402":
|
||||
break
|
||||
# Some other problem occurred
|
||||
raise e
|
||||
r = r + 1
|
||||
elif structure == 'ber_tlv':
|
||||
tags = self._cmd.lchan.retrieve_tags()
|
||||
body = {}
|
||||
for t in tags:
|
||||
result = self._cmd.lchan.retrieve_data(t)
|
||||
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
|
||||
body[t] = b2h(val)
|
||||
else:
|
||||
raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
|
||||
res['body'] = body
|
||||
|
||||
except SwMatchError as e:
|
||||
res['error'] = {
|
||||
'sw_actual': e.sw_actual,
|
||||
'sw_expected': e.sw_expected,
|
||||
'message': e.description,
|
||||
}
|
||||
except Exception as e:
|
||||
raise(e)
|
||||
res['error'] = {
|
||||
'message': str(e)
|
||||
}
|
||||
|
||||
context['result']['files'][file.fully_qualified_path_str(True)] = res
|
||||
|
||||
fsdump_parser = argparse.ArgumentParser()
|
||||
fsdump_parser.add_argument(
|
||||
'--filename', type=str, default=None, help='only export specific (named) file')
|
||||
fsdump_parser.add_argument(
|
||||
'--json', action='store_true', help='export file contents as JSON (less reliable)')
|
||||
|
||||
@cmd2.with_argparser(fsdump_parser)
|
||||
def do_fsdump(self, opts):
|
||||
"""Export filesystem metadata and file contents of all files below current DF in
|
||||
machine-readable json format. This is similar to "export", but much easier to parse by
|
||||
downstream processing tools. You usually may want to call this from the MF and verify
|
||||
the ADM1 PIN (if available) to maximize the amount of readable files."""
|
||||
result = {
|
||||
'name': self._cmd.card.name,
|
||||
'atr': self._cmd.rs.identity['ATR'],
|
||||
'eid': self._cmd.rs.identity.get('EID', None),
|
||||
'iccid': self._cmd.rs.identity.get('ICCID', None),
|
||||
'aids': {x.aid:{} for x in self._cmd.rs.mf.applications.values()},
|
||||
'files': {},
|
||||
}
|
||||
context = {'result': result, 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
|
||||
kwargs_export = {'as_json': opts.json}
|
||||
exception_str_add = ""
|
||||
|
||||
if opts.filename:
|
||||
self.__walk_action(self.__dump_file, opts.filename, context, **kwargs_export)
|
||||
else:
|
||||
# export an entire subtree
|
||||
try:
|
||||
self.__walk(0, self.__dump_file, self.__dump_file, context, **kwargs_export)
|
||||
except Exception as e:
|
||||
print("# Stopping early here due to exception: " + str(e))
|
||||
print("#")
|
||||
exception_str_add = ", also had to stop early due to exception:" + str(e)
|
||||
#raise e
|
||||
|
||||
self._cmd.poutput_json(context['result'])
|
||||
|
||||
|
||||
def do_desc(self, opts):
|
||||
"""Display human readable file description for the currently selected file"""
|
||||
desc = self._cmd.lchan.selected_file.desc
|
||||
if desc:
|
||||
self._cmd.poutput(desc)
|
||||
self._cmd.poutput("%s: %s" % (self._cmd.lchan.selected_file, desc))
|
||||
else:
|
||||
self._cmd.poutput("no description available")
|
||||
self._cmd.poutput("%s: no description available" % self._cmd.lchan.selected_file)
|
||||
self._cmd.poutput(" file structure: %s" % self._cmd.lchan.selected_file_structure())
|
||||
if isinstance(self._cmd.lchan.selected_file, LinFixedEF):
|
||||
self._cmd.poutput(" record length:")
|
||||
self._cmd.poutput(" minimum_length: %s" % str(self._cmd.lchan.selected_file.rec_len[0]))
|
||||
self._cmd.poutput(" recommended_length: %s" % str(self._cmd.lchan.selected_file.rec_len[1]))
|
||||
self._cmd.poutput(" actual_length: %s" % str(self._cmd.lchan.selected_file_record_len()))
|
||||
self._cmd.poutput(" number of records: %s" % str(self._cmd.lchan.selected_file_num_of_rec()))
|
||||
elif isinstance(self._cmd.lchan.selected_file, TransparentEF):
|
||||
self._cmd.poutput(" file size:")
|
||||
self._cmd.poutput(" minimum_size: %s" % str(self._cmd.lchan.selected_file.size[0]))
|
||||
self._cmd.poutput(" recommended_size: %s" % str(self._cmd.lchan.selected_file.size[1]))
|
||||
self._cmd.poutput(" actual_size: %s" % str(self._cmd.lchan.selected_file_size()))
|
||||
elif isinstance(self._cmd.lchan.selected_file, BerTlvEF):
|
||||
self._cmd.poutput(" file size:")
|
||||
self._cmd.poutput(" minimum_size: %s" % str(self._cmd.lchan.selected_file.size[0]))
|
||||
self._cmd.poutput(" recommended_size: %s" % str(self._cmd.lchan.selected_file.size[1]))
|
||||
self._cmd.poutput(" actual_size: %s" % str(self._cmd.lchan.selected_file_size()))
|
||||
self._cmd.poutput(" reserved_file_size: %s" % str(self._cmd.lchan.selected_file_reserved_file_size()))
|
||||
self._cmd.poutput(" maximum_file_size: %s" % str(self._cmd.lchan.selected_file_maximum_file_size()))
|
||||
|
||||
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')
|
||||
verify_adm_parser.add_argument('--pin-is-hex', action='store_true',
|
||||
help='ADM pin value is specified as hex-string (not decimal)')
|
||||
verify_adm_parser.add_argument('--adm-type',
|
||||
choices=[x for x in pin_names.values() if x.startswith('ADM')],
|
||||
help='Override ADM number. Default is card-model-specific, usually 1')
|
||||
verify_adm_parser.add_argument('ADM', nargs='?', type=is_hexstr_or_decimal,
|
||||
help='ADM 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(opts.ADM1)
|
||||
to get write/update permissions to most of the files on SIM cards.
|
||||
"""
|
||||
if opts.adm_type:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
adm_chv_num = pin_names.inverse[opts.adm_type]
|
||||
else:
|
||||
# try to find an ADM-PIN if none is specified
|
||||
result = card_key_provider_get_field(
|
||||
'ADM1', key='ICCID', value=self._cmd.iccid)
|
||||
pin_adm = sanitize_pin_adm(result)
|
||||
if pin_adm:
|
||||
self._cmd.poutput(
|
||||
"found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
|
||||
adm_chv_num = self._cmd.card._adm_chv_num
|
||||
if opts.ADM:
|
||||
# use specified ADM-PIN
|
||||
if opts.pin_is_hex:
|
||||
pin_adm = sanitize_pin_adm(None, opts.ADM)
|
||||
else:
|
||||
raise ValueError(
|
||||
"cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
|
||||
pin_adm = sanitize_pin_adm(opts.ADM)
|
||||
else:
|
||||
iccid = self._cmd.rs.identity['ICCID']
|
||||
adm_type = opts.adm_type or 'ADM1'
|
||||
# try to find an ADM-PIN if none is specified
|
||||
result = card_key_provider_get_field(adm_type, key='ICCID', value=iccid)
|
||||
if opts.pin_is_hex or (result and len(result) > 8):
|
||||
pin_adm = sanitize_pin_adm(None, result)
|
||||
else:
|
||||
pin_adm = sanitize_pin_adm(result)
|
||||
if pin_adm:
|
||||
self._cmd.poutput("found %s '%s' for ICCID '%s'" % (adm_type, result, iccid))
|
||||
else:
|
||||
raise ValueError("cannot find %s for ICCID '%s'" % (adm_type, iccid))
|
||||
|
||||
if pin_adm:
|
||||
self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
|
||||
self._cmd.lchan.scc.verify_chv(adm_chv_num, h2b(pin_adm))
|
||||
else:
|
||||
raise ValueError("error: cannot authenticate, no adm-pin!")
|
||||
|
||||
@@ -750,13 +858,17 @@ Currently only ADM1 is supported."""
|
||||
"""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)
|
||||
self._cmd.poutput(" ATR: %s" % self._cmd.rs.identity['ATR'].lower())
|
||||
eid = self._cmd.rs.identity.get('EID', None)
|
||||
if eid:
|
||||
self._cmd.poutput(" EID: %s" % eid.lower())
|
||||
self._cmd.poutput(" ICCID: %s" % self._cmd.rs.identity['ICCID'].lower())
|
||||
self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte.lower())
|
||||
self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl.lower())
|
||||
if len(self._cmd.rs.mf.applications) > 0:
|
||||
self._cmd.poutput(" AIDs:")
|
||||
for a in self._cmd.rs.mf.applications:
|
||||
self._cmd.poutput(" %s" % a.lower())
|
||||
|
||||
@with_default_category('ISO7816 Commands')
|
||||
class Iso7816Commands(CommandSet):
|
||||
@@ -781,69 +893,64 @@ class Iso7816Commands(CommandSet):
|
||||
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)
|
||||
|
||||
def get_code(self, code):
|
||||
"""Use code either directly or try to get it from external data source"""
|
||||
auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
|
||||
|
||||
if str(code).upper() not in auto:
|
||||
def get_code(self, code, field):
|
||||
"""Use code either directly or try to get it from external data source using the provided field name"""
|
||||
if code is not None:
|
||||
return sanitize_pin_adm(code)
|
||||
|
||||
result = card_key_provider_get_field(
|
||||
str(code), key='ICCID', value=self._cmd.iccid)
|
||||
iccid = self._cmd.rs.identity['ICCID']
|
||||
result = card_key_provider_get_field(field, key='ICCID', value=iccid)
|
||||
result = sanitize_pin_adm(result)
|
||||
if result:
|
||||
self._cmd.poutput("found %s '%s' for ICCID '%s'" %
|
||||
(code.upper(), result, self._cmd.iccid))
|
||||
self._cmd.poutput("found %s '%s' for ICCID '%s'" % (field, result, iccid))
|
||||
else:
|
||||
self._cmd.poutput("cannot find %s for ICCID '%s'" %
|
||||
(code.upper(), self._cmd.iccid))
|
||||
raise RuntimeError("cannot find %s for ICCID '%s'" % (field, iccid))
|
||||
return result
|
||||
|
||||
verify_chv_parser = argparse.ArgumentParser()
|
||||
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=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
verify_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
|
||||
@cmd2.with_argparser(verify_chv_parser)
|
||||
def do_verify_chv(self, opts):
|
||||
"""Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
|
||||
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)
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
(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=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=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
unblock_chv_parser.add_argument('PUK', nargs='?', type=is_decimal,
|
||||
help='PUK code value. If none given, CSV file will be queried')
|
||||
unblock_chv_parser.add_argument('NEWPIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
|
||||
@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)
|
||||
new_pin = self.get_code(opts.NEWPIN, "PIN" + str(opts.pin_nr))
|
||||
puk = self.get_code(opts.PUK, "PUK" + str(opts.pin_nr))
|
||||
(data, sw) = self._cmd.lchan.scc.unblock_chv(
|
||||
opts.pin_nr, h2b(puk), h2b(new_pin))
|
||||
self._cmd.poutput("CHV unblock successful")
|
||||
|
||||
change_chv_parser = argparse.ArgumentParser()
|
||||
change_chv_parser.add_argument('NEWPIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
change_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
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=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=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)
|
||||
new_pin = self.get_code(opts.NEWPIN, "PIN" + str(opts.pin_nr))
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
(data, sw) = self._cmd.lchan.scc.change_chv(
|
||||
opts.pin_nr, h2b(pin), h2b(new_pin))
|
||||
self._cmd.poutput("CHV change successful")
|
||||
@@ -851,26 +958,26 @@ class Iso7816Commands(CommandSet):
|
||||
disable_chv_parser = argparse.ArgumentParser()
|
||||
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=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
disable_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
|
||||
@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)
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
(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=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
enable_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
|
||||
@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)
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
(data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
|
||||
self._cmd.poutput("CHV enable successful")
|
||||
|
||||
@@ -882,8 +989,15 @@ class Iso7816Commands(CommandSet):
|
||||
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]:
|
||||
@@ -893,7 +1007,7 @@ class Iso7816Commands(CommandSet):
|
||||
|
||||
open_chan_parser = argparse.ArgumentParser()
|
||||
open_chan_parser.add_argument(
|
||||
'chan_nr', type=int, default=0, help='Channel Number')
|
||||
'chan_nr', type=int, default=1, choices=range(1,16), help='Channel Number')
|
||||
|
||||
@cmd2.with_argparser(open_chan_parser)
|
||||
def do_open_channel(self, opts):
|
||||
@@ -905,7 +1019,7 @@ class Iso7816Commands(CommandSet):
|
||||
|
||||
close_chan_parser = argparse.ArgumentParser()
|
||||
close_chan_parser.add_argument(
|
||||
'chan_nr', type=int, default=0, help='Channel Number')
|
||||
'chan_nr', type=int, default=1, choices=range(1,16), help='Channel Number')
|
||||
|
||||
@cmd2.with_argparser(close_chan_parser)
|
||||
def do_close_channel(self, opts):
|
||||
@@ -917,14 +1031,14 @@ class Iso7816Commands(CommandSet):
|
||||
|
||||
switch_chan_parser = argparse.ArgumentParser()
|
||||
switch_chan_parser.add_argument(
|
||||
'chan_nr', type=int, default=0, help='Channel Number')
|
||||
'chan_nr', type=int, default=0, choices=range(0,16), 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.unregister_cmds(self._cmd)
|
||||
self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
|
||||
self._cmd.lchan._select_post(self._cmd)
|
||||
self._cmd.lchan.register_cmds(self._cmd)
|
||||
self._cmd.update_prompt()
|
||||
|
||||
def do_status(self, opts):
|
||||
@@ -950,8 +1064,14 @@ global_group.add_argument('--script', metavar='PATH', default=None,
|
||||
help='script with pySim-shell commands to be executed automatically at start-up')
|
||||
global_group.add_argument('--csv', metavar='FILE',
|
||||
default=None, help='Read card data from CSV file')
|
||||
global_group.add_argument('--csv-column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
|
||||
help='per-CSV-column AES transport key')
|
||||
global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||
help="Use automatic card handling machine")
|
||||
global_group.add_argument("--noprompt", help="Run in non interactive mode",
|
||||
action='store_true', default=False)
|
||||
global_group.add_argument("--skip-card-init", help="Skip all card/profile initialization",
|
||||
action='store_true', default=False)
|
||||
|
||||
adm_group = global_group.add_mutually_exclusive_group()
|
||||
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
|
||||
@@ -959,6 +1079,8 @@ 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('-e', '--execute-command', action='append', default=[],
|
||||
help='A pySim-shell command that will be executed at startup')
|
||||
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,
|
||||
@@ -967,22 +1089,20 @@ option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# Parse options
|
||||
startup_errors = False
|
||||
opts = option_parser.parse_args()
|
||||
|
||||
# If a script file is specified, be sure that it actually exists
|
||||
if opts.script:
|
||||
if not os.access(opts.script, os.R_OK):
|
||||
print("Invalid script file!")
|
||||
sys.exit(2)
|
||||
|
||||
# Register csv-file as card data provider, either from specified CSV
|
||||
# or from CSV file in home directory
|
||||
csv_column_keys = {}
|
||||
for par in opts.csv_column_key:
|
||||
name, key = par.split(':')
|
||||
csv_column_keys[name] = key
|
||||
csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
|
||||
if opts.csv:
|
||||
card_key_provider_register(CardKeyProviderCsv(opts.csv))
|
||||
card_key_provider_register(CardKeyProviderCsv(opts.csv, csv_column_keys))
|
||||
if os.path.isfile(csv_default):
|
||||
card_key_provider_register(CardKeyProviderCsv(csv_default))
|
||||
card_key_provider_register(CardKeyProviderCsv(csv_default, csv_column_keys))
|
||||
|
||||
# Init card reader driver
|
||||
sl = init_reader(opts, proactive_handler = Proact())
|
||||
@@ -997,20 +1117,19 @@ if __name__ == '__main__':
|
||||
# is no card in the reader or the card is unresponsive. PysimApp is
|
||||
# able to tolerate and recover from that.
|
||||
try:
|
||||
rs, card = init_card(sl)
|
||||
app = PysimApp(card, rs, sl, ch, opts.script)
|
||||
rs, card = init_card(sl, opts.skip_card_init)
|
||||
app = PysimApp(card, rs, sl, ch)
|
||||
except:
|
||||
startup_errors = True
|
||||
print("Card initialization (%s) failed with an exception:" % str(sl))
|
||||
print("---------------------8<---------------------")
|
||||
traceback.print_exc()
|
||||
print("---------------------8<---------------------")
|
||||
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
||||
print(
|
||||
" it should also be noted that some readers may behave strangely when no card")
|
||||
print(" is inserted.)")
|
||||
print("")
|
||||
if opts.script:
|
||||
print("will not execute startup script due to card initialization errors!")
|
||||
if not opts.noprompt:
|
||||
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
||||
print(" it should also be noted that some readers may behave strangely when no card")
|
||||
print(" is inserted.)")
|
||||
print("")
|
||||
app = PysimApp(None, None, sl, ch)
|
||||
|
||||
# If the user supplies an ADM PIN at via commandline args authenticate
|
||||
@@ -1022,9 +1141,38 @@ if __name__ == '__main__':
|
||||
try:
|
||||
card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
|
||||
except Exception as e:
|
||||
startup_errors = True
|
||||
print("ADM verification (%s) failed with an exception:" % str(pin_adm))
|
||||
print("---------------------8<---------------------")
|
||||
print(e)
|
||||
print("---------------------8<---------------------")
|
||||
|
||||
# Run optional commands
|
||||
for c in opts.execute_command:
|
||||
if not startup_errors:
|
||||
app.onecmd_plus_hooks(c)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute command (%s)" % c)
|
||||
|
||||
# Run optional command
|
||||
if opts.command:
|
||||
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
|
||||
else:
|
||||
if not startup_errors:
|
||||
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
|
||||
else:
|
||||
print("Errors during startup, refusing to execute command (%s)" % opts.command)
|
||||
|
||||
# Run optional script file
|
||||
if opts.script:
|
||||
if not startup_errors:
|
||||
if not os.access(opts.script, os.R_OK):
|
||||
print("Error: script file (%s) not readable!" % opts.script)
|
||||
startup_errors = True
|
||||
else:
|
||||
app.onecmd_plus_hooks('{} {}'.format('run_script', opts.script), add_to_history = False)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute script (%s)" % opts.script)
|
||||
|
||||
if not opts.noprompt:
|
||||
app.cmdloop()
|
||||
elif startup_errors:
|
||||
sys.exit(2)
|
||||
|
||||
@@ -8,17 +8,21 @@ from pprint import pprint as pp
|
||||
from pySim.apdu import *
|
||||
from pySim.runtime import RuntimeState
|
||||
|
||||
from osmocom.utils import JsonEncoder
|
||||
|
||||
from pySim.cards import UiccCardBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.ts_31_102 import CardApplicationUSIM
|
||||
from pySim.ts_31_103 import CardApplicationISIM
|
||||
from pySim.euicc import CardApplicationISDR, CardApplicationECASD
|
||||
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_source.tca_loader_log import TcaLoaderLogApduSource
|
||||
|
||||
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
|
||||
|
||||
@@ -51,7 +55,7 @@ class DummySimLink(LinkBase):
|
||||
def __str__(self):
|
||||
return "dummy"
|
||||
|
||||
def _send_apdu_raw(self, pdu):
|
||||
def _send_apdu(self, pdu):
|
||||
#print("DummySimLink-apdu: %s" % pdu)
|
||||
return [], '9000'
|
||||
|
||||
@@ -61,7 +65,7 @@ class DummySimLink(LinkBase):
|
||||
def disconnect(self):
|
||||
pass
|
||||
|
||||
def reset_card(self):
|
||||
def _reset_card(self):
|
||||
return 1
|
||||
|
||||
def get_atr(self):
|
||||
@@ -78,6 +82,8 @@ class Tracer:
|
||||
profile = CardProfileUICC()
|
||||
profile.add_application(CardApplicationUSIM())
|
||||
profile.add_application(CardApplicationISIM())
|
||||
profile.add_application(CardApplicationISDR())
|
||||
profile.add_application(CardApplicationECASD())
|
||||
scc = SimCardCommands(transport=DummySimLink())
|
||||
card = UiccCardBase(scc)
|
||||
self.rs = RuntimeState(card, profile)
|
||||
@@ -93,7 +99,8 @@ class Tracer:
|
||||
"""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("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id,
|
||||
inst.col_sw, json.dumps(inst.processed, cls=JsonEncoder)))
|
||||
print("===============================")
|
||||
|
||||
def format_reset(self, apdu: CardReset):
|
||||
@@ -178,6 +185,11 @@ parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
|
||||
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
|
||||
help='Name of the network interface to capture on')
|
||||
|
||||
parser_tcaloader_log = subparsers.add_parser('tca-loader-log', help="""
|
||||
Read APDUs from a TCA Loader log file.""")
|
||||
parser_tcaloader_log.add_argument('-f', '--log-file', required=True,
|
||||
help='Name of te log file to be read')
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
opts = option_parser.parse_args()
|
||||
@@ -191,6 +203,10 @@ if __name__ == '__main__':
|
||||
s = PysharkRsproLive(opts.interface)
|
||||
elif opts.source == 'gsmtap-pyshark-pcap':
|
||||
s = PysharkGsmtapPcap(opts.pcap_file)
|
||||
elif opts.source == 'tca-loader-log':
|
||||
s = TcaLoaderLogApduSource(opts.log_file)
|
||||
else:
|
||||
raise ValueError("unsupported source %s", opts.source)
|
||||
|
||||
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
|
||||
show_raw_apdu=opts.show_raw_apdu)
|
||||
|
||||
@@ -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
|
||||
@@ -10,7 +9,7 @@ is far too simplistic, while this decoder can utilize all of the information
|
||||
we already know in pySim about the filesystem structure, file encoding, etc.
|
||||
"""
|
||||
|
||||
# (C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
# (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
|
||||
@@ -27,14 +26,14 @@ 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 construct import *
|
||||
from termcolor import colored
|
||||
from construct import Byte, GreedyBytes
|
||||
from construct import Optional as COptional
|
||||
from pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
|
||||
from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
|
||||
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
||||
|
||||
@@ -52,8 +51,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))
|
||||
@@ -150,8 +149,10 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
# fall-back constructs if the derived class provides no override
|
||||
_construct_p1 = Byte
|
||||
_construct_p2 = Byte
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
_construct_rsp = HexAdapter(GreedyBytes)
|
||||
_construct = GreedyBytes
|
||||
_construct_rsp = GreedyBytes
|
||||
_tlv = None
|
||||
_tlv_rsp = None
|
||||
|
||||
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
|
||||
"""Instantiate a new ApduCommand from give cmd + resp."""
|
||||
@@ -187,44 +188,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())
|
||||
@@ -236,7 +232,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']
|
||||
@@ -275,7 +271,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
"""Does the given CLA match the CLA list of the command?."""
|
||||
if not isinstance(cla, str):
|
||||
cla = '%02X' % cla
|
||||
cla = cla.lower()
|
||||
cla = cla.upper()
|
||||
# see https://github.com/PyCQA/pylint/issues/7219
|
||||
# pylint: disable=no-member
|
||||
for cla_match in cls._cla:
|
||||
@@ -285,7 +281,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
cla_masked += 'X'
|
||||
else:
|
||||
cla_masked += cla[i]
|
||||
if cla_masked == cla_match:
|
||||
if cla_masked == cla_match.upper():
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -295,17 +291,26 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
if callable(method):
|
||||
return method()
|
||||
else:
|
||||
r = {}
|
||||
method = getattr(self, '_decode_p1p2', None)
|
||||
if callable(method):
|
||||
r = self._decode_p1p2()
|
||||
return self._cmd_to_dict()
|
||||
|
||||
def _cmd_to_dict(self) -> Dict:
|
||||
"""back-end function performing automatic decoding using _construct / _tlv."""
|
||||
r = {}
|
||||
method = getattr(self, '_decode_p1p2', None)
|
||||
if callable(method):
|
||||
r = self._decode_p1p2()
|
||||
else:
|
||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
|
||||
r['p3'] = self.p3
|
||||
if self.cmd_data:
|
||||
if self._tlv:
|
||||
ie = self._tlv()
|
||||
ie.from_tlv(self.cmd_data)
|
||||
r['body'] = ie.to_dict()
|
||||
else:
|
||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
|
||||
r['p3'] = self.p3
|
||||
if self.cmd_data:
|
||||
r['body'] = parse_construct(self._construct, self.cmd_data)
|
||||
return r
|
||||
return r
|
||||
|
||||
def rsp_to_dict(self) -> Dict:
|
||||
"""Convert the Response part of the APDU to a dict."""
|
||||
@@ -315,7 +320,12 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
else:
|
||||
r = {}
|
||||
if self.rsp_data:
|
||||
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
|
||||
if self._tlv_rsp:
|
||||
ie = self._tlv_rsp()
|
||||
ie.from_tlv(self.rsp_data)
|
||||
r['body'] = ie.to_dict()
|
||||
else:
|
||||
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
|
||||
r['sw'] = b2h(self.sw)
|
||||
return r
|
||||
|
||||
@@ -428,8 +438,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):
|
||||
@@ -452,7 +461,6 @@ class CardReset:
|
||||
self.atr = atr
|
||||
|
||||
def __str__(self):
|
||||
if (self.atr):
|
||||
if self.atr:
|
||||
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
|
||||
else:
|
||||
return '%s' % (type(self).__name__)
|
||||
return '%s' % (type(self).__name__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding=utf-8
|
||||
"""APDU definition/decoder of GlobalPLatform Card Spec (currently 2.1.1)
|
||||
|
||||
(C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
(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
|
||||
@@ -17,7 +17,11 @@ 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 FlagsEnum, Struct
|
||||
from osmocom.tlv import flatten_dict_lists
|
||||
from osmocom.construct import *
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
from pySim.global_platform import InstallParameters
|
||||
|
||||
class GpDelete(ApduCommand, n='DELETE', ins=0xE4, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
@@ -40,8 +44,29 @@ class GpGetDataCB(ApduCommand, n='GET DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
|
||||
class GpGetStatus(ApduCommand, n='GET STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
|
||||
# GPCS Section 11.5.2
|
||||
class GpInstall(ApduCommand, n='INSTALL', ins=0xE6, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
_construct_p1 = FlagsEnum(Byte, more_commands=0x80, for_registry_update=0x40,
|
||||
for_personalization=0x20, for_extradition=0x10,
|
||||
for_make_selectable=0x08, for_install=0x04, for_load=0x02)
|
||||
_construct_p2 = Enum(Byte, no_info_provided=0x00, beginning_of_combined=0x01,
|
||||
end_of_combined=0x03)
|
||||
_construct = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'module_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'application_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'privileges'/Prefixed(Int8ub, GreedyBytes),
|
||||
'install_parameters'/Prefixed(Int8ub, GreedyBytes), # TODO: InstallParameters
|
||||
'install_token'/Prefixed(Int8ub, GreedyBytes))
|
||||
def _decode_cmd(self):
|
||||
# first use _construct* above
|
||||
res = self._cmd_to_dict()
|
||||
# then do TLV decode of install_parameters
|
||||
ip = InstallParameters()
|
||||
ip.from_tlv(res['body']['install_parameters'])
|
||||
res['body']['install_parameters'] = flatten_dict_lists(ip.to_dict())
|
||||
return res
|
||||
|
||||
|
||||
class GpLoad(ApduCommand, n='LOAD', ins=0xE8, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding=utf-8
|
||||
"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
|
||||
|
||||
(C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
(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
|
||||
@@ -17,12 +17,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, Dict
|
||||
import logging
|
||||
from pySim.construct import *
|
||||
|
||||
from construct import GreedyRange, Struct
|
||||
|
||||
from osmocom.utils import i2h
|
||||
from osmocom.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 import cat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,7 +100,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
||||
logger.warning('SELECT UNKNOWN FID %s', file_hex)
|
||||
elif mode == 'df_name':
|
||||
# Select by AID (can be sub-string!)
|
||||
aid = self.cmd_dict['body']
|
||||
aid = b2h(self.cmd_dict['body'])
|
||||
sels = lchan.rs.mf.get_app_selectables(['AIDS'])
|
||||
adf = self._find_aid_substr(sels, aid)
|
||||
if adf:
|
||||
@@ -103,7 +109,6 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
||||
#print("\tSELECT AID %s" % adf)
|
||||
else:
|
||||
logger.warning('SELECT UNKNOWN AID %s', aid)
|
||||
pass
|
||||
else:
|
||||
raise ValueError('Select Mode %s not implemented' % mode)
|
||||
# decode the SELECT response
|
||||
@@ -111,7 +116,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
||||
self.file = lchan.selected_file
|
||||
if 'body' in self.rsp_dict:
|
||||
# not every SELECT is asking for the FCP in response...
|
||||
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
|
||||
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
|
||||
return None
|
||||
|
||||
|
||||
@@ -124,7 +129,7 @@ class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
if self.cmd_dict['p2'] == 'response_like_select':
|
||||
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
|
||||
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
|
||||
|
||||
def _decode_binary_p1p2(p1, p2) -> Dict:
|
||||
ret = {}
|
||||
@@ -290,12 +295,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):
|
||||
@@ -307,7 +309,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):
|
||||
@@ -319,7 +321,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):
|
||||
@@ -330,7 +332,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):
|
||||
@@ -342,7 +344,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):
|
||||
@@ -395,13 +397,12 @@ class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X',
|
||||
manage_channel.add_lchan(created_channel_nr)
|
||||
self.col_id = '%02u' % created_channel_nr
|
||||
return {'mode': mode, 'created_channel': created_channel_nr }
|
||||
elif mode == 'close_channel':
|
||||
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
|
||||
return {'mode': mode, 'closed_channel': closed_channel_nr }
|
||||
else:
|
||||
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
|
||||
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']):
|
||||
@@ -419,13 +420,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)))
|
||||
|
||||
@@ -436,8 +437,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']):
|
||||
@@ -460,14 +460,17 @@ class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
|
||||
# TS 102 221 Section 11.2.2 / TS 102 223
|
||||
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
|
||||
_apdu_case = 4
|
||||
_tlv = cat.EventCollection
|
||||
|
||||
# TS 102 221 Section 11.2.3 / TS 102 223
|
||||
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
|
||||
_apdu_case = 2
|
||||
_tlv_rsp = cat.ProactiveCommand
|
||||
|
||||
# TS 102 221 Section 11.2.3 / TS 102 223
|
||||
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
|
||||
_apdu_case = 3
|
||||
_tlv = cat.TerminalResponse
|
||||
|
||||
# TS 102 221 Section 11.3.1
|
||||
class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
|
||||
|
||||
60
pySim/apdu/ts_102_222.py
Normal file
60
pySim/apdu/ts_102_222.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# coding=utf-8
|
||||
"""APDU definitions/decoders of ETSI TS 102 222.
|
||||
|
||||
(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 logging
|
||||
|
||||
from construct import Struct
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
from pySim.ts_102_221 import FcpTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TS 102 222 Section 6.3
|
||||
class CreateFile(ApduCommand, n='CREATE FILE', ins=0xE0, cla=['0X', '4X', 'EX']):
|
||||
_apdu_case = 3
|
||||
_tlv = FcpTemplate
|
||||
|
||||
# TS 102 222 Section 6.4
|
||||
class DeleteFile(ApduCommand, n='DELETE FILE', ins=0xE4, cla=['0X', '4X']):
|
||||
_apdu_case = 3
|
||||
_construct = Struct('file_id'/Bytes(2))
|
||||
|
||||
# TS 102 222 Section 6.7
|
||||
class TerminateDF(ApduCommand, n='TERMINATE DF', ins=0xE6, cla=['0X', '4X']):
|
||||
_apdu_case = 1
|
||||
|
||||
# TS 102 222 Section 6.8
|
||||
class TerminateEF(ApduCommand, n='TERMINATE EF', ins=0xE8, cla=['0X', '4X']):
|
||||
_apdu_case = 1
|
||||
|
||||
# TS 102 222 Section 6.9
|
||||
class TerminateCardUsage(ApduCommand, n='TERMINATE CARD USAGE', ins=0xFE, cla=['0X', '4X']):
|
||||
_apdu_case = 1
|
||||
|
||||
# TS 102 222 Section 6.10
|
||||
class ResizeFile(ApduCommand, n='RESIZE FILE', ins=0xD4, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 3
|
||||
_construct_p1 = Enum(Byte, mode_0=0, mode_1=1)
|
||||
_tlv = FcpTemplate
|
||||
|
||||
|
||||
ApduCommands = ApduCommandSet('TS 102 222', cmds=[CreateFile, DeleteFile, TerminateDF,
|
||||
TerminateEF, TerminateCardUsage, ResizeFile])
|
||||
@@ -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 osmocom.construct import *
|
||||
|
||||
from pySim.filesystem 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
|
||||
@@ -44,28 +42,28 @@ class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '
|
||||
BitsInteger(4),
|
||||
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
|
||||
vgcs_vbs=2, gba=4))
|
||||
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
|
||||
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, HexAdapter(Bytes(this._autn_len))))
|
||||
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/HexAdapter(Bytes(this._vsid_len)),
|
||||
'_vkid_len'/Int8ub, 'vk_id'/HexAdapter(Bytes(this._vkid_len)),
|
||||
'_vstk_rand_len'/Int8ub, 'vstk_rand'/HexAdapter(Bytes(this._vstk_rand_len)))
|
||||
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
|
||||
'_autn_len'/Int8ub, 'autn'/HexAdapter(Bytes(this._autn_len)))
|
||||
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/HexAdapter(Bytes(this._naf_id_len)),
|
||||
'_impi_len'/Int8ub, 'impi'/HexAdapter(Bytes(this._impi_len)))
|
||||
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
|
||||
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, Bytes(this._autn_len)))
|
||||
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/Bytes(this._vsid_len),
|
||||
'_vkid_len'/Int8ub, 'vk_id'/Bytes(this._vkid_len),
|
||||
'_vstk_rand_len'/Int8ub, 'vstk_rand'/Bytes(this._vstk_rand_len))
|
||||
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
|
||||
'_autn_len'/Int8ub, 'autn'/Bytes(this._autn_len))
|
||||
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/Bytes(this._naf_id_len),
|
||||
'_impi_len'/Int8ub, 'impi'/Bytes(this._impi_len))
|
||||
_cs_cmd_gba = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDD: 'bootstrap'/_cmd_gba_bs,
|
||||
0xDE: 'naf_derivation'/_cmd_gba_naf }))
|
||||
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/HexAdapter(Bytes(this._len_sres)),
|
||||
'_len_kc'/Int8ub, 'kc'/HexAdapter(Bytes(this._len_kc)))
|
||||
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/HexAdapter(Bytes(this._len_res)),
|
||||
'_len_ck'/Int8ub, 'ck'/HexAdapter(Bytes(this._len_ck)),
|
||||
'_len_ik'/Int8ub, 'ik'/HexAdapter(Bytes(this._len_ik)),
|
||||
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, HexAdapter(Bytes(this._len_kc))))
|
||||
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/HexAdapter(Bytes(this._len_auts)))
|
||||
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/Bytes(this._len_sres),
|
||||
'_len_kc'/Int8ub, 'kc'/Bytes(this._len_kc))
|
||||
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/Bytes(this._len_res),
|
||||
'_len_ck'/Int8ub, 'ck'/Bytes(this._len_ck),
|
||||
'_len_ik'/Int8ub, 'ik'/Bytes(this._len_ik),
|
||||
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, Bytes(this._len_kc)))
|
||||
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/Bytes(this._len_auts))
|
||||
_cs_rsp_3g = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDB: 'success'/_rsp_3g_ok,
|
||||
0xDC: 'sync_fail'/_rsp_3g_sync}))
|
||||
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/HexAdapter(Bytes(this._vstk_len)))
|
||||
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/HexAdapter(Bytes(this._ks_ext_naf_len)))
|
||||
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/Bytes(this._vstk_len))
|
||||
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/Bytes(this._ks_ext_naf_len))
|
||||
def _decode_cmd(self) -> Dict:
|
||||
r = {}
|
||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pySim.gsmtap import GsmtapMessage, GsmtapSource
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
from osmocom.gsmtap import GsmtapReceiver
|
||||
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
class GsmtapApduSource(ApduSource):
|
||||
"""ApduSource for handling GSMTAP-SIM messages received via UDP, such as
|
||||
@@ -38,19 +41,19 @@ class GsmtapApduSource(ApduSource):
|
||||
bind_port: UDP port number to which the socket should be bound (default: 4729)
|
||||
"""
|
||||
super().__init__()
|
||||
self.gsmtap = GsmtapSource(bind_ip, bind_port)
|
||||
self.gsmtap = GsmtapReceiver(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(gsmtap_msg['body'])
|
||||
elif sub_type in ['pps_req', 'pps_rsp']:
|
||||
if sub_type in ['pps_req', 'pps_rsp']:
|
||||
# simply ignore for now
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -16,21 +16,20 @@
|
||||
# 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 osmocom.gsmtap import GsmtapMessage
|
||||
|
||||
from pySim.utils import h2b, b2h
|
||||
from pySim.apdu import Tpdu
|
||||
from pySim.gsmtap import GsmtapMessage
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
from pySim.utils import h2b
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,10 +66,10 @@ class _PysharkGsmtap(ApduSource):
|
||||
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(gsmtap_msg['body'])
|
||||
elif sub_type in ['pps_req', 'pps_rsp']:
|
||||
if sub_type in ['pps_req', 'pps_rsp']:
|
||||
# simply ignore for now
|
||||
pass
|
||||
else:
|
||||
@@ -87,4 +86,3 @@ class PysharkGsmtapPcap(_PysharkGsmtap):
|
||||
"""
|
||||
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', use_json=True, keep_packets=False)
|
||||
super().__init__(pyshark_inst)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
48
pySim/apdu_source/tca_loader_log.py
Normal file
48
pySim/apdu_source/tca_loader_log.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# coding=utf-8
|
||||
|
||||
# (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.utils import h2b
|
||||
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
|
||||
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 + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
class TcaLoaderLogApduSource(ApduSource):
|
||||
"""ApduSource for reading log files created by TCALoader."""
|
||||
def __init__(self, filename:str):
|
||||
super().__init__()
|
||||
self.logfile = open(filename, 'r')
|
||||
|
||||
def read_packet(self) -> PacketType:
|
||||
command = None
|
||||
response = None
|
||||
for line in self.logfile:
|
||||
if line.startswith('Command'):
|
||||
command = line.split()[1]
|
||||
print("Command: '%s'" % command)
|
||||
pass
|
||||
elif command and line.startswith('Response'):
|
||||
response = line.split()[1]
|
||||
print("Response: '%s'" % response)
|
||||
return ApduCommands.parse_cmd_bytes(h2b(command) + h2b(response))
|
||||
raise StopIteration
|
||||
33
pySim/app.py
33
pySim/app.py
@@ -19,13 +19,13 @@ 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.exceptions import NoCardError
|
||||
from pySim.cards import card_detect, SimCardBase, UiccCardBase, CardBase
|
||||
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.profile.cdma_ruim import CardProfileRUIM
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.utils import all_subclasses
|
||||
from pySim.exceptions import SwMatchError
|
||||
|
||||
# we need to import this module so that the SysmocomSJA2 sub-class of
|
||||
# CardModel is created, which will add the ATR-based matching and
|
||||
@@ -40,9 +40,9 @@ import pySim.ts_31_103
|
||||
import pySim.ts_31_104
|
||||
import pySim.ara_m
|
||||
import pySim.global_platform
|
||||
import pySim.euicc
|
||||
import pySim.profile.euicc
|
||||
|
||||
def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
|
||||
def init_card(sl: LinkBase, skip_card_init: bool = False) -> 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
|
||||
@@ -57,6 +57,12 @@ def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
|
||||
print("Waiting for card...")
|
||||
sl.wait_for_card(3)
|
||||
|
||||
# The user may opt to skip all card initialization. In this case only the
|
||||
# most basic card profile is selected. This mode is suitable for blank
|
||||
# cards that need card O/S initialization using APDU scripts first.
|
||||
if skip_card_init:
|
||||
return None, CardBase(scc)
|
||||
|
||||
generic_card = False
|
||||
card = card_detect(scc)
|
||||
if card is None:
|
||||
@@ -107,7 +113,16 @@ def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
|
||||
# inform the transport that we can do context-specific SW interpretation
|
||||
sl.set_sw_interpreter(rs)
|
||||
|
||||
# try to obtain the EID, if any
|
||||
isd_r = rs.mf.applications.get(pySim.euicc.AID_ISD_R.lower(), None)
|
||||
if isd_r:
|
||||
rs.lchan[0].select_file(isd_r)
|
||||
try:
|
||||
rs.identity['EID'] = pySim.euicc.CardApplicationISDR.get_eid(scc)
|
||||
except SwMatchError:
|
||||
# has ISD-R but not a SGP.22/SGP.32 eUICC - maybe SGP.02?
|
||||
pass
|
||||
finally:
|
||||
rs.reset()
|
||||
|
||||
return rs, card
|
||||
|
||||
|
||||
|
||||
|
||||
172
pySim/ara_m.py
172
pySim/ara_m.py
@@ -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 osmocom.construct import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.utils import Hexstr
|
||||
from pySim.filesystem import *
|
||||
from pySim.tlv import *
|
||||
import pySim.global_platform
|
||||
|
||||
# various BER-TLV encoded Data Objects (DOs)
|
||||
|
||||
@@ -67,30 +69,28 @@ 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))
|
||||
self.decoded['apdu_filter'] = []
|
||||
self.decoded = {'apdu_filter': []}
|
||||
offset = 0
|
||||
while offset < len(do):
|
||||
self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
|
||||
'mask': b2h(do[offset+4:offset+8])}
|
||||
self.decoded = res
|
||||
return res
|
||||
self.decoded['apdu_filter'] += [{'header': b2h(do[offset:offset+4]),
|
||||
'mask': b2h(do[offset+4:offset+8])}]
|
||||
offset += 8 # Move offset to the beginning of the next apdu_filter object
|
||||
return self.decoded
|
||||
|
||||
def _to_bytes(self):
|
||||
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)))
|
||||
|
||||
|
||||
@@ -227,16 +228,14 @@ class BlockDO(BER_TLV_IE, tag=0xe7):
|
||||
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
|
||||
pass
|
||||
|
||||
|
||||
# SEID v1.1 Table 4-2
|
||||
|
||||
|
||||
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
|
||||
ResponseRefreshTagDO, ResponseAramConfigDO]):
|
||||
pass
|
||||
|
||||
|
||||
# SEID v1.1 Table 5-1
|
||||
|
||||
|
||||
class StoreCommandDoCollection(TLV_IE_Collection,
|
||||
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
|
||||
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
|
||||
@@ -259,8 +258,11 @@ 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'):
|
||||
def xceive_apdu_tlv(scc, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
|
||||
"""Transceive an APDU with the card, transparently encoding the command data from TLV
|
||||
and decoding the response data tlv."""
|
||||
if cmd_do:
|
||||
@@ -272,47 +274,43 @@ 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) = scc.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
|
||||
|
||||
@staticmethod
|
||||
def store_data(tp, do) -> bytes:
|
||||
def store_data(scc, do) -> bytes:
|
||||
"""Build the Command APDU for STORE DATA."""
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80e29000', do, StoreResponseDoCollection)
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80e29000', do, StoreResponseDoCollection)
|
||||
|
||||
@staticmethod
|
||||
def get_all(tp):
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
|
||||
def get_all(scc):
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80caff40', None, GetResponseDoCollection)
|
||||
|
||||
@staticmethod
|
||||
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
|
||||
def get_config(scc, v_major=0, v_minor=0, v_patch=1):
|
||||
cmd_do = DeviceConfigDO()
|
||||
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)
|
||||
cmd_do.from_val_dict([{'device_interface_version_do': {
|
||||
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '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.lchan.scc._tp)
|
||||
res_do = ADF_ARAM.get_all(self._cmd.lchan.scc)
|
||||
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.lchan.scc._tp)
|
||||
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
@@ -334,7 +332,7 @@ class ADF_ARAM(CardADF):
|
||||
apdu_grp.add_argument(
|
||||
'--apdu-always', action='store_true', help='APDU access is allowed')
|
||||
apdu_grp.add_argument(
|
||||
'--apdu-filter', help='APDU filter: 4 byte CLA/INS/P1/P2 followed by 4 byte mask (8 hex bytes)')
|
||||
'--apdu-filter', help='APDU filter: multiple groups of 8 hex bytes (4 byte CLA/INS/P1/P2 followed by 4 byte mask)')
|
||||
nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
nfc_grp.add_argument('--nfc-always', action='store_true',
|
||||
help='NFC event access is allowed')
|
||||
@@ -348,7 +346,7 @@ 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:
|
||||
if opts.aid is not None:
|
||||
ref_do_content += [{'aid_ref_do': opts.aid}]
|
||||
elif opts.aid_empty:
|
||||
ref_do_content += [{'aid_ref_empty_do': None}]
|
||||
@@ -358,12 +356,19 @@ class ADF_ARAM(CardADF):
|
||||
# AR
|
||||
ar_do_content = []
|
||||
if opts.apdu_never:
|
||||
ar_do_content += [{'apdu_ar_od': {'generic_access_rule': 'never'}}]
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
|
||||
elif opts.apdu_always:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
||||
elif opts.apdu_filter:
|
||||
# TODO: multiple filters
|
||||
ar_do_content += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
|
||||
if len(opts.apdu_filter) % 16:
|
||||
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
|
||||
offset = 0
|
||||
apdu_filter = []
|
||||
while offset < len(opts.apdu_filter):
|
||||
apdu_filter += [{'header': opts.apdu_filter[offset:offset+8],
|
||||
'mask': opts.apdu_filter[offset+8:offset+16]}]
|
||||
offset += 16 # Move offset to the beginning of the next apdu_filter object
|
||||
ar_do_content += [{'apdu_ar_do': {'apdu_filter': apdu_filter}}]
|
||||
if opts.nfc_always:
|
||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
|
||||
elif opts.nfc_never:
|
||||
@@ -372,15 +377,15 @@ class ADF_ARAM(CardADF):
|
||||
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.lchan.scc._tp, csrado)
|
||||
csrado.from_val_dict(d)
|
||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, 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.lchan.scc._tp, deldo)
|
||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, deldo)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
@@ -410,3 +415,78 @@ sw_aram = {
|
||||
class CardApplicationARAM(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
|
||||
|
||||
@staticmethod
|
||||
def __export_get_from_dictlist(key, dictlist):
|
||||
# Data objects are organized in lists that contain dictionaries, usually there is only one dictionary per
|
||||
# list item. This function goes through that list and gets the value of the first dictionary that has the
|
||||
# matching key.
|
||||
if dictlist is None:
|
||||
return None
|
||||
obj = None
|
||||
for d in dictlist:
|
||||
obj = d.get(key, obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def __export_ref_ar_do_list(ref_ar_do_list):
|
||||
export_str = ""
|
||||
ref_do_list = CardApplicationARAM.__export_get_from_dictlist('ref_do', ref_ar_do_list.get('ref_ar_do'))
|
||||
ar_do_list = CardApplicationARAM.__export_get_from_dictlist('ar_do', ref_ar_do_list.get('ref_ar_do'))
|
||||
|
||||
if ref_do_list and ar_do_list:
|
||||
# Get ref_do parameters
|
||||
aid_ref_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_do', ref_do_list)
|
||||
dev_app_id_ref_do = CardApplicationARAM.__export_get_from_dictlist('dev_app_id_ref_do', ref_do_list)
|
||||
pkg_ref_do = CardApplicationARAM.__export_get_from_dictlist('pkg_ref_do', ref_do_list)
|
||||
|
||||
# Get ar_do parameters
|
||||
apdu_ar_do = CardApplicationARAM.__export_get_from_dictlist('apdu_ar_do', ar_do_list)
|
||||
nfc_ar_do = CardApplicationARAM.__export_get_from_dictlist('nfc_ar_do', ar_do_list)
|
||||
perm_ar_do = CardApplicationARAM.__export_get_from_dictlist('perm_ar_do', ar_do_list)
|
||||
|
||||
# Write command-line
|
||||
export_str += "aram_store_ref_ar_do"
|
||||
if aid_ref_do:
|
||||
export_str += (" --aid %s" % aid_ref_do)
|
||||
else:
|
||||
export_str += " --aid-empty"
|
||||
if dev_app_id_ref_do:
|
||||
export_str += (" --device-app-id %s" % dev_app_id_ref_do)
|
||||
if apdu_ar_do and 'generic_access_rule' in apdu_ar_do:
|
||||
export_str += (" --apdu-%s" % apdu_ar_do['generic_access_rule'])
|
||||
elif apdu_ar_do and 'apdu_filter' in apdu_ar_do:
|
||||
export_str += (" --apdu-filter ")
|
||||
for apdu_filter in apdu_ar_do['apdu_filter']:
|
||||
export_str += apdu_filter['header']
|
||||
export_str += apdu_filter['mask']
|
||||
if nfc_ar_do and 'nfc_event_access_rule' in nfc_ar_do:
|
||||
export_str += (" --nfc-%s" % nfc_ar_do['nfc_event_access_rule'])
|
||||
if perm_ar_do:
|
||||
export_str += (" --android-permissions %s" % perm_ar_do['permissions'])
|
||||
if pkg_ref_do:
|
||||
export_str += (" --pkg-ref %s" % pkg_ref_do['package_name_string'])
|
||||
export_str += "\n"
|
||||
return export_str
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
|
||||
# TODO: Add JSON output as soon as aram_store_ref_ar_do is able to process input in JSON format.
|
||||
if as_json:
|
||||
raise NotImplementedError("res_do encoder not yet implemented. Patches welcome.")
|
||||
|
||||
export_str = ""
|
||||
export_str += "aram_delete_all\n"
|
||||
|
||||
res_do = ADF_ARAM.get_all(lchan.scc)
|
||||
if not res_do:
|
||||
return export_str.strip()
|
||||
|
||||
for res_do_dict in res_do.to_dict():
|
||||
if not res_do_dict.get('response_all_ref_ar_do', False):
|
||||
continue
|
||||
for ref_ar_do_list in res_do_dict['response_all_ref_ar_do']:
|
||||
export_str += CardApplicationARAM.__export_ref_ar_do_list(ref_ar_do_list)
|
||||
|
||||
return export_str.strip()
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -10,10 +10,10 @@ the need of manually entering the related card-individual data on every
|
||||
operation with pySim-shell.
|
||||
"""
|
||||
|
||||
# (C) 2021 by Sysmocom s.f.m.c. GmbH
|
||||
# (C) 2021-2024 by Sysmocom s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier
|
||||
# Author: Philipp Maier, Harald Welte
|
||||
#
|
||||
# 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
|
||||
@@ -29,18 +29,29 @@ operation with pySim-shell.
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
from Cryptodome.Cipher import AES
|
||||
from osmocom.utils import h2b, b2h
|
||||
|
||||
import abc
|
||||
import csv
|
||||
|
||||
card_key_providers = [] # type: List['CardKeyProvider']
|
||||
|
||||
# well-known groups of columns relate to a given functionality. This avoids having
|
||||
# to specify the same transport key N number of times, if the same key is used for multiple
|
||||
# fields of one group, like KIC+KID+KID of one SD.
|
||||
CRYPT_GROUPS = {
|
||||
'UICC_SCP02': ['UICC_SCP02_KIC1', 'UICC_SCP02_KID1', 'UICC_SCP02_KIK1'],
|
||||
'UICC_SCP03': ['UICC_SCP03_KIC1', 'UICC_SCP03_KID1', 'UICC_SCP03_KIK1'],
|
||||
'SCP03_ISDR': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDR', 'SCP03_DEK_ISDR'],
|
||||
'SCP03_ISDA': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDA', 'SCP03_DEK_ISDA'],
|
||||
'SCP03_ECASD': ['SCP03_ENC_ECASD', 'SCP03_MAC_ECASD', 'SCP03_DEK_ECASD'],
|
||||
}
|
||||
|
||||
class CardKeyProvider(abc.ABC):
|
||||
"""Base class, not containing any concrete implementation."""
|
||||
|
||||
VALID_FIELD_NAMES = ['ICCID', 'ADM1',
|
||||
'IMSI', 'PIN1', 'PIN2', 'PUK1', 'PUK2']
|
||||
VALID_KEY_FIELD_NAMES = ['ICCID', 'EID', 'IMSI' ]
|
||||
|
||||
# check input parameters, but do nothing concrete yet
|
||||
def _verify_get_data(self, fields: List[str] = [], key: str = 'ICCID', value: str = "") -> Dict[str, str]:
|
||||
@@ -53,14 +64,10 @@ class CardKeyProvider(abc.ABC):
|
||||
Returns:
|
||||
dictionary of {field, value} strings for each requested field from 'fields'
|
||||
"""
|
||||
for f in fields:
|
||||
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_KEY_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)))
|
||||
(key, str(self.VALID_KEY_FIELD_NAMES)))
|
||||
|
||||
return {}
|
||||
|
||||
@@ -84,19 +91,47 @@ class CardKeyProvider(abc.ABC):
|
||||
|
||||
|
||||
class CardKeyProviderCsv(CardKeyProvider):
|
||||
"""Card key provider implementation that allows to query against a specified CSV file"""
|
||||
"""Card key provider implementation that allows to query against a specified CSV file.
|
||||
Supports column-based encryption as it is generally a bad idea to store cryptographic key material in
|
||||
plaintext. Instead, the key material should be encrypted by a "key-encryption key", occasionally also
|
||||
known as "transport key" (see GSMA FS.28)."""
|
||||
IV = b'\x23' * 16
|
||||
csv_file = None
|
||||
filename = None
|
||||
|
||||
def __init__(self, filename: str):
|
||||
def __init__(self, filename: str, transport_keys: dict):
|
||||
"""
|
||||
Args:
|
||||
filename : file name (path) of CSV file containing card-individual key/data
|
||||
transport_keys : a dict indexed by field name, whose values are hex-encoded AES keys for the
|
||||
respective field (column) of the CSV. This is done so that different fields
|
||||
(columns) can use different transport keys, which is strongly recommended by
|
||||
GSMA FS.28
|
||||
"""
|
||||
self.csv_file = open(filename, 'r')
|
||||
if not self.csv_file:
|
||||
raise RuntimeError("Could not open CSV file '%s'" % filename)
|
||||
self.filename = filename
|
||||
self.transport_keys = self.process_transport_keys(transport_keys)
|
||||
|
||||
@staticmethod
|
||||
def process_transport_keys(transport_keys: dict):
|
||||
"""Apply a single transport key to multiple fields/columns, if the name is a group."""
|
||||
new_dict = {}
|
||||
for name, key in transport_keys.items():
|
||||
if name in CRYPT_GROUPS:
|
||||
for field in CRYPT_GROUPS[name]:
|
||||
new_dict[field] = key
|
||||
else:
|
||||
new_dict[name] = key
|
||||
return new_dict
|
||||
|
||||
def _decrypt_field(self, field_name: str, encrypted_val: str) -> str:
|
||||
"""decrypt a single field, if we have a transport key for the field of that name."""
|
||||
if not field_name in self.transport_keys:
|
||||
return encrypted_val
|
||||
cipher = AES.new(h2b(self.transport_keys[field_name]), AES.MODE_CBC, self.IV)
|
||||
return b2h(cipher.decrypt(h2b(encrypted_val)))
|
||||
|
||||
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
|
||||
super()._verify_get_data(fields, key, value)
|
||||
@@ -113,7 +148,7 @@ class CardKeyProviderCsv(CardKeyProvider):
|
||||
if row[key] == value:
|
||||
for f in fields:
|
||||
if f in row:
|
||||
rc.update({f: row[f]})
|
||||
rc.update({f: self._decrypt_field(f, row[f])})
|
||||
else:
|
||||
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
|
||||
(self.filename, f))
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import Optional, Dict, Tuple
|
||||
from pySim.ts_102_221 import EF_DIR
|
||||
from pySim.ts_51_011 import DF_GSM
|
||||
import abc
|
||||
from typing import Optional, Tuple
|
||||
from osmocom.utils import *
|
||||
|
||||
from pySim.utils import *
|
||||
from pySim.profile.ts_102_221 import EF_DIR, CardProfileUICC
|
||||
from pySim.profile.ts_51_011 import DF_GSM
|
||||
from pySim.utils import SwHexstr
|
||||
from pySim.commands import Path, SimCardCommands
|
||||
|
||||
class CardBase:
|
||||
@@ -40,8 +40,7 @@ class CardBase:
|
||||
rc = self._scc.reset_card()
|
||||
if rc == 1:
|
||||
return self._scc.get_atr()
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
|
||||
"""Set apdu parameters (class byte and selection control bytes)"""
|
||||
@@ -54,13 +53,19 @@ class CardBase:
|
||||
|
||||
def erase(self):
|
||||
print("warning: erasing is not supported for specified card type!")
|
||||
return
|
||||
|
||||
def file_exists(self, fid: Path) -> bool:
|
||||
"""Determine if the file exists (and is not deactivated)."""
|
||||
res_arr = self._scc.try_select_path(fid)
|
||||
for res in res_arr:
|
||||
if res[1] != '9000':
|
||||
return False
|
||||
try:
|
||||
d = CardProfileUICC.decode_select_response(res_arr[-1][0])
|
||||
if d.get('life_cycle_status_integer', 'operational_activated') != 'operational_activated':
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
return True
|
||||
|
||||
def read_aids(self) -> List[Hexstr]:
|
||||
@@ -75,7 +80,7 @@ class SimCardBase(CardBase):
|
||||
name = 'SIM'
|
||||
|
||||
def __init__(self, scc: SimCardCommands):
|
||||
super(SimCardBase, self).__init__(scc)
|
||||
super().__init__(scc)
|
||||
self._scc.cla_byte = "A0"
|
||||
self._scc.sel_ctrl = "0000"
|
||||
|
||||
@@ -88,7 +93,7 @@ class UiccCardBase(SimCardBase):
|
||||
name = 'UICC'
|
||||
|
||||
def __init__(self, scc: SimCardCommands):
|
||||
super(UiccCardBase, self).__init__(scc)
|
||||
super().__init__(scc)
|
||||
self._scc.cla_byte = "00"
|
||||
self._scc.sel_ctrl = "0004" # request an FCP
|
||||
# See also: ETSI TS 102 221, Table 9.3
|
||||
@@ -158,9 +163,8 @@ class UiccCardBase(SimCardBase):
|
||||
aid_full = self._complete_aid(aid)
|
||||
if aid_full:
|
||||
return scc.select_adf(aid_full)
|
||||
else:
|
||||
# If we cannot get the full AID, try with short AID
|
||||
return scc.select_adf(aid)
|
||||
# If we cannot get the full AID, try with short AID
|
||||
return scc.select_adf(aid)
|
||||
return (None, None)
|
||||
|
||||
def card_detect(scc: SimCardCommands) -> Optional[CardBase]:
|
||||
|
||||
344
pySim/cat.py
344
pySim/cat.py
@@ -18,43 +18,58 @@ 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 PlmnAdapter, 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 osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
||||
from osmocom.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi, GsmString
|
||||
from osmocom.utils import b2h
|
||||
from pySim.utils import dec_xplmn_w_act
|
||||
|
||||
# Tag values as per TS 101 220 Table 7.23
|
||||
|
||||
# TS 102 223 Section 8.1
|
||||
class Address(COMPR_TLV_IE, tag=0x06):
|
||||
class Address(COMPR_TLV_IE, tag=0x86):
|
||||
_construct = Struct('ton_npi'/Int8ub,
|
||||
'call_number'/BcdAdapter(Bytes(this._.total_len-1)))
|
||||
'call_number'/BcdAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.2
|
||||
class AlphaIdentifier(COMPR_TLV_IE, tag=0x05):
|
||||
class AlphaIdentifier(COMPR_TLV_IE, tag=0x85):
|
||||
# FIXME: like EF.ADN
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.3
|
||||
class Subaddress(COMPR_TLV_IE, tag=0x08):
|
||||
class Subaddress(COMPR_TLV_IE, tag=0x88):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.4 + TS 31.111 Section 8.4
|
||||
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x07):
|
||||
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x87):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 8.5
|
||||
class CBSPage(COMPR_TLV_IE, tag=0x0C):
|
||||
class CBSPage(COMPR_TLV_IE, tag=0x8C):
|
||||
pass
|
||||
|
||||
# TS 102 223 V15.3.0 Section 9.4
|
||||
TypeOfCommand = Enum(Int8ub, refresh=0x01, more_time=0x02, poll_interval=0x03, polling_off=0x04,
|
||||
set_up_event_list=0x05, set_up_call=0x10, send_ss=0x11, send_ussd=0x12,
|
||||
send_short_message=0x13, send_dtmf=0x14, launch_browser=0x15, geo_location_req=0x16,
|
||||
play_tone=0x20, display_text=0x21, get_inkey=0x22, get_input=0x23, select_item=0x24,
|
||||
set_up_menu=0x25, provide_local_info=0x26, timer_management=0x27,
|
||||
set_up_idle_mode_text=0x28, perform_card_apdu=0x30, power_on_card=0x31,
|
||||
power_off_card=0x32, get_reader_status=0x33, run_at_command=0x34,
|
||||
language_notification=0x35, open_channel=0x40, close_channel=0x41, receive_data=0x42,
|
||||
send_data=0x43, get_channel_status=0x44, service_search=0x45, get_service_info=0x46,
|
||||
declare_service=0x47, set_frames=0x50, get_frames_status=0x51, retrieve_mms=0x60,
|
||||
submit_mms=0x61, display_mms=0x62, activate=0x70, contactless_state_changed=0x71,
|
||||
command_container=0x72, encapsulated_session_control=0x73)
|
||||
|
||||
# TS 102 223 Section 8.6 + TS 31.111 Section 8.6
|
||||
class CommandDetails(COMPR_TLV_IE, tag=0x81):
|
||||
_construct = Struct('command_number'/Int8ub,
|
||||
'type_of_command'/Int8ub,
|
||||
'type_of_command'/TypeOfCommand,
|
||||
'command_qualifier'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.7
|
||||
@@ -107,26 +122,26 @@ class DeviceIdentities(COMPR_TLV_IE, tag=0x82):
|
||||
return bytes([src, dst])
|
||||
|
||||
# TS 102 223 Section 8.8
|
||||
class Duration(COMPR_TLV_IE, tag=0x04):
|
||||
class Duration(COMPR_TLV_IE, tag=0x84):
|
||||
_construct = Struct('time_unit'/Enum(Int8ub, minutes=0, seconds=1, tenths_of_seconds=2),
|
||||
'time_interval'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.9
|
||||
class Item(COMPR_TLV_IE, tag=0x0f):
|
||||
class Item(COMPR_TLV_IE, tag=0x8f):
|
||||
_construct = Struct('identifier'/Int8ub,
|
||||
'text_string'/GsmStringAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.10
|
||||
class ItemIdentifier(COMPR_TLV_IE, tag=0x10):
|
||||
class ItemIdentifier(COMPR_TLV_IE, tag=0x90):
|
||||
_construct = Struct('identifier'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.11
|
||||
class ResponseLength(COMPR_TLV_IE, tag=0x11):
|
||||
class ResponseLength(COMPR_TLV_IE, tag=0x91):
|
||||
_construct = Struct('minimum_length'/Int8ub,
|
||||
'maximum_length'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.12
|
||||
class Result(COMPR_TLV_IE, tag=0x03):
|
||||
class Result(COMPR_TLV_IE, tag=0x83):
|
||||
GeneralResult = Enum(Int8ub,
|
||||
# '0X' and '1X' indicate that the command has been performed
|
||||
performed_successfully=0,
|
||||
@@ -252,12 +267,15 @@ class SsString(COMPR_TLV_IE, tag=0x89):
|
||||
|
||||
|
||||
# TS 102 223 Section 8.15
|
||||
class TextString(COMPR_TLV_IE, tag=0x0d):
|
||||
class TextString(COMPR_TLV_IE, tag=0x8D):
|
||||
_test_de_encode = [
|
||||
( '8d090470617373776f7264', {'dcs': 4, 'text_string': '70617373776f7264'} ),
|
||||
]
|
||||
_construct = Struct('dcs'/Int8ub, # TS 03.38
|
||||
'text_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.16
|
||||
class Tone(COMPR_TLV_IE, tag=0x0e):
|
||||
class Tone(COMPR_TLV_IE, tag=0x8E):
|
||||
_construct = Struct('tone'/Enum(Int8ub, dial_tone=0x01,
|
||||
called_subscriber_busy=0x02,
|
||||
congestion=0x03,
|
||||
@@ -288,12 +306,12 @@ class Tone(COMPR_TLV_IE, tag=0x0e):
|
||||
melody_8=0x47))
|
||||
|
||||
# TS 31 111 Section 8.17
|
||||
class USSDString(COMPR_TLV_IE, tag=0x0a):
|
||||
class USSDString(COMPR_TLV_IE, tag=0x8A):
|
||||
_construct = Struct('dcs'/Int8ub,
|
||||
'ussd_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.18
|
||||
class FileList(COMPR_TLV_IE, tag=0x12):
|
||||
class FileList(COMPR_TLV_IE, tag=0x92):
|
||||
FileId=HexAdapter(Bytes(2))
|
||||
_construct = Struct('number_of_files'/Int8ub,
|
||||
'files'/GreedyRange(FileId))
|
||||
@@ -320,7 +338,7 @@ class DefaultText(COMPR_TLV_IE, tag=0x97):
|
||||
'text_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.24
|
||||
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x18):
|
||||
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x98):
|
||||
_construct = GreedyRange(Int8ub)
|
||||
|
||||
class EventList(COMPR_TLV_IE, tag=0x99):
|
||||
@@ -365,7 +383,7 @@ class LocationStatus(COMPR_TLV_IE, tag=0x9b):
|
||||
_construct = Enum(Int8ub, normal_service=0, limited_service=1, no_service=2)
|
||||
|
||||
# TS 102 223 Section 8.31
|
||||
class IconIdentifier(COMPR_TLV_IE, tag=0x1e):
|
||||
class IconIdentifier(COMPR_TLV_IE, tag=0x9e):
|
||||
_construct = Struct('icon_qualifier'/FlagsEnum(Int8ub, not_self_explanatory=1),
|
||||
'icon_identifier'/Int8ub)
|
||||
|
||||
@@ -391,25 +409,48 @@ class AtCommand(COMPR_TLV_IE, tag=0xA8):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.43
|
||||
class ImmediateResponse(COMPR_TLV_IE, tag=0x2b):
|
||||
class ImmediateResponse(COMPR_TLV_IE, tag=0xAB):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.44
|
||||
class DtmfString(COMPR_TLV_IE, tag=0xAC):
|
||||
_construct = BcdAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.45
|
||||
class Language(COMPR_TLV_IE, tag=0xAD):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 31.111 Section 8.46
|
||||
class TimingAdvance(COMPR_TLV_IE, tag=0x46):
|
||||
class TimingAdvance(COMPR_TLV_IE, tag=0xC6):
|
||||
_construct = Struct('me_status'/Enum(Int8ub, in_idle_state=0, not_in_idle_state=1),
|
||||
'timing_advance'/Int8ub)
|
||||
|
||||
# TS 31.111 Section 8.47
|
||||
class BrowserIdentity(COMPR_TLV_IE, tag=0xB0):
|
||||
_construct = Enum(Int8ub, default=0, wml=1, html=2, xhtml=3, chtml=4)
|
||||
|
||||
# TS 31.111 Section 8.48
|
||||
class Url(COMPR_TLV_IE, tag=0xB1):
|
||||
_construct = GsmString(GreedyBytes)
|
||||
|
||||
# TS 31.111 Section 8.49
|
||||
class Bearer(COMPR_TLV_IE, tag=0xB2):
|
||||
SingleBearer = Enum(Int8ub, sms=0, csd=1, ussd=2, packet_Service=3)
|
||||
_construct = GreedyRange(SingleBearer)
|
||||
|
||||
# TS 102 223 Section 8.50
|
||||
class ProvisioningFileReference(COMPR_TLV_IE, tag=0xB3):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.51
|
||||
class BrowserTerminationCause(COMPR_TLV_IE, tag=0xB4):
|
||||
_construct = Enum(Int8ub, user_termination=0, error_termination=1)
|
||||
|
||||
# TS 102 223 Section 8.52
|
||||
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
||||
_test_de_encode = [
|
||||
( 'b50103', {'bearer_parameters': '', 'bearer_type': 'default'} ),
|
||||
]
|
||||
# TS 31.111 Section 8.52.1
|
||||
BearerParsCs = Struct('data_rate'/Int8ub,
|
||||
'bearer_service'/Int8ub,
|
||||
@@ -465,25 +506,32 @@ class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
|
||||
class BufferSize(COMPR_TLV_IE, tag = 0xB9):
|
||||
_construct = Int16ub
|
||||
|
||||
# TS 31.111 Section 8.56
|
||||
# TS 102 223 Section 8.56 + TS 31.111 Section 8.56
|
||||
class ChannelStatus(COMPR_TLV_IE, tag = 0xB8):
|
||||
# complex decoding, depends on out-of-band context/knowledge :(
|
||||
pass
|
||||
# for default / TCP Client mode: bit 8 of first byte indicates connected, 3 LSB indicate channel nr
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.58
|
||||
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
|
||||
_test_de_encode = [
|
||||
( 'be052101020304', {'address': '01020304', 'type_of_address': 'ipv4'} ),
|
||||
]
|
||||
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
|
||||
'address'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.59
|
||||
class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
|
||||
_test_de_encode = [
|
||||
( 'bc03028000', {'port_number': 32768, 'protocol_type': 'tcp_uicc_client_remote'} ),
|
||||
]
|
||||
_construct = Struct('protocol_type'/Enum(Int8ub, udp_uicc_client_remote=1, tcp_uicc_client_remote=2,
|
||||
tcp_uicc_server=3, udp_uicc_client_local=4,
|
||||
tcp_uicc_client_local=5, direct_channel=6),
|
||||
'port_number'/Int16ub)
|
||||
|
||||
# TS 102 223 Section 8.60
|
||||
class Aid(COMPR_TLV_IE, tag=0x2f):
|
||||
class Aid(COMPR_TLV_IE, tag=0xAF):
|
||||
_construct = Struct('aid'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.61
|
||||
@@ -523,10 +571,13 @@ class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
|
||||
|
||||
# TS 102 223 Section 8.70
|
||||
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
|
||||
_test_de_encode = [
|
||||
( 'c704036e6161', '036e6161' ),
|
||||
]
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.72
|
||||
class TextAttribute(COMPR_TLV_IE, tag=0x50):
|
||||
class TextAttribute(COMPR_TLV_IE, tag=0xD0):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 8.72
|
||||
@@ -562,7 +613,7 @@ class ItemTextAttributeList(COMPR_TLV_IE, tag=0xD1):
|
||||
_construct = GreedyRange(Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.80
|
||||
class FrameIdentifier(COMPR_TLV_IE, tag=0x68):
|
||||
class FrameIdentifier(COMPR_TLV_IE, tag=0xE8):
|
||||
_construct = Struct('identifier'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.82
|
||||
@@ -583,11 +634,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
|
||||
|
||||
@@ -669,7 +720,7 @@ class SaTemplate(COMPR_TLV_IE, tag=0xA3):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.103
|
||||
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0x3A):
|
||||
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0xBA):
|
||||
_construct = FlagsEnum(Byte, even_if_navigating_menus=0, even_if_data_call=1, even_if_voice_call=2)
|
||||
|
||||
# TS 102 223 Section 8.104
|
||||
@@ -683,7 +734,7 @@ class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||
_construct = GreedyRange(AccessTechTuple)
|
||||
|
||||
# TS 102 223 Section 8.107
|
||||
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0x3B):
|
||||
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0xBB):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 8.108
|
||||
@@ -717,19 +768,194 @@ class SMSCBDownload(BER_TLV_IE, tag=0xD2,
|
||||
nested=[DeviceIdentities, CBSPage]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class MenuSelection(BER_TLV_IE, tag=0xD3,
|
||||
nested=[DeviceIdentities, ItemIdentifier, HelpRequest]):
|
||||
pass
|
||||
|
||||
class BcRepeatIndicator(BER_TLV_IE, tag=0x2A):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class CallControl(BER_TLV_IE, tag=0xD4,
|
||||
nested=[DeviceIdentities, Address, CapabilityConfigParams, Subaddress,
|
||||
LocationInformation, BcRepeatIndicator]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class MoShortMessageControl(BER_TLV_IE, tag=0xD5):
|
||||
pass
|
||||
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class TransactionIdentifier(BER_TLV_IE, tag=0x1C):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImsURI(BER_TLV_IE, tag=0x31):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class UriTruncated(BER_TLV_IE, tag=0x73):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class TrackingAreaIdentification(BER_TLV_IE, tag=0x7D):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ExtendedRejectionCauseCode(BER_TLV_IE, tag=0x57):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class CsgCellSelectionStatus(BER_TLV_IE, tag=0x55):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class CsgId(BER_TLV_IE, tag=0x56):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class HnbName(BER_TLV_IE, tag=0x57):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class PlmnId(BER_TLV_IE, tag=0x09):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImsCallDisconnectionStatus(BER_TLV_IE, tag=0x55):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class Iari(BER_TLV_IE, tag=0x76):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImpuList(BER_TLV_IE, tag=0x77):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImsStatusCode(BER_TLV_IE, tag=0x77):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class DateTimeAndTimezone(BER_TLV_IE, tag=0x26):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class PdpPdnPduType(BER_TLV_IE, tag=0x0B):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class GadShape(BER_TLV_IE, tag=0x77):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class NmeaSentence(BER_TLV_IE, tag=0x78):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class WlanAccessStatus(BER_TLV_IE, tag=0x4B):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class EventDownload(BER_TLV_IE, tag=0xD6,
|
||||
nested=[EventList, DeviceIdentities,
|
||||
# 7.5.1.2 (I-)WLAN Access Status
|
||||
WlanAccessStatus,
|
||||
# 7.5.1A.2 MT Call
|
||||
TransactionIdentifier, Address,
|
||||
Subaddress, ImsURI, MediaType, UriTruncated,
|
||||
# 7.5.2.2 Network Rejection
|
||||
LocationInformation, RoutingAreaIdentification, TrackingAreaIdentification,
|
||||
AccessTechnology, UpdateAttachRegistrationType, RejectionCauseCode,
|
||||
ExtendedRejectionCauseCode,
|
||||
# 7.5.2A.2 Call Connected
|
||||
# TransactionIdentifier, MediaType
|
||||
# 7.5.3.2 CSG Cell Selection
|
||||
# AccessTechnology
|
||||
CsgCellSelectionStatus, CsgId, HnbName, PlmnId,
|
||||
# 7.5.3A.2 CAll Disconnected
|
||||
# TransactionIdentifier, MediaType,
|
||||
ImsCallDisconnectionStatus,
|
||||
# TS 102 223 7.5.4 LocationStatusEvent
|
||||
# TS 102 223 7.5.5 UserActivityEvent
|
||||
# TS 102 223 7.5.6 IdleScreenAvailableEvent
|
||||
# TS 102 223 7.5.7 CardReaderStatusEvent
|
||||
# TS 102 223 7.5.8 LanguageSelectionEvent
|
||||
# TS 102 223 7.5.9 BrowserTerminationEvent
|
||||
# TS 102 223 7.5.10 DataAvailableEvent
|
||||
ChannelStatus, ChannelDataLength,
|
||||
# TS 102 223 7.5.11 ChannelStatusEvent
|
||||
# TS 102 223 7.5.12 AccessTechnologyChangeEvent
|
||||
# TS 102 223 7.5.13 DisplayParametersChangedEvent
|
||||
# TS 102 223 7.5.14 LocalConnectionEvent
|
||||
# TS 102 223 7.5.15 NetworkSearchModeChangeEvent
|
||||
# TS 102 223 7.5.16 BrowsingStatusEvent
|
||||
# TS 102 223 7.5.17 FramesInformationChangedEvent
|
||||
# 7.5.20 Incoming IMS Data
|
||||
Iari,
|
||||
# 7.5.21 MS Registration Event
|
||||
ImpuList, ImsStatusCode,
|
||||
# 7.5.24 / TS 102 223 7.5.22 PollIntervalNegotiation
|
||||
# 7.5.25 DataConnectionStatusChangeEvent
|
||||
DataConnectionStatus, DataConnectionType, SmCause,
|
||||
# TransactionIdentifier, LocationInformation, AccessTechnology
|
||||
DateTimeAndTimezone, LocationStatus, NetworkAccessName, PdpPdnPduType,
|
||||
# 7.7 / TS 102 223 7.6 MMS Transfer Status
|
||||
# 7.8 / TS 102 223 MMS Notification Download
|
||||
# 7.9 / TS 102 223 8.8 Terminal Applications
|
||||
]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class TimerExpiration(BER_TLV_IE, tag=0xD7):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + TS 31.111 7.6.2
|
||||
class USSDDownload(BER_TLV_IE, tag=0xD9,
|
||||
nested=[DeviceIdentities, USSDString]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + TS 102 223 7.6
|
||||
class MmsTransferStatus(BER_TLV_IE, tag=0xDA):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223
|
||||
class MmsNotificationDownload(BER_TLV_IE, tag=0xDB):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223 7.8
|
||||
class TerminalApplication(BER_TLV_IE, tag=0xDC):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + TS 31.111 7.10.2
|
||||
class GeographicalLocation(BER_TLV_IE, tag=0xDD,
|
||||
nested=[DeviceIdentities, GadShape, NmeaSentence]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class EnvelopeContainer(BER_TLV_IE, tag=0xDE):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class ProSeReport(BER_TLV_IE, tag=0xDF):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class ProactiveCmd(BER_TLV_IE):
|
||||
def _compute_tag(self) -> int:
|
||||
return 0xD0
|
||||
|
||||
|
||||
class EventCollection(TLV_IE_Collection,
|
||||
nested=[SMSPPDownload, SMSCBDownload,
|
||||
EventDownload, CallControl, MoShortMessageControl,
|
||||
USSDDownload, GeographicalLocation, ProSeReport]):
|
||||
pass
|
||||
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223 6.6.13/9.4 + TS 31.111 6.6.13
|
||||
class Refresh(ProactiveCmd, tag=0x01,
|
||||
nested=[CommandDetails, DeviceIdentities, FileList, Aid, AlphaIdentifier,
|
||||
@@ -737,20 +963,24 @@ class Refresh(ProactiveCmd, tag=0x01,
|
||||
ApplicationSpecificRefreshData, PlmnWactList, PlmnList]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.4
|
||||
class MoreTime(ProactiveCmd, tag=0x02,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.5
|
||||
class PollInterval(ProactiveCmd, tag=0x03,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities, Duration]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.14
|
||||
class PollingOff(ProactiveCmd, tag=0x04,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.16
|
||||
class SetUpEventList(ProactiveCmd, tag=0x05,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities, EventList]):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 6.6.12
|
||||
@@ -778,20 +1008,27 @@ class SendShortMessage(ProactiveCmd, tag=0x13,
|
||||
SMS_TPDU, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||
pass
|
||||
|
||||
# TS 102 223 6.6.24
|
||||
class SendDTMF(ProactiveCmd, tag=0x14,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
|
||||
DtmfString, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||
pass
|
||||
|
||||
# TS 102 223 6.6.26
|
||||
class LaunchBrowser(ProactiveCmd, tag=0x15,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities, BrowserIdentity, Url, Bearer, ProvisioningFileReference,
|
||||
TextString, AlphaIdentifier, IconIdentifier, TextAttribute, FrameIdentifier,
|
||||
NetworkAccessName]):
|
||||
pass
|
||||
|
||||
class GeographicalLocationRequest(ProactiveCmd, tag=0x16,
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 102 223 6.6.5
|
||||
class PlayTone(ProactiveCmd, tag=0x20,
|
||||
nested=[CommandDetails]):
|
||||
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
|
||||
Tone, Duration, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223 6.6.1/9.4 CMD=0x21
|
||||
@@ -978,8 +1215,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,17 +1234,17 @@ 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)
|
||||
cmd_details = pcmd.find_cmd_details()
|
||||
# then do a second decode stage for the specific
|
||||
cmd_type = cmd_details.decoded['type_of_command']
|
||||
cmd_type = TypeOfCommand.encmapping[cmd_details.decoded['type_of_command']]
|
||||
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,9 +1256,18 @@ 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()
|
||||
|
||||
# TS 101 223 Section 6.8.0
|
||||
class TerminalResponse(TLV_IE_Collection,
|
||||
nested=[CommandDetails, DeviceIdentities, Result,
|
||||
Duration, TextString, ItemIdentifier,
|
||||
#TODO: LocalInformation and other optional/conditional IEs
|
||||
ChannelData, ChannelDataLength,
|
||||
ChannelStatus, BufferSize, BearerDescription,
|
||||
]):
|
||||
pass
|
||||
|
||||
# reasonable default for playing with OTA
|
||||
# 010203040506070809101112131415161718192021222324252627282930313233
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#
|
||||
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
||||
# Copyright (C) 2010-2023 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,15 +21,18 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
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 osmocom.construct import LV, filter_dict
|
||||
from osmocom.utils import rpad, lpad, b2h, h2b, h2i, i2h, str_sanitize, Hexstr
|
||||
from osmocom.tlv import bertlv_encode_len
|
||||
|
||||
from construct import *
|
||||
from pySim.construct import LV
|
||||
from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, Hexstr, h2i, i2h, str_sanitize, expand_hex
|
||||
from pySim.utils import Hexstr, SwHexstr, ResTuple
|
||||
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, SwMatchstr
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.ts_102_221 import decode_select_response
|
||||
|
||||
# A path can be either just a FID or a list of FID
|
||||
Path = typing.Union[Hexstr, List[Hexstr]]
|
||||
@@ -64,73 +67,115 @@ class SimCardCommands:
|
||||
byte by the respective instance. """
|
||||
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
|
||||
self._tp = transport
|
||||
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.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
|
||||
|
||||
@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
|
||||
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 cla_with_lchan(cla, self.lchan_nr)
|
||||
return 255
|
||||
|
||||
# Extract a single FCP item from TLV
|
||||
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
|
||||
tlvparser = TLV(['82', '83', '84', 'a5', '8a', '8b',
|
||||
'8c', '80', 'ab', 'c6', '81', '88'])
|
||||
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
|
||||
"""Sends an APDU and auto fetch response data
|
||||
|
||||
# pytlv is case sensitive!
|
||||
fcp = fcp.lower()
|
||||
|
||||
if fcp[0:2] != '62':
|
||||
raise ValueError(
|
||||
'Tag of the FCP template does not match, expected 62 but got %s' % fcp[0:2])
|
||||
|
||||
# Unfortunately the spec is not very clear if the FCP length is
|
||||
# coded as one or two byte vale, so we have to try it out by
|
||||
# checking if the length of the remaining TLV string matches
|
||||
# what we get in the length field.
|
||||
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
|
||||
exp_tlv_len = int(fcp[2:4], 16)
|
||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||
skip = 4
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
if apply_lchan:
|
||||
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
|
||||
if self.scp:
|
||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
|
||||
else:
|
||||
exp_tlv_len = int(fcp[2:6], 16)
|
||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||
skip = 6
|
||||
return self._tp.send_apdu(pdu)
|
||||
|
||||
# Skip FCP tag and length
|
||||
tlv = fcp[skip:]
|
||||
return tlvparser.parse(tlv)
|
||||
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000", apply_lchan:bool = True) -> 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.
|
||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
if apply_lchan:
|
||||
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
|
||||
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, apply_lchan:bool = True) -> 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
|
||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
|
||||
lc = i2h([len(cmd)]) if cmd_data else ''
|
||||
le = '00' if resp_constr else ''
|
||||
pdu = ''.join([cla, ins, p1, p2, lc, b2h(cmd), le])
|
||||
(data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan)
|
||||
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", apply_lchan:bool = True) -> 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,
|
||||
apply_lchan = apply_lchan)
|
||||
if not sw_match(sw, sw_exp):
|
||||
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
|
||||
return (rsp, sw)
|
||||
|
||||
# Tell the length of a record by the card response
|
||||
# USIMs respond with an FCP template, which is different
|
||||
@@ -139,10 +184,8 @@ class SimCardCommands:
|
||||
# SIM: GSM 11.11, chapter 9.2.1 SELECT
|
||||
def __record_len(self, r) -> int:
|
||||
if self.sel_ctrl == "0004":
|
||||
tlv_parsed = self.__parse_fcp(r[-1])
|
||||
file_descriptor = tlv_parsed['82']
|
||||
# See also ETSI TS 102 221, chapter 11.1.1.4.3 File Descriptor
|
||||
return int(file_descriptor[4:8], 16)
|
||||
fcp_parsed = decode_select_response(r[-1])
|
||||
return fcp_parsed['file_descriptor']['record_len']
|
||||
else:
|
||||
return int(r[-1][28:30], 16)
|
||||
|
||||
@@ -150,8 +193,8 @@ class SimCardCommands:
|
||||
# above.
|
||||
def __len(self, r) -> int:
|
||||
if self.sel_ctrl == "0004":
|
||||
tlv_parsed = self.__parse_fcp(r[-1])
|
||||
return int(tlv_parsed['80'], 16)
|
||||
fcp_parsed = decode_select_response(r[-1])
|
||||
return fcp_parsed['file_size']
|
||||
else:
|
||||
return int(r[-1][4:8], 16)
|
||||
|
||||
@@ -167,11 +210,10 @@ 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 + "00")
|
||||
rv.append((data, sw))
|
||||
if sw != '9000':
|
||||
return rv
|
||||
@@ -187,10 +229,10 @@ 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
|
||||
|
||||
@@ -201,11 +243,11 @@ class SimCardCommands:
|
||||
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 + "00")
|
||||
|
||||
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 + "a40304")
|
||||
|
||||
def select_adf(self, aid: Hexstr) -> ResTuple:
|
||||
"""Execute SELECT a given Applicaiton ADF.
|
||||
@@ -215,7 +257,7 @@ 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 + "00")
|
||||
|
||||
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
|
||||
"""Execute READD BINARY.
|
||||
@@ -236,14 +278,14 @@ 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))
|
||||
e.add_note('failed to read (offset %d)' % offset)
|
||||
raise e
|
||||
total_data += data
|
||||
chunk_offset += chunk_len
|
||||
return total_data, sw
|
||||
@@ -294,16 +336,16 @@ class SimCardCommands:
|
||||
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))
|
||||
e.add_note('failed to write chunk (chunk_offset %d, chunk_len %d)' % (chunk_offset, chunk_len))
|
||||
raise e
|
||||
total_data += data
|
||||
chunk_offset += chunk_len
|
||||
if verify:
|
||||
@@ -320,7 +362,7 @@ 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 __verify_record(self, ef: Path, rec_no: int, data: str):
|
||||
"""Verify record against given data
|
||||
@@ -359,10 +401,10 @@ class SimCardCommands:
|
||||
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):
|
||||
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):
|
||||
elif len(data) // 2 < rec_length:
|
||||
if leftpad:
|
||||
data = lpad(data, rec_length * 2)
|
||||
else:
|
||||
@@ -383,7 +425,7 @@ class SimCardCommands:
|
||||
pass
|
||||
|
||||
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
|
||||
res = self._tp.send_apdu_checksw(pdu)
|
||||
res = self.send_apdu_checksw(pdu)
|
||||
if verify:
|
||||
self.__verify_record(ef, rec_no, data)
|
||||
return res
|
||||
@@ -418,10 +460,10 @@ class SimCardCommands:
|
||||
# TS 102 221 Section 11.3.1 low-level helper
|
||||
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
|
||||
if first:
|
||||
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
|
||||
pdu = '80cb008001%02x00' % (tag)
|
||||
else:
|
||||
pdu = self.cla4lchan('80') + 'cb000000'
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
pdu = '80cb0000'
|
||||
return self.send_apdu_checksw(pdu)
|
||||
|
||||
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
|
||||
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
|
||||
@@ -437,7 +479,7 @@ 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
|
||||
@@ -448,10 +490,10 @@ class SimCardCommands:
|
||||
p1 = 0x80
|
||||
else:
|
||||
p1 = 0x00
|
||||
if isinstance(data, bytes) or isinstance(data, bytearray):
|
||||
if isinstance(data, (bytes, bytearray)):
|
||||
data = b2h(data)
|
||||
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
pdu = '80db00%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) -> ResTuple:
|
||||
"""Execute SET DATA.
|
||||
@@ -478,10 +520,10 @@ 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: Hexstr) -> ResTuple:
|
||||
@@ -493,7 +535,7 @@ class SimCardCommands:
|
||||
if len(rand) != 32:
|
||||
raise ValueError('Invalid rand')
|
||||
self.select_path(['3f00', '7f20'])
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
|
||||
return self.send_apdu_checksw('a088000010' + rand + '00', sw='9000')
|
||||
|
||||
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
|
||||
"""Execute AUTHENTICATE (USIM/ISIM).
|
||||
@@ -504,10 +546,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}
|
||||
@@ -515,7 +556,9 @@ class SimCardCommands:
|
||||
p2 = '81'
|
||||
elif context == 'gsm':
|
||||
p2 = '80'
|
||||
(data, sw) = self._tp.send_apdu_constr_checksw(
|
||||
else:
|
||||
raise ValueError("Unsupported context '%s'" % context)
|
||||
(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}
|
||||
@@ -525,11 +568,11 @@ class SimCardCommands:
|
||||
|
||||
def status(self) -> ResTuple:
|
||||
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
|
||||
return self.send_apdu_checksw('80F20000')
|
||||
|
||||
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: Hexstr) -> ResTuple:
|
||||
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
|
||||
@@ -537,31 +580,31 @@ class SimCardCommands:
|
||||
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) -> 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))
|
||||
"""Execute CREATE FILE command as per TS 102 222 Section 6.3"""
|
||||
return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
|
||||
|
||||
def resize_file(self, payload: Hexstr) -> ResTuple:
|
||||
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
|
||||
return self.send_apdu_checksw('80d40000%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: 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: 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) -> 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: str = 'open', lchan_nr: int =0) -> ResTuple:
|
||||
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
|
||||
@@ -574,8 +617,8 @@ class SimCardCommands:
|
||||
p1 = 0x80
|
||||
else:
|
||||
p1 = 0x00
|
||||
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
|
||||
return self.send_apdu_checksw(pdu)
|
||||
|
||||
def reset_card(self) -> Hexstr:
|
||||
"""Physically reset the card"""
|
||||
@@ -585,8 +628,8 @@ class SimCardCommands:
|
||||
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'):
|
||||
raise SwMatchError(sw, '9000')
|
||||
if sw != '9000':
|
||||
raise SwMatchError(sw, '9000', self._tp.sw_interpreter)
|
||||
|
||||
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
|
||||
"""Verify a given CHV (Card Holder Verification == PIN)
|
||||
@@ -596,8 +639,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)
|
||||
|
||||
@@ -610,8 +652,7 @@ 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)
|
||||
|
||||
@@ -624,8 +665,7 @@ 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)
|
||||
|
||||
@@ -638,8 +678,7 @@ 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)
|
||||
|
||||
@@ -651,8 +690,7 @@ 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)
|
||||
|
||||
@@ -662,7 +700,7 @@ class SimCardCommands:
|
||||
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), apply_lchan = False)
|
||||
|
||||
def terminal_profile(self, payload: Hexstr) -> ResTuple:
|
||||
"""Send TERMINAL PROFILE to card
|
||||
@@ -671,7 +709,7 @@ class SimCardCommands:
|
||||
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_checksw(('80100000%02x' % data_length) + payload, apply_lchan = False)
|
||||
return (data, sw)
|
||||
|
||||
# ETSI TS 102 221 11.1.22
|
||||
@@ -685,34 +723,31 @@ 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])[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, apply_lchan = False)
|
||||
negotiated_duration_secs = decode_duration(data[:4])
|
||||
resume_token = data[4:]
|
||||
return (negotiated_duration_secs, resume_token, sw)
|
||||
@@ -722,14 +757,15 @@ class SimCardCommands:
|
||||
"""Send SUSPEND UICC (resume) to the card."""
|
||||
if len(h2b(token)) != 8:
|
||||
raise ValueError("Token must be 8 bytes long")
|
||||
data, sw = self._tp.send_apdu_checksw('8076010008' + token)
|
||||
data, sw = self.send_apdu_checksw('8076010008' + token, apply_lchan = False)
|
||||
return (data, sw)
|
||||
|
||||
# GPC_SPE_034 11.3
|
||||
def get_data(self, tag: int, cla: int = 0x00):
|
||||
data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
|
||||
data, sw = self.send_apdu_checksw('%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._tp.send_apdu_checksw('807800%02x00' % (context))
|
||||
data, sw = self.send_apdu_checksw('807800%02x00' % (context))
|
||||
return (data, sw)
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
from construct.lib.containers import Container, ListContainer
|
||||
from construct.core import EnumIntegerString
|
||||
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 gsm0338
|
||||
import codecs
|
||||
import ipaddress
|
||||
|
||||
"""Utility code related to the integration of the 'construct' declarative parser."""
|
||||
|
||||
# (C) 2021-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/>.
|
||||
|
||||
|
||||
class HexAdapter(Adapter):
|
||||
"""convert a bytes() type to a string of hex nibbles."""
|
||||
|
||||
def _decode(self, obj, context, path):
|
||||
return b2h(obj)
|
||||
|
||||
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(obj, '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 == 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."""
|
||||
|
||||
def _decode(self, obj, context, path):
|
||||
return swap_nibbles(b2h(obj))
|
||||
|
||||
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
|
||||
def _invert_bool_in_obj(obj):
|
||||
for k,v in obj.items():
|
||||
# skip all private entries
|
||||
if k.startswith('_'):
|
||||
continue
|
||||
if v == False:
|
||||
obj[k] = True
|
||||
elif v == True:
|
||||
obj[k] = False
|
||||
return obj
|
||||
|
||||
def _decode(self, obj, context, path):
|
||||
return self._invert_bool_in_obj(obj)
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
return self._invert_bool_in_obj(obj)
|
||||
|
||||
class Rpad(Adapter):
|
||||
"""
|
||||
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', 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):
|
||||
target_size = self.sizeof() * self.num_per_byte
|
||||
if len(obj) > target_size:
|
||||
raise SizeofError("Input ({}) exceeds target size ({})".format(
|
||||
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):
|
||||
"""Convert GSM 03.38 encoded bytes to a string."""
|
||||
|
||||
def __init__(self, subcon, codec='gsm03.38', err='strict'):
|
||||
super().__init__(subcon)
|
||||
self.codec = codec
|
||||
self.err = err
|
||||
|
||||
def _decode(self, obj, context, path):
|
||||
return obj.decode(self.codec)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def filter_dict(d, exclude_prefix='_'):
|
||||
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
|
||||
if not isinstance(d, dict):
|
||||
return d
|
||||
res = {}
|
||||
for (key, value) in d.items():
|
||||
if key.startswith(exclude_prefix):
|
||||
continue
|
||||
if type(value) is dict:
|
||||
res[key] = filter_dict(value)
|
||||
else:
|
||||
res[key] = value
|
||||
return res
|
||||
|
||||
|
||||
def normalize_construct(c):
|
||||
"""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):
|
||||
r = {k: normalize_construct(v) for (k, v) in c.items()}
|
||||
elif isinstance(c, ListContainer):
|
||||
r = [normalize_construct(x) for x in c]
|
||||
elif isinstance(c, list):
|
||||
r = [normalize_construct(x) for x in c]
|
||||
elif isinstance(c, EnumIntegerString):
|
||||
r = str(c)
|
||||
else:
|
||||
r = c
|
||||
return r
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
# Default value for Reserved for Future Use (RFU) bits/bytes
|
||||
# See TS 31.101 Sec. "3.4 Coding Conventions"
|
||||
__RFU_VALUE = 0
|
||||
|
||||
# Field that packs Reserved for Future Use (RFU) bit
|
||||
FlagRFU = Default(Flag, __RFU_VALUE)
|
||||
|
||||
# Field that packs Reserved for Future Use (RFU) byte
|
||||
ByteRFU = Default(Byte, __RFU_VALUE)
|
||||
|
||||
# Field that packs all remaining Reserved for Future Use (RFU) bytes
|
||||
GreedyBytesRFU = Default(GreedyBytes, b'')
|
||||
|
||||
|
||||
def BitsRFU(n=1):
|
||||
'''
|
||||
Field that packs Reserved for Future Use (RFU) bit(s)
|
||||
as defined in TS 31.101 Sec. "3.4 Coding Conventions"
|
||||
|
||||
Use this for (currently) unused/reserved bits whose contents
|
||||
should be initialized automatically but should not be cleared
|
||||
in the future or when restoring read data (unlike padding).
|
||||
|
||||
Parameters:
|
||||
n (Integer): Number of bits (default: 1)
|
||||
'''
|
||||
return Default(BitsInteger(n), __RFU_VALUE)
|
||||
|
||||
|
||||
def BytesRFU(n=1):
|
||||
'''
|
||||
Field that packs Reserved for Future Use (RFU) byte(s)
|
||||
as defined in TS 31.101 Sec. "3.4 Coding Conventions"
|
||||
|
||||
Use this for (currently) unused/reserved bytes whose contents
|
||||
should be initialized automatically but should not be cleared
|
||||
in the future or when restoring read data (unlike padding).
|
||||
|
||||
Parameters:
|
||||
n (Integer): Number of bytes (default: 1)
|
||||
'''
|
||||
return Default(Bytes(n), __RFU_VALUE)
|
||||
|
||||
|
||||
def GsmString(n):
|
||||
'''
|
||||
GSM 03.38 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 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):
|
||||
super().__init__()
|
||||
self.signed = signed
|
||||
self.swapped = swapped
|
||||
self.minlen = minlen
|
||||
|
||||
def _parse(self, stream, context, path):
|
||||
data = stream_read_entire(stream, path)
|
||||
if evaluate(self.swapped, context):
|
||||
data = swapbytes(data)
|
||||
try:
|
||||
return int.from_bytes(data, byteorder='big', signed=self.signed)
|
||||
except ValueError as e:
|
||||
raise IntegerError(str(e), path=path)
|
||||
|
||||
def __bytes_required(self, i, minlen=0):
|
||||
if self.signed:
|
||||
raise NotImplementedError("FIXME: Implement support for encoding signed integer")
|
||||
|
||||
# compute how many bytes we need
|
||||
nbytes = 1
|
||||
while True:
|
||||
i = i >> 8
|
||||
if i == 0:
|
||||
break
|
||||
else:
|
||||
nbytes = nbytes + 1
|
||||
|
||||
# round up to the minimum number
|
||||
# of bytes we anticipate
|
||||
if nbytes < minlen:
|
||||
nbytes = minlen
|
||||
|
||||
return nbytes
|
||||
|
||||
def _build(self, obj, stream, context, path):
|
||||
if not isinstance(obj, integertypes):
|
||||
raise IntegerError(f"value {obj} is not an integer", path=path)
|
||||
length = self.__bytes_required(obj, self.minlen)
|
||||
try:
|
||||
data = obj.to_bytes(length, byteorder='big', signed=self.signed)
|
||||
except ValueError as e:
|
||||
raise IntegerError(str(e), path=path)
|
||||
if evaluate(self.swapped, context):
|
||||
data = swapbytes(data)
|
||||
stream_write(stream, data, length, path)
|
||||
return obj
|
||||
|
||||
# merged definitions of 24.008 + 23.040
|
||||
TypeOfNumber = Enum(BitsInteger(3), unknown=0, international=1, national=2, network_specific=3,
|
||||
short_code=4, alphanumeric=5, abbreviated=6, reserved_for_extension=7)
|
||||
NumberingPlan = Enum(BitsInteger(4), unknown=0, isdn_e164=1, data_x121=3, telex_f69=4,
|
||||
sc_specific_5=5, sc_specific_6=6, national=8, private=9,
|
||||
ermes=10, reserved_cts=11, reserved_for_extension=15)
|
||||
TonNpi = BitStruct('ext'/Flag, 'type_of_number'/TypeOfNumber, 'numbering_plan_id'/NumberingPlan)
|
||||
@@ -1,10 +1,55 @@
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
from importlib import resources
|
||||
|
||||
import asn1tools
|
||||
class PMO:
|
||||
"""Convenience conversion class for ProfileManagementOperation as used in ES9+ notifications."""
|
||||
pmo4operation = {
|
||||
'install': 0x80,
|
||||
'enable': 0x40,
|
||||
'disable': 0x20,
|
||||
'delete': 0x10,
|
||||
}
|
||||
|
||||
def compile_asn1_subdir(subdir_name:str):
|
||||
def __init__(self, op: str):
|
||||
if not op in self.pmo4operation:
|
||||
raise ValueError('Unknown operation "%s"' % op)
|
||||
self.op = op
|
||||
|
||||
def to_int(self):
|
||||
return self.pmo4operation[self.op]
|
||||
|
||||
@staticmethod
|
||||
def _num_bits(data: int)-> int:
|
||||
for i in range(0, 8):
|
||||
if data & (1 << i):
|
||||
return 8-i
|
||||
return 0
|
||||
|
||||
def to_bitstring(self) -> Tuple[bytes, int]:
|
||||
"""return value in a format as used by asn1tools for BITSTRING."""
|
||||
val = self.to_int()
|
||||
return (bytes([val]), self._num_bits(val))
|
||||
|
||||
@classmethod
|
||||
def from_int(cls, i: int) -> 'PMO':
|
||||
"""Parse an integer representation."""
|
||||
for k, v in cls.pmo4operation.items():
|
||||
if v == i:
|
||||
return cls(k)
|
||||
raise ValueError('Unknown PMO 0x%02x' % i)
|
||||
|
||||
@classmethod
|
||||
def from_bitstring(cls, bstr: Tuple[bytes, int]) -> 'PMO':
|
||||
"""Parse a asn1tools BITSTRING representation."""
|
||||
return cls.from_int(bstr[0][0])
|
||||
|
||||
def __str__(self):
|
||||
return self.op
|
||||
|
||||
def compile_asn1_subdir(subdir_name:str, codec='der'):
|
||||
"""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):
|
||||
@@ -13,4 +58,80 @@ def compile_asn1_subdir(subdir_name:str):
|
||||
asn_txt += "\n"
|
||||
#else:
|
||||
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
|
||||
return asn1tools.compile_string(asn_txt, codec='der')
|
||||
return asn1tools.compile_string(asn_txt, codec=codec)
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -983,7 +983,7 @@ keyAccess [22] OCTET STRING (SIZE (1)) DEFAULT '00'H,
|
||||
keyIdentifier [2] OCTET STRING (SIZE (1)),
|
||||
keyVersionNumber [3] OCTET STRING (SIZE (1)),
|
||||
keyCounterValue [5] OCTET STRING OPTIONAL,
|
||||
keyCompontents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||
keyComponents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||
keyType [0] OCTET STRING,
|
||||
keyData [6] OCTET STRING,
|
||||
macLength[7] UInt8 DEFAULT 8
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
# 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).
|
||||
MAX_SEGMENT_SIZE = 1020
|
||||
|
||||
import abc
|
||||
from typing import List
|
||||
@@ -36,12 +35,15 @@ 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
|
||||
from osmocom.utils import b2h
|
||||
from osmocom.tlv import bertlv_encode_len, bertlv_parse_one
|
||||
|
||||
# don't log by default
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
MAX_SEGMENT_SIZE = 1020
|
||||
|
||||
class BspAlgo(abc.ABC):
|
||||
blocksize: int
|
||||
|
||||
@@ -50,11 +52,11 @@ class BspAlgo(abc.ABC):
|
||||
if in_len % multiple == 0:
|
||||
return b''
|
||||
pad_cnt = multiple - (in_len % multiple)
|
||||
return b'\x00' * pad_cnt
|
||||
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), self.blocksize, padding)
|
||||
return indat + self._get_padding(len(indat), multiple, padding)
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
@@ -81,17 +83,14 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def _unpad(self, padded: bytes) -> bytes:
|
||||
"""Remove the padding from padded data."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _encrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _decrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
class BspAlgoCryptAES128(BspAlgoCrypt):
|
||||
name = 'AES-CBC-128'
|
||||
@@ -166,7 +165,6 @@ class BspAlgoMac(BspAlgo, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def _auth(self, temp_data: bytes) -> bytes:
|
||||
"""To be implemented by algorithm specific derived class."""
|
||||
pass
|
||||
|
||||
class BspAlgoMacAES128(BspAlgoMac):
|
||||
name = 'AES-CMAC-128'
|
||||
@@ -289,7 +287,9 @@ class BspInstance:
|
||||
|
||||
def demac_only_one(self, ciphertext: bytes) -> bytes:
|
||||
payload = self.m_algo.verify(ciphertext)
|
||||
tdict, l, val, remain = bertlv_parse_one(payload)
|
||||
_tdict, _l, val, _remain = bertlv_parse_one(payload)
|
||||
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
|
||||
self.c_algo.block_nr += 1
|
||||
return val
|
||||
|
||||
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
|
||||
|
||||
239
pySim/esim/es2p.py
Normal file
239
pySim/esim/es2p.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""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 requests
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from pySim.esim.http_json_api import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
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 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.isoformat(data)
|
||||
|
||||
class NotificationPointId(ApiParamInteger):
|
||||
pass
|
||||
|
||||
class NotificationPointStatus(ApiParam):
|
||||
pass
|
||||
|
||||
class ResultData(ApiParamBase64):
|
||||
pass
|
||||
|
||||
class Es2PlusApiFunction(JsonHttpApiFunction):
|
||||
"""Base classs for representing an ES2+ API Function."""
|
||||
pass
|
||||
|
||||
# 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': 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': JsonResponseHeader,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'smdpAddress': 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': 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': 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())
|
||||
@@ -17,10 +17,14 @@
|
||||
# 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
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.tlv import bertlv_encode_tag, bertlv_encode_len, bertlv_parse_one_rawtag
|
||||
from osmocom.tlv import bertlv_return_one_rawtlv
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim.bsp import BspInstance
|
||||
from pySim.esim import PMO
|
||||
|
||||
# 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
|
||||
@@ -74,6 +78,23 @@ class ProfileMetadata:
|
||||
self.iccid_bin = iccid_bin
|
||||
self.spn = spn
|
||||
self.profile_name = profile_name
|
||||
self.icon = None
|
||||
self.icon_type = None
|
||||
self.notifications = []
|
||||
|
||||
def set_icon(self, is_png: bool, icon_data: bytes):
|
||||
"""Set the icon that is part of the metadata."""
|
||||
if len(icon_data) > 1024:
|
||||
raise ValueError('Icon data must not exceed 1024 bytes')
|
||||
self.icon = icon_data
|
||||
if is_png:
|
||||
self.icon_type = 1
|
||||
else:
|
||||
self.icon_type = 0
|
||||
|
||||
def add_notification(self, event: str, address: str):
|
||||
"""Add an 'other' notification to the notification configuration of the metadata"""
|
||||
self.notifications.append((event, address))
|
||||
|
||||
def gen_store_metadata_request(self) -> bytes:
|
||||
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
|
||||
@@ -82,6 +103,15 @@ class ProfileMetadata:
|
||||
'serviceProviderName': self.spn,
|
||||
'profileName': self.profile_name,
|
||||
}
|
||||
if self.icon:
|
||||
smr['icon'] = self.icon
|
||||
smr['iconType'] = self.icon_type
|
||||
nci = []
|
||||
for n in self.notifications:
|
||||
pmo = PMO(n[0])
|
||||
nci.append({'profileManagementOperation': pmo.to_bitstring(), 'notificationAddress': n[1]})
|
||||
if len(nci):
|
||||
smr['notificationConfigurationInfo'] = nci
|
||||
return rsp.asn1.encode('StoreMetadataRequest', smr)
|
||||
|
||||
|
||||
@@ -183,3 +213,79 @@ class BoundProfilePackage(ProfilePackage):
|
||||
|
||||
# manual DER encode: wrap in outer SEQUENCE
|
||||
return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq
|
||||
|
||||
def decode(self, euicc_ot, eid: str, bpp_bin: bytes):
|
||||
"""Decode a BPP into the PPP and subsequently UPP. This is what happens inside an eUICC."""
|
||||
|
||||
def split_bertlv_sequence(sequence: bytes) -> List[bytes]:
|
||||
remainder = sequence
|
||||
ret = []
|
||||
while remainder:
|
||||
_tag, _l, tlv, remainder = bertlv_return_one_rawtlv(remainder)
|
||||
ret.append(tlv)
|
||||
return ret
|
||||
|
||||
# we don't use rsp.asn1.decode('boundProfilePackage') here, as the BSP needs
|
||||
# fully encoded + MACed TLVs including their tag + length values.
|
||||
#bpp = rsp.asn1.decode('BoundProfilePackage', bpp_bin)
|
||||
|
||||
tag, _l, v, _remainder = bertlv_parse_one_rawtag(bpp_bin)
|
||||
if len(_remainder):
|
||||
raise ValueError('Excess data at end of TLV')
|
||||
if tag != 0xbf36:
|
||||
raise ValueError('Unexpected outer tag: %s' % tag)
|
||||
|
||||
# InitialiseSecureChannelRequest
|
||||
tag, _l, iscr_bin, remainder = bertlv_return_one_rawtlv(v)
|
||||
iscr = rsp.asn1.decode('InitialiseSecureChannelRequest', iscr_bin)
|
||||
|
||||
# configureIsdpRequest
|
||||
tag, _l, firstSeqOf87, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag != 0xa0:
|
||||
raise ValueError("Unexpected 'firstSequenceOf87' tag: %s" % tag)
|
||||
firstSeqOf87 = split_bertlv_sequence(firstSeqOf87)
|
||||
|
||||
# storeMetadataRequest
|
||||
tag, _l, seqOf88, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag != 0xa1:
|
||||
raise ValueError("Unexpected 'sequenceOf88' tag: %s" % tag)
|
||||
seqOf88 = split_bertlv_sequence(seqOf88)
|
||||
|
||||
tag, _l, tlv, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag == 0xa2:
|
||||
secondSeqOf87 = split_bertlv_sequence(tlv)
|
||||
tag2, _l, seqOf86, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag2 != 0xa3:
|
||||
raise ValueError("Unexpected 'sequenceOf86' tag: %s" % tag)
|
||||
seqOf86 = split_bertlv_sequence(seqOf86)
|
||||
elif tag == 0xa3:
|
||||
secondSeqOf87 = None
|
||||
seqOf86 = split_bertlv_sequence(tlv)
|
||||
else:
|
||||
raise ValueError("Unexpected 'secondSequenceOf87' tag: %s" % tag)
|
||||
|
||||
# extract smdoOtpk from initialiseSecureChannel
|
||||
smdp_otpk = iscr['smdpOtpk']
|
||||
|
||||
# Generate Session Keys using the CRT, opPK.DP.ECKA and otSK.EUICC.ECKA according to annex G
|
||||
smdp_public_key = ec.EllipticCurvePublicKey.from_encoded_point(euicc_ot.curve, smdp_otpk)
|
||||
self.shared_secret = euicc_ot.exchange(ec.ECDH(), smdp_public_key)
|
||||
|
||||
crt = iscr['controlRefTemplate']
|
||||
bsp = BspInstance.from_kdf(self.shared_secret, int.from_bytes(crt['keyType'], 'big'), int.from_bytes(crt['keyLen'], 'big'), crt['hostId'], h2b(eid))
|
||||
|
||||
self.encoded_configureISDPRequest = bsp.demac_and_decrypt(firstSeqOf87)
|
||||
self.configureISDPRequest = rsp.asn1.decode('ConfigureISDPRequest', self.encoded_configureISDPRequest)
|
||||
|
||||
self.encoded_storeMetadataRequest = bsp.demac_only(seqOf88)
|
||||
self.storeMetadataRequest = rsp.asn1.decode('StoreMetadataRequest', self.encoded_storeMetadataRequest)
|
||||
|
||||
if secondSeqOf87 != None:
|
||||
rsk_bin = bsp.demac_and_decrypt(secondSeqOf87)
|
||||
rsk = rsp.asn1.decode('ReplaceSessionKeysRequest', rsk_bin)
|
||||
# process replace_session_keys!
|
||||
bsp = BspInstance(rsk['ppkEnc'], rsk['ppkCmac'], rsk['initialMacChainingValue'])
|
||||
self.replaceSessionKeysRequest = rsk
|
||||
|
||||
self.upp = bsp.demac_and_decrypt(seqOf86)
|
||||
return self.upp
|
||||
|
||||
177
pySim/esim/es9p.py
Normal file
177
pySim/esim/es9p.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""GSMA eSIM RSP ES9+ interface according ot 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 requests
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim.http_json_api import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class param:
|
||||
class RspAsn1Par(ApiParamBase64):
|
||||
"""Generalized RSP ASN.1 parameter: base64-wrapped ASN.1 DER. Derived classes must provide
|
||||
the asn1_type class variable to indicate the name of the ASN.1 type to use for encode/decode."""
|
||||
asn1_type = None # must be overridden by derived class
|
||||
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
data = ApiParamBase64.decode(data)
|
||||
return rsp.asn1.decode(cls.asn1_type, data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
data = rsp.asn1.encode(cls.asn1_type, data)
|
||||
return ApiParamBase64.encode(data)
|
||||
|
||||
class EuiccInfo1(RspAsn1Par):
|
||||
asn1_type = 'EUICCInfo1'
|
||||
|
||||
class ServerSigned1(RspAsn1Par):
|
||||
asn1_type = 'ServerSigned1'
|
||||
|
||||
class PrepareDownloadResponse(RspAsn1Par):
|
||||
asn1_type = 'PrepareDownloadResponse'
|
||||
|
||||
class AuthenticateServerResponse(RspAsn1Par):
|
||||
asn1_type = 'AuthenticateServerResponse'
|
||||
|
||||
class SmdpSigned2(RspAsn1Par):
|
||||
asn1_type = 'SmdpSigned2'
|
||||
|
||||
class StoreMetadataRequest(RspAsn1Par):
|
||||
asn1_type = 'StoreMetadataRequest'
|
||||
|
||||
class PendingNotification(RspAsn1Par):
|
||||
asn1_type = 'PendingNotification'
|
||||
|
||||
class CancelSessionResponse(RspAsn1Par):
|
||||
asn1_type = 'CancelSessionResponse'
|
||||
|
||||
class TransactionId(ApiParamString):
|
||||
pass
|
||||
|
||||
class Es9PlusApiFunction(JsonHttpApiFunction):
|
||||
pass
|
||||
|
||||
# ES9+ InitiateAuthentication function (SGP.22 section 6.5.2.6)
|
||||
class InitiateAuthentication(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/initiateAuthentication'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'euiccChallenge': ApiParamBase64,
|
||||
'euiccInfo1': param.EuiccInfo1,
|
||||
'smdpAddress': SmdpAddress,
|
||||
}
|
||||
input_mandatory = ['euiccChallenge', 'euiccInfo1', 'smdpAddress']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'transactionId': param.TransactionId,
|
||||
'serverSigned1': param.ServerSigned1,
|
||||
'serverSignature1': ApiParamBase64,
|
||||
'euiccCiPKIdToBeUsed': ApiParamBase64,
|
||||
'serverCertificate': ApiParamBase64,
|
||||
}
|
||||
output_mandatory = ['header', 'transactionId', 'serverSigned1', 'serverSignature1',
|
||||
'euiccCiPKIdToBeUsed', 'serverCertificate']
|
||||
|
||||
# ES9+ GetBoundProfilePackage function (SGP.22 section 6.5.2.7)
|
||||
class GetBoundProfilePackage(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/getBoundProfilePackage'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'transactionId': param.TransactionId,
|
||||
'prepareDownloadResponse': param.PrepareDownloadResponse,
|
||||
}
|
||||
input_mandatory = ['transactionId', 'prepareDownloadResponse']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'transactionId': param.TransactionId,
|
||||
'boundProfilePackage': ApiParamBase64,
|
||||
}
|
||||
output_mandatory = ['header', 'transactionId', 'boundProfilePackage']
|
||||
|
||||
# ES9+ AuthenticateClient function (SGP.22 section 6.5.2.8)
|
||||
class AuthenticateClient(Es9PlusApiFunction):
|
||||
path= '/gsma/rsp2/es9plus/authenticateClient'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'transactionId': param.TransactionId,
|
||||
'authenticateServerResponse': param.AuthenticateServerResponse,
|
||||
}
|
||||
input_mandatory = ['transactionId', 'authenticateServerResponse']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'transactionId': param.TransactionId,
|
||||
'profileMetadata': param.StoreMetadataRequest,
|
||||
'smdpSigned2': param.SmdpSigned2,
|
||||
'smdpSignature2': ApiParamBase64,
|
||||
'smdpCertificate': ApiParamBase64,
|
||||
}
|
||||
output_mandatory = ['header', 'transactionId', 'profileMetadata', 'smdpSigned2',
|
||||
'smdpSignature2', 'smdpCertificate']
|
||||
|
||||
# ES9+ HandleNotification function (SGP.22 section 6.5.2.9)
|
||||
class HandleNotification(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/handleNotification'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'pendingNotification': param.PendingNotification,
|
||||
}
|
||||
input_mandatory = ['pendingNotification']
|
||||
expected_http_status = 204
|
||||
|
||||
# ES9+ CancelSession function (SGP.22 section 6.5.2.10)
|
||||
class CancelSession(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/cancelSession'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'transactionId': param.TransactionId,
|
||||
'cancelSessionResponse': param.CancelSessionResponse,
|
||||
}
|
||||
input_mandatory = ['transactionId', 'cancelSessionResponse']
|
||||
|
||||
class Es9pApiClient:
|
||||
def __init__(self, url_prefix:str, server_cert_verify: str = None):
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False # FIXME HACK
|
||||
if server_cert_verify:
|
||||
self.session.verify = server_cert_verify
|
||||
|
||||
self.initiateAuthentication = InitiateAuthentication(url_prefix, '', self.session)
|
||||
self.authenticateClient = AuthenticateClient(url_prefix, '', self.session)
|
||||
self.getBoundProfilePackage = GetBoundProfilePackage(url_prefix, '', self.session)
|
||||
self.handleNotification = HandleNotification(url_prefix, '', self.session)
|
||||
self.cancelSession = CancelSession(url_prefix, '', self.session)
|
||||
|
||||
def call_initiateAuthentication(self, data: dict) -> dict:
|
||||
return self.initiateAuthentication.call(data)
|
||||
|
||||
def call_authenticateClient(self, data: dict) -> dict:
|
||||
return self.authenticateClient.call(data)
|
||||
|
||||
def call_getBoundProfilePackage(self, data: dict) -> dict:
|
||||
return self.getBoundProfilePackage.call(data)
|
||||
|
||||
def call_handleNotification(self, data: dict) -> dict:
|
||||
return self.handleNotification.call(data)
|
||||
|
||||
def call_cancelSession(self, data: dict) -> dict:
|
||||
return self.cancelSession.call(data)
|
||||
259
pySim/esim/http_json_api.py
Normal file
259
pySim/esim/http_json_api.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""GSMA eSIM RSP HTTP/REST/JSON 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 typing import Optional
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class ApiParam(abc.ABC):
|
||||
"""A class reprsenting a single parameter in the 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 isinstance(data, int):
|
||||
return
|
||||
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 ApiParamBase64(ApiParam):
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
return base64.b64decode(data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
return base64.b64encode(data).decode('ascii')
|
||||
|
||||
class SmdpAddress(ApiParamFqdn):
|
||||
pass
|
||||
|
||||
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 ApiError(Exception):
|
||||
"""Exception representing an error at the 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 JsonHttpApiFunction(abc.ABC):
|
||||
"""Base classs for representing an HTTP[s] 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
|
||||
# the HTTP method used (GET, OPTIONS, HEAD, POST, PUT, PATCH or DELETE)
|
||||
http_method = 'POST'
|
||||
extra_http_req_headers = {}
|
||||
|
||||
def __init__(self, url_prefix: str, func_req_id: Optional[str], session: requests.Session):
|
||||
self.url_prefix = url_prefix
|
||||
self.func_req_id = func_req_id
|
||||
self.session = session
|
||||
|
||||
def encode(self, data: dict, func_call_id: Optional[str] = None) -> dict:
|
||||
"""Validate an encode input dict into JSON-serializable dict for request body."""
|
||||
output = {}
|
||||
if func_call_id:
|
||||
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 response body."""
|
||||
output = {}
|
||||
if 'header' in self.output_params:
|
||||
# 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 ApiError(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: Optional[str] = None, timeout=10) -> Optional[dict]:
|
||||
"""Make an API call to the HTTP 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))
|
||||
req_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
|
||||
}
|
||||
req_headers.update(self.extra_http_req_headers)
|
||||
|
||||
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
|
||||
response = self.session.request(self.http_method, url, data=encoded, headers=req_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(req_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)
|
||||
|
||||
if response.content:
|
||||
return self.decode(response.json())
|
||||
return None
|
||||
@@ -19,12 +19,12 @@
|
||||
|
||||
from typing import Optional
|
||||
import shelve
|
||||
import copyreg
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography import x509
|
||||
from collections.abc import MutableMapping
|
||||
from osmocom.utils import b2h
|
||||
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
|
||||
|
||||
from pySim.esim import compile_asn1_subdir
|
||||
|
||||
@@ -35,10 +35,11 @@ class RspSessionState:
|
||||
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):
|
||||
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
|
||||
# used at a later point between API calls
|
||||
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
|
||||
@@ -97,4 +98,35 @@ class RspSessionState:
|
||||
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."""
|
||||
pass
|
||||
|
||||
def extract_euiccSigned1(authenticateServerResponse: bytes) -> bytes:
|
||||
"""Extract the raw, DER-encoded binary euiccSigned1 field from the given AuthenticateServerResponse. This
|
||||
is needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
|
||||
rawtag, l, v, remainder = bertlv_parse_one_rawtag(authenticateServerResponse)
|
||||
if len(remainder):
|
||||
raise ValueError('Excess data at end of TLV')
|
||||
if rawtag != 0xbf38:
|
||||
raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
|
||||
rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
|
||||
if rawtag != 0xa0:
|
||||
raise ValueError('Unexpected tag where CHOICE was expected')
|
||||
rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
|
||||
if rawtag != 0x30:
|
||||
raise ValueError('Unexpected tag where SEQUENCE was expected')
|
||||
return tlv2
|
||||
|
||||
def extract_euiccSigned2(prepareDownloadResponse: bytes) -> bytes:
|
||||
"""Extract the raw, DER-encoded binary euiccSigned2 field from the given prepareDownloadrResponse. This is
|
||||
needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
|
||||
rawtag, l, v, remainder = bertlv_parse_one_rawtag(prepareDownloadResponse)
|
||||
if len(remainder):
|
||||
raise ValueError('Excess data at end of TLV')
|
||||
if rawtag != 0xbf21:
|
||||
raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
|
||||
rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
|
||||
if rawtag != 0xa0:
|
||||
raise ValueError('Unexpected tag where CHOICE was expected')
|
||||
rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
|
||||
if rawtag != 0x30:
|
||||
raise ValueError('Unexpected tag where SEQUENCE was expected')
|
||||
return tlv2
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
120
pySim/esim/saip/oid.py
Normal file
120
pySim/esim/saip/oid.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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 functools import total_ordering
|
||||
from typing import List, Union
|
||||
|
||||
@total_ordering
|
||||
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])
|
||||
|
||||
@staticmethod
|
||||
def highest_oid(oids: List['OID']) -> 'OID':
|
||||
return sorted(oids)[-1]
|
||||
|
||||
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))
|
||||
|
||||
def __eq__(self, other: 'OID'):
|
||||
return (self.intlist == other.intlist)
|
||||
|
||||
def __ne__(self, other: 'OID'):
|
||||
# implement based on __eq__
|
||||
return not (self == other)
|
||||
|
||||
def cmp(self, other: 'OID'):
|
||||
self_len = len(self.intlist)
|
||||
other_len = len(other.intlist)
|
||||
common_len = min(self_len, other_len)
|
||||
max_len = max(self_len, other_len)
|
||||
|
||||
for i in range(0, max_len+1):
|
||||
if i >= self_len:
|
||||
# other list is longer
|
||||
return -1
|
||||
if i >= other_len:
|
||||
# our list is longer
|
||||
return 1
|
||||
if self.intlist[i] > other.intlist[i]:
|
||||
# our version is higher
|
||||
return 1
|
||||
if self.intlist[i] < other.intlist[i]:
|
||||
# other version is higher
|
||||
return -1
|
||||
# continue to next digit
|
||||
return 0
|
||||
|
||||
def __gt__(self, other: 'OID'):
|
||||
if self.cmp(other) > 0:
|
||||
return True
|
||||
|
||||
def prefix_match(self, oid_str: Union[str, 'OID']):
|
||||
"""determine if oid_str is equal or below our OID."""
|
||||
return str(oid_str).startswith(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_USIMopt_not_by_default = eOID("2.5")
|
||||
ADF_USIMopt_not_by_default_v2 = eOID("2.5.2")
|
||||
ADF_USIMopt_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_ISIMopt_not_by_default = eOID("2.9")
|
||||
ADF_ISIMopt_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_CSIMopt_not_by_default = eOID("2.11")
|
||||
ADF_CSIMopt_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_by_default = eOID("2.17")
|
||||
IoTopt_not_by_default = eOID("2.18")
|
||||
@@ -16,57 +16,189 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
from typing import List, Tuple, Optional
|
||||
import io
|
||||
from typing import List, Tuple
|
||||
|
||||
from osmocom.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_key:str) -> List[Tuple]:
|
||||
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] != unwanted_key, l))
|
||||
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."""
|
||||
file = remove_unwanted_tuples_from_list(file, 'fillFileContent')
|
||||
# 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."""
|
||||
"""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, value):
|
||||
self.value = value
|
||||
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, pe_seq: ProfileElementSequence):
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
pass
|
||||
|
||||
class Iccid(ConfigurableParameter):
|
||||
"""Configurable ICCID. Expects the value to be in EF.ICCID format."""
|
||||
name = 'iccid'
|
||||
"""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; FIXME: swap nibbles!
|
||||
pes.get_pe_by_type('header').decoded['iccid'] = self.value
|
||||
# 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_by_type('mf').decoded['ef-iccid'], self.value)
|
||||
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 n EF.IMSI format."""
|
||||
name = 'imsi'
|
||||
"""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_by_type('usim'):
|
||||
file_replace_content(pe.decoded['ef-imsi'], self.value)
|
||||
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
|
||||
@@ -77,13 +209,25 @@ def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> Pr
|
||||
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'] = self.value
|
||||
pukCode['pukValue'] = h2b(padded_puk)
|
||||
return
|
||||
raise ValueError('cannot find pukCode')
|
||||
class Puk1(Puk, keyReference=0x01):
|
||||
@@ -92,29 +236,52 @@ 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'] = self.value
|
||||
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'] = self.value
|
||||
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']:
|
||||
@@ -133,7 +300,12 @@ class Adm2(Pin, keyReference=0x0B):
|
||||
|
||||
|
||||
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']
|
||||
@@ -146,5 +318,7 @@ class K(AlgoConfig, key='key'):
|
||||
class Opc(AlgoConfig, key='opc'):
|
||||
pass
|
||||
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
974
pySim/esim/saip/templates.py
Normal file
974
pySim/esim/saip/templates.py
Normal file
@@ -0,0 +1,974 @@
|
||||
# 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
|
||||
from pySim.utils import all_subclasses, h2b
|
||||
from pySim.filesystem import Path
|
||||
import pySim.esim.saip.oid as OID
|
||||
|
||||
class FileTemplate:
|
||||
"""Representation of a single file in a SimAlliance/TCA Profile Template. The argument order
|
||||
is done to match that of the tables in Section 9 of the SAIP specification."""
|
||||
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, repeat:bool = False, ppath: List[int] = []):
|
||||
"""
|
||||
Args:
|
||||
fid: The 16bit file-identifier of the file
|
||||
name: The name of the file in human-readable "EF.FOO", "DF.BAR" notation
|
||||
ftype: The type of the file; can be 'MF', 'ADF', 'DF', 'TR', 'LF', 'CY', 'BT'
|
||||
nb_rec: Then number of records (only valid for 'LF' and 'CY')
|
||||
size: The size of the file ('TR', 'BT'); size of each record ('LF, 'CY')
|
||||
arr: The record number of EF.ARR for referenced access rules
|
||||
sfi: The short file identifier, if any
|
||||
default_val: The default value [pattern] of the file
|
||||
content_rqd: Whether an instance of template *must* specify file contents
|
||||
params: A list of parameters that an instance of the template *must* specify
|
||||
ass_serv: The associated service[s] of the service table
|
||||
high_update: Is this file of "high update frequency" type?
|
||||
pe_name: The name of this file in the ASN.1 type of the PE. Auto-generated for most.
|
||||
repeat: Whether the default_val pattern is a repeating pattern.
|
||||
ppath: The intermediate path between the base_df of the ProfileTemplate and this file. If not
|
||||
specified, the file will be created immediately underneath the base_df.
|
||||
"""
|
||||
# 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', 'BT']:
|
||||
self.file_size = size
|
||||
self.arr = arr
|
||||
self.sfi = sfi
|
||||
self.default_val = default_val
|
||||
self.default_val_repeat = repeat
|
||||
self.content_rqd = content_rqd
|
||||
self.params = params
|
||||
self.ass_serv = ass_serv
|
||||
self.high_update = high_update
|
||||
self.ppath = ppath # parent path, if this FileTemplate is not immediately below the base_df
|
||||
# initialize empty
|
||||
self.parent = None
|
||||
self.children = []
|
||||
if self.default_val:
|
||||
length = self._default_value_len() or 100
|
||||
# run the method once to verify the pattern can be processed
|
||||
self.expand_default_value_pattern(length)
|
||||
|
||||
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, ppath=%s)" % (self.name, self.pe_name, s_fid, self.file_type, s_arr, s_sfi, self.ppath)
|
||||
|
||||
def print_tree(self, indent:str = ""):
|
||||
"""recursive printing of FileTemplate tree structure."""
|
||||
print("%s%s (%s)" % (indent, repr(self), self.path))
|
||||
indent += " "
|
||||
for c in self.children:
|
||||
c.print_tree(indent)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Return the path of the given File within the hierarchy."""
|
||||
if self.parent:
|
||||
return self.parent.path + self.name
|
||||
else:
|
||||
return Path(self.name)
|
||||
|
||||
def get_file_by_path(self, path: List[str]) -> Optional['FileTemplate']:
|
||||
"""Return a FileTemplate matching the given path within this ProfileTemplate."""
|
||||
if path[0].lower() != self.name.lower():
|
||||
return None
|
||||
for c in self.children:
|
||||
if path[1].lower() == c.name.lower():
|
||||
return c.get_file_by_path(path[1:])
|
||||
|
||||
def _default_value_len(self):
|
||||
if self.file_type in ['TR']:
|
||||
return self.file_size
|
||||
elif self.file_type in ['LF', 'CY']:
|
||||
return self.rec_len
|
||||
|
||||
def expand_default_value_pattern(self, length: Optional[int] = None) -> Optional[bytes]:
|
||||
"""Expand the default value pattern to the specified length."""
|
||||
if length is None:
|
||||
length = self._default_value_len()
|
||||
if length is None:
|
||||
raise ValueError("%s does not have a default length" % self)
|
||||
if not self.default_val:
|
||||
return None
|
||||
if not '...' in self.default_val:
|
||||
return h2b(self.default_val)
|
||||
l = self.default_val.split('...')
|
||||
if len(l) != 2:
|
||||
raise ValueError("Pattern '%s' contains more than one ..." % self.default_val)
|
||||
prefix = h2b(l[0])
|
||||
suffix = h2b(l[1])
|
||||
pad_len = length - len(prefix) - len(suffix)
|
||||
if pad_len <= 0:
|
||||
ret = prefix + suffix
|
||||
return ret[:length]
|
||||
return prefix + prefix[-1:] * pad_len + suffix
|
||||
|
||||
|
||||
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
|
||||
optional: bool = False
|
||||
oid: Optional[OID.eOID] = None
|
||||
files: List[FileTemplate] = []
|
||||
|
||||
# indicates that a given template does not have its own 'base DF', but that its contents merely
|
||||
# extends that of the 'base DF' of another template
|
||||
extends: Optional['ProfileTemplate'] = None
|
||||
|
||||
# indicates a parent ProfileTemplate below whose 'base DF' our files should be placed.
|
||||
parent: Optional['ProfileTemplate'] = None
|
||||
|
||||
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)
|
||||
cur_df = None
|
||||
|
||||
cls.files_by_pename: dict[str,FileTemplate] = {}
|
||||
cls.tree: List[FileTemplate] = []
|
||||
|
||||
if not cls.optional and not cls.files[0].file_type in ['MF', 'DF', 'ADF']:
|
||||
raise ValueError('First file in non-optional template must be MF, DF or ADF (is: %s)' % cls.files[0])
|
||||
for f in cls.files:
|
||||
if f.file_type in ['MF', 'DF', 'ADF']:
|
||||
if cur_df == None:
|
||||
cls.tree.append(f)
|
||||
f.parent = None
|
||||
cur_df = f
|
||||
else:
|
||||
# "cd .."
|
||||
if cur_df.parent:
|
||||
cur_df = cur_df.parent
|
||||
f.parent = cur_df
|
||||
cur_df.children.append(f)
|
||||
cur_df = f
|
||||
else:
|
||||
if cur_df == None:
|
||||
cls.tree.append(f)
|
||||
f.parent = None
|
||||
else:
|
||||
cur_df.children.append(f)
|
||||
f.parent = cur_df
|
||||
cls.files_by_pename[f.pe_name] = f
|
||||
ProfileTemplateRegistry.add(cls)
|
||||
|
||||
@classmethod
|
||||
def print_tree(cls):
|
||||
for c in cls.tree:
|
||||
c.print_tree()
|
||||
|
||||
@classmethod
|
||||
def base_df(cls) -> FileTemplate:
|
||||
"""Return the FileTemplate for the base DF of the given template. This may be a DF or ADF
|
||||
within this template, or refer to another template (e.g. mandatory USIM if we are optional USIM."""
|
||||
if cls.extends:
|
||||
return cls.extends.base_df
|
||||
return cls.files[0]
|
||||
|
||||
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.3.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'], ppath=[0x5f3a]),
|
||||
]
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
df_pb_files += [
|
||||
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi'], ppath=[0x5f3a]),
|
||||
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
|
||||
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
|
||||
]
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
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'], ppath=[0x5f3a]))
|
||||
|
||||
# Section 9.4 v2.3.1
|
||||
class FilesTelecom(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_TELECOM
|
||||
base_path = Path('MF')
|
||||
files = [
|
||||
FileTemplate(0x7f10, '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'], ppath=[0x5f50]),
|
||||
# EF.IIDF below
|
||||
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
|
||||
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
|
||||
# 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'], ppath=[0x5f50]))
|
||||
for i in range(0x80, 0xC0):
|
||||
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'], ppath=[0x5f50]))
|
||||
|
||||
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
|
||||
df_pb = deepcopy(df_pb_files)
|
||||
files += df_pb
|
||||
|
||||
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], ppath=[0x5f3b]),
|
||||
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
|
||||
|
||||
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
|
||||
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
|
||||
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.4
|
||||
class FilesTelecomV2(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_TELECOM_v2
|
||||
base_path = Path('MF')
|
||||
files = [
|
||||
FileTemplate(0x7f10, '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'], ppath=[0x5f50]),
|
||||
# EF.IIDF below
|
||||
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
|
||||
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
|
||||
# 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'], ppath=[0x5f50]))
|
||||
for i in range(0x80, 0xC0):
|
||||
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'],ppath=[0x5f50]))
|
||||
|
||||
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
|
||||
df_pb = deepcopy(df_pb_files)
|
||||
files += df_pb
|
||||
|
||||
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], ppath=[0x5f3b]),
|
||||
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
|
||||
|
||||
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
|
||||
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
|
||||
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
|
||||
|
||||
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}, ppath=[0x5f3d]),
|
||||
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
|
||||
|
||||
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], ppath=[0x5f3e]),
|
||||
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]),
|
||||
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # VST: 2
|
||||
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # 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
|
||||
optional = True
|
||||
oid = OID.ADF_USIMopt_not_by_default
|
||||
base_path = Path('ADF.USIM')
|
||||
extends = FilesUsimMandatory
|
||||
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', False, ass_serv=[20], repeat=True),
|
||||
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000', False, ass_serv=[42], repeat=True),
|
||||
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43], repeat=True),
|
||||
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
|
||||
optional = True
|
||||
oid = OID.ADF_USIMopt_not_by_default_v2
|
||||
base_path = Path('ADF.USIM')
|
||||
extends = FilesUsimMandatoryV2
|
||||
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.2.3 v3.3.1
|
||||
class FilesUsimOptionalV3(ProfileTemplate):
|
||||
created_by_default = False
|
||||
optional = True
|
||||
oid = OID.ADF_USIMopt_not_by_default_v3
|
||||
base_path = Path('ADF.USIM')
|
||||
extends = FilesUsimMandatoryV2
|
||||
files = FilesUsimOptionalV2.files + [
|
||||
FileTemplate(0x6f01, 'EF.eAKA', 'TR', None, 1, 3, None, None, True, ['size'], ass_serv=[134]),
|
||||
]
|
||||
|
||||
# Section 9.5.3
|
||||
class FilesUsimDfPhonebook(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_PHONEBOOK_ADF_USIM
|
||||
base_path = Path('ADF.USIM')
|
||||
files = df_pb_files
|
||||
|
||||
|
||||
# Section 9.5.4
|
||||
class FilesUsimDfGsmAccess(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_GSM_ACCESS_ADF_USIM
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
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
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
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
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
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
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
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.11.4
|
||||
class FilesUsimDf5GSv4(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS_v4
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
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, 'F0FF0000', 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]),
|
||||
FileTemplate(0x4f0d, 'EF.CAG', 'TR', None, 2, 2, 0x0d, None, True, ass_serv=[137]),
|
||||
FileTemplate(0x4f0e, 'EF.SOR_CMCI', 'TR', None, None, 2, 0x0e, None, True, ass_serv=[138]),
|
||||
FileTemplate(0x4f0f, 'EF.DRI', 'TR', None, 7, 2, 0x0f, None, True, ass_serv=[150]),
|
||||
FileTemplate(0x4f10, 'EF.5GSEDRX', 'TR', None, 2, 2, 0x10, None, True, ass_serv=[141]),
|
||||
FileTemplate(0x4f11, 'EF.5GNSWO_CONF', 'TR', None, 1, 2, 0x11, None, True, ass_serv=[142]),
|
||||
FileTemplate(0x4f15, 'EF.MCHPPLMN', 'TR', None, 1, 2, 0x15, None, True, ass_serv=[144]),
|
||||
FileTemplate(0x4f16, 'EF.KAUSF_DERIVATION', 'TR', None, 1, 2, 0x16, None, True, ass_serv=[145]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.12
|
||||
class FilesUsimDfSaip(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_SAIP
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
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.5.13
|
||||
class FilesDfSnpn(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_SNPN
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
files = [
|
||||
FileTemplate(0x5fe0, 'DF.SNPN', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[143], pe_name='df-df-snpn'),
|
||||
FileTemplate(0x4f01, 'EF.PWS_SNPN', 'TR', None, 1, 10, None, None, True, ass_serv=[143]),
|
||||
]
|
||||
|
||||
# Section 9.5.14
|
||||
class FilesDf5GProSe(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GProSe
|
||||
base_path = Path('ADF.USIM')
|
||||
parent = FilesUsimMandatory
|
||||
files = [
|
||||
FileTemplate(0x5ff0, 'DF.5G_ProSe', 'DF', None, None, 14, None, None, False, ['pinStatusTeimplateDO'], ass_serv=[139], pe_name='df-df-5g-prose'),
|
||||
FileTemplate(0x4f01, 'EF.5G_PROSE_ST', 'TR', None, 1, 2, 0x01, None, True, ass_serv=[139]),
|
||||
FileTemplate(0x4f02, 'EF.5G_PROSE_DD', 'TR', None, 26, 2, 0x02, None, True, ass_serv=[139,1001]),
|
||||
FileTemplate(0x4f03, 'EF.5G_PROSE_DC', 'TR', None, 12, 2, 0x03, None, True, ass_serv=[139,1002]),
|
||||
FileTemplate(0x4f04, 'EF.5G_PROSE_U2NRU', 'TR', None, 32, 2, 0x04, None, True, ass_serv=[139,1003]),
|
||||
FileTemplate(0x4f05, 'EF.5G_PROSE_RU', 'TR', None, 29, 2, 0x05, None, True, ass_serv=[139,1004]),
|
||||
FileTemplate(0x4f06, 'EF.5G_PROSE_UIR', 'TR', None, 32, 2, 0x06, None, True, ass_serv=[139,1005]),
|
||||
]
|
||||
|
||||
# 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
|
||||
optional = True
|
||||
oid = OID.ADF_ISIMopt_not_by_default
|
||||
base_path = Path('ADF.ISIM')
|
||||
extends = FilesIsimMandatory
|
||||
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
|
||||
optional = True
|
||||
oid = OID.ADF_ISIMopt_not_by_default_v2
|
||||
base_path = Path('ADF.ISIM')
|
||||
extends = FilesIsimMandatory
|
||||
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'],
|
||||
}
|
||||
|
||||
class SaipSpecVersionMeta(type):
|
||||
def __getitem__(self, ver: str):
|
||||
"""Syntactic sugar so that SaipSpecVersion['2.3.0'] will work."""
|
||||
return SaipSpecVersion.for_version(ver)
|
||||
|
||||
class SaipSpecVersion(object, metaclass=SaipSpecVersionMeta):
|
||||
"""Represents a specific version of the SIMalliance / TCA eUICC Profile Package:
|
||||
Interoperable Format Technical Specification."""
|
||||
version = None
|
||||
oids = []
|
||||
|
||||
@classmethod
|
||||
def suports_template_OID(cls, OID: OID.OID) -> bool:
|
||||
"""Return if a given spec version supports a template of given OID."""
|
||||
return OID in cls.oids
|
||||
|
||||
@classmethod
|
||||
def version_match(cls, ver: str) -> bool:
|
||||
"""Check if the given version-string matches the classes version. trailing zeroes are ignored,
|
||||
so that for example 2.2.0 will be considered equal to 2.2"""
|
||||
def strip_trailing_zeroes(l: List):
|
||||
while l[-1] == '0':
|
||||
l.pop()
|
||||
cls_ver_l = cls.version.split('.')
|
||||
strip_trailing_zeroes(cls_ver_l)
|
||||
ver_l = ver.split('.')
|
||||
strip_trailing_zeroes(ver_l)
|
||||
return cls_ver_l == ver_l
|
||||
|
||||
@staticmethod
|
||||
def for_version(req_version: str) -> Optional['SaipSpecVersion']:
|
||||
"""Return the subclass for the requested version number string."""
|
||||
for cls in all_subclasses(SaipSpecVersion):
|
||||
if cls.version_match(req_version):
|
||||
return cls
|
||||
|
||||
|
||||
class SaipSpecVersion101(SaipSpecVersion):
|
||||
version = '1.0.1'
|
||||
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM, OID.ADF_USIM_by_default, OID.ADF_USIMopt_not_by_default,
|
||||
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.ADF_ISIM_by_default,
|
||||
OID.ADF_ISIMopt_not_by_default, OID.ADF_CSIM_by_default, OID.ADF_CSIMopt_not_by_default]
|
||||
|
||||
class SaipSpecVersion20(SaipSpecVersion):
|
||||
version = '2.0'
|
||||
# no changes in filesystem teplates to previous 1.0.1
|
||||
oids = SaipSpecVersion101.oids
|
||||
|
||||
class SaipSpecVersion21(SaipSpecVersion):
|
||||
version = '2.1'
|
||||
# no changes in filesystem teplates to previous 2.0
|
||||
oids = SaipSpecVersion20.oids
|
||||
|
||||
class SaipSpecVersion22(SaipSpecVersion):
|
||||
version = '2.2'
|
||||
oids = SaipSpecVersion21.oids + [OID.DF_EAP]
|
||||
|
||||
class SaipSpecVersion23(SaipSpecVersion):
|
||||
version = '2.3'
|
||||
oids = SaipSpecVersion22.oids + [OID.DF_5GS, OID.DF_SAIP]
|
||||
|
||||
class SaipSpecVersion231(SaipSpecVersion):
|
||||
version = '2.3.1'
|
||||
# no changes in filesystem teplates to previous 2.3
|
||||
oids = SaipSpecVersion23.oids
|
||||
|
||||
class SaipSpecVersion31(SaipSpecVersion):
|
||||
version = '3.1'
|
||||
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM_v2, OID.ADF_USIM_by_default_v2, OID.ADF_USIMopt_not_by_default_v2,
|
||||
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.DF_5GS_v2, OID.DF_5GS_v3, OID.DF_SAIP,
|
||||
OID.ADF_ISIM_by_default, OID.ADF_ISIMopt_not_by_default_v2, OID.ADF_CSIM_by_default_v2,
|
||||
OID.ADF_CSIMopt_not_by_default_v2, OID.DF_EAP]
|
||||
|
||||
class SaipSpecVersion32(SaipSpecVersion):
|
||||
version = '3.2'
|
||||
# no changes in filesystem teplates to previous 3.1
|
||||
oids = SaipSpecVersion31.oids
|
||||
|
||||
class SaipSpecVersion331(SaipSpecVersion):
|
||||
version = '3.3.1'
|
||||
oids = SaipSpecVersion32.oids + [OID.ADF_USIMopt_not_by_default_v3, OID.DF_5GS_v4, OID.DF_SAIP, OID.DF_SNPN, OID.DF_5GProSe, OID.IoT_by_default, OID.IoTopt_not_by_default]
|
||||
@@ -92,5 +92,43 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
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 not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
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')
|
||||
|
||||
def check_identification_unique(self, pes: ProfileElementSequence):
|
||||
"""Ensure that each PE has a unique identification value."""
|
||||
id_list = [pe.header['identification'] for pe in pes.pe_list if pe.header]
|
||||
if len(id_list) != len(set(id_list)):
|
||||
raise ProfileError('PE identification values are not unique')
|
||||
|
||||
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
210
pySim/esim/x509_cert.py
Normal 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
|
||||
202
pySim/euicc.py
202
pySim/euicc.py
@@ -1,9 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Various definitions related to GSMA eSIM / eUICC
|
||||
Various definitions related to GSMA consumer + IoT eSIM / eUICC
|
||||
|
||||
Related Specs: GSMA SGP.22, GSMA SGP.02, etc.
|
||||
Does *not* implement anything related to M2M eUICC
|
||||
|
||||
Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
|
||||
"""
|
||||
|
||||
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
|
||||
@@ -21,17 +23,56 @@ Related Specs: GSMA SGP.22, GSMA SGP.02, etc.
|
||||
# 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.tlv import *
|
||||
from pySim.construct import *
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
import argparse
|
||||
|
||||
from construct import Array, Struct, FlagsEnum, GreedyRange
|
||||
from cmd2 import cmd2, CommandSet, with_default_category
|
||||
from osmocom.utils import Hexstr
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardADF, CardApplication
|
||||
from pySim.utils import Hexstr, SwHexstr
|
||||
import pySim.global_platform
|
||||
|
||||
# SGP.02 Section 2.2.2
|
||||
class Sgp02Eid(BER_TLV_IE, tag=0x5a):
|
||||
_construct = BcdAdapter(GreedyBytes)
|
||||
|
||||
# patch this into global_platform, to allow 'get_data sgp02_eid' in EF.ECASD
|
||||
pySim.global_platform.DataCollection.possible_nested.append(Sgp02Eid)
|
||||
|
||||
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."""
|
||||
|
||||
@@ -49,15 +90,6 @@ AID_ECASD = "A0000005591010FFFFFFFF8900000200"
|
||||
AID_ISD_P_FILE = "A0000005591010FFFFFFFF8900000D00"
|
||||
AID_ISD_P_MODULE = "A0000005591010FFFFFFFF8900000E00"
|
||||
|
||||
sw_isdr = {
|
||||
'ISD-R': {
|
||||
'6a80': 'Incorrect values in command data',
|
||||
'6a82': 'Profile not found',
|
||||
'6a88': 'Reference data not found',
|
||||
'6985': 'Conditions of use not satisfied',
|
||||
}
|
||||
}
|
||||
|
||||
class SupportedVersionNumber(BER_TLV_IE, tag=0x82):
|
||||
_construct = GreedyBytes
|
||||
|
||||
@@ -144,7 +176,7 @@ class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
|
||||
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
|
||||
pass
|
||||
class SeqNumber(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyInteger()
|
||||
_construct = Asn1DerInteger()
|
||||
class NotificationAddress(BER_TLV_IE, tag=0x0c):
|
||||
_construct = Utf8Adapter(GreedyBytes)
|
||||
class Iccid(BER_TLV_IE, tag=0x5a):
|
||||
@@ -258,7 +290,7 @@ 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):
|
||||
class GetCertsError(BER_TLV_IE, tag=0x81):
|
||||
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
|
||||
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
|
||||
pass
|
||||
@@ -271,9 +303,9 @@ class EimFqdn(BER_TLV_IE, tag=0x81):
|
||||
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
|
||||
_construct = Asn1DerInteger()
|
||||
class AssociationToken(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyInteger
|
||||
_construct = Asn1DerInteger()
|
||||
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
|
||||
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
|
||||
eimProprietary=4)
|
||||
@@ -286,21 +318,24 @@ class EimConfigurationDataSeq(BER_TLV_IE, tag=0xa0, nested=[EimConfigurationData
|
||||
class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
|
||||
pass
|
||||
|
||||
class ADF_ISDR(CardADF):
|
||||
def __init__(self, aid=AID_ISD_R, name='ADF.ISD-R', fid=None, sfid=None,
|
||||
desc='ISD-R (Issuer Security Domain Root) Application'):
|
||||
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
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()]
|
||||
# we attempt to retrieve ISD-R key material from CardKeyProvider identified by EID
|
||||
self.adf.scp_key_identity = 'EID'
|
||||
|
||||
@staticmethod
|
||||
def store_data(scc: SimCardCommands, tx_do: Hexstr) -> Tuple[Hexstr, SwHexstr]:
|
||||
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._tp.send_apdu_checksw(capdu)
|
||||
capdu = '80E29100%02x%s00' % (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='9000'):
|
||||
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:
|
||||
@@ -310,7 +345,7 @@ class ADF_ISDR(CardADF):
|
||||
return ValueError('DO > 255 bytes not supported yet')
|
||||
else:
|
||||
cmd_do_enc = b''
|
||||
(data, sw) = ADF_ISDR.store_data(scc, b2h(cmd_do_enc))
|
||||
(data, _sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc), exp_sw=exp_sw)
|
||||
if data:
|
||||
if resp_cls:
|
||||
resp_do = resp_cls()
|
||||
@@ -321,6 +356,13 @@ class ADF_ISDR(CardADF):
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_eid(scc: SimCardCommands) -> str:
|
||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||
ged = CardApplicationISDR.store_data_tlv(scc, ged_cmd, GetEuiccData)
|
||||
d = ged.to_dict()
|
||||
return flatten_dict_lists(d['get_euicc_data'])['eid_value']
|
||||
|
||||
def decode_select_response(self, data_hex: Hexstr) -> object:
|
||||
t = FciTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
@@ -336,11 +378,11 @@ class ADF_ISDR(CardADF):
|
||||
@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) = ADF_ISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
|
||||
(_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
|
||||
|
||||
def do_get_euicc_configured_addresses(self, opts):
|
||||
def do_get_euicc_configured_addresses(self, _opts):
|
||||
"""Perform an ES10a GetEuiccConfiguredAddresses function."""
|
||||
eca = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
|
||||
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']))
|
||||
|
||||
@@ -351,31 +393,31 @@ class ADF_ISDR(CardADF):
|
||||
def do_set_default_dp_address(self, opts):
|
||||
"""Perform an ES10a SetDefaultDpAddress function."""
|
||||
sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
|
||||
sdda = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
|
||||
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):
|
||||
def do_get_euicc_challenge(self, _opts):
|
||||
"""Perform an ES10b GetEUICCChallenge function."""
|
||||
gec = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetEuiccChallenge(), GetEuiccChallenge)
|
||||
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):
|
||||
def do_get_euicc_info1(self, _opts):
|
||||
"""Perform an ES10b GetEUICCInfo (1) function."""
|
||||
ei1 = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
|
||||
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):
|
||||
def do_get_euicc_info2(self, _opts):
|
||||
"""Perform an ES10b GetEUICCInfo (2) function."""
|
||||
ei2 = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
|
||||
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):
|
||||
def do_list_notification(self, _opts):
|
||||
"""Perform an ES10b ListNotification function."""
|
||||
ln = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
||||
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']))
|
||||
|
||||
@@ -386,13 +428,13 @@ class ADF_ISDR(CardADF):
|
||||
def do_remove_notification_from_list(self, opts):
|
||||
"""Perform an ES10b RemoveNotificationFromList function."""
|
||||
rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
|
||||
rn = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
|
||||
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):
|
||||
def do_get_profiles_info(self, _opts):
|
||||
"""Perform an ES10c GetProfilesInfo function."""
|
||||
pi = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
|
||||
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']))
|
||||
|
||||
@@ -407,11 +449,14 @@ class ADF_ISDR(CardADF):
|
||||
"""Perform an ES10c EnableProfile function."""
|
||||
if opts.isdp_aid:
|
||||
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
|
||||
if opts.iccid:
|
||||
elif opts.iccid:
|
||||
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
|
||||
else:
|
||||
# this is guaranteed by argparse; but we need this to make pylint happy
|
||||
raise ValueError('Either ISD-P AID or ICCID must be given')
|
||||
ep_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
|
||||
ep = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
|
||||
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']))
|
||||
|
||||
@@ -426,11 +471,14 @@ class ADF_ISDR(CardADF):
|
||||
"""Perform an ES10c DisableProfile function."""
|
||||
if opts.isdp_aid:
|
||||
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
|
||||
if opts.iccid:
|
||||
elif opts.iccid:
|
||||
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
|
||||
else:
|
||||
# this is guaranteed by argparse; but we need this to make pylint happy
|
||||
raise ValueError('Either ISD-P AID or ICCID must be given')
|
||||
dp_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
|
||||
dp = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
|
||||
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']))
|
||||
|
||||
@@ -444,26 +492,28 @@ class ADF_ISDR(CardADF):
|
||||
"""Perform an ES10c DeleteProfile function."""
|
||||
if opts.isdp_aid:
|
||||
p_id = IsdpAid(decoded=opts.isdp_aid)
|
||||
if opts.iccid:
|
||||
elif opts.iccid:
|
||||
p_id = Iccid(decoded=opts.iccid)
|
||||
else:
|
||||
# this is guaranteed by argparse; but we need this to make pylint happy
|
||||
raise ValueError('Either ISD-P AID or ICCID must be given')
|
||||
dp_cmd_contents = [p_id]
|
||||
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
|
||||
dp = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
|
||||
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):
|
||||
def do_get_eid(self, _opts):
|
||||
"""Perform an ES10c GetEID function."""
|
||||
(data, sw) = ADF_ISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
|
||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||
ged = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
|
||||
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')
|
||||
set_nickname_parser.add_argument('ICCID', help='ICCID of the profile whose nickname to set')
|
||||
|
||||
@cmd2.with_argparser(set_nickname_parser)
|
||||
def do_set_nickname(self, opts):
|
||||
@@ -471,46 +521,38 @@ class ADF_ISDR(CardADF):
|
||||
nickname = opts.profile_nickname or ''
|
||||
sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
|
||||
sn_cmd = SetNicknameReq(children=sn_cmd_contents)
|
||||
sn = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
|
||||
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):
|
||||
def do_get_certs(self, _opts):
|
||||
"""Perform an ES10c GetCerts() function on an IoT eUICC."""
|
||||
gc = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
||||
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']))
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_certs_resp']))
|
||||
|
||||
def do_get_eim_configuration_data(self, opts):
|
||||
def do_get_eim_configuration_data(self, _opts):
|
||||
"""Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
|
||||
gec = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
||||
GetEimConfigurationData)
|
||||
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 ADF_ECASD(CardADF):
|
||||
def __init__(self, aid=AID_ECASD, name='ADF.ECASD', fid=None, sfid=None,
|
||||
desc='ECASD (eUICC Controlling Authority Security Domain) Application'):
|
||||
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
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()]
|
||||
# we attempt to retrieve ECASD key material from CardKeyProvider identified by EID
|
||||
self.adf.scp_key_identity = 'EID'
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class CardApplicationISDR(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__('ISD-R', adf=ADF_ISDR(), sw=sw_isdr)
|
||||
|
||||
class CardApplicationECASD(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__('ECASD', adf=ADF_ECASD(), sw=sw_isdr)
|
||||
|
||||
@@ -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):
|
||||
@@ -52,9 +49,18 @@ class SwMatchError(Exception):
|
||||
self.sw_expected = sw_expected
|
||||
self.rs = rs
|
||||
|
||||
def __str__(self):
|
||||
@property
|
||||
def description(self):
|
||||
if self.rs and self.rs.lchan[0]:
|
||||
r = self.rs.lchan[0].interpret_sw(self.sw_actual)
|
||||
if r:
|
||||
return "SW match failed! Expected %s and got %s: %s - %s" % (self.sw_expected, self.sw_actual, r[0], r[1])
|
||||
return "SW match failed! Expected %s and got %s." % (self.sw_expected, self.sw_actual)
|
||||
return "%s - %s" % (r[0], r[1])
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
description = self.description
|
||||
if description:
|
||||
description = ": " + description
|
||||
else:
|
||||
description = "."
|
||||
return "SW match failed! Expected %s and got %s%s" % (self.sw_expected, self.sw_actual, description)
|
||||
|
||||
@@ -9,7 +9,7 @@ The classes are intended to represent the *specification* of the filesystem,
|
||||
not the actual contents / runtime state of interacting with a given smart card.
|
||||
"""
|
||||
|
||||
# (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
|
||||
@@ -24,25 +24,25 @@ not the actual contents / runtime state of interacting with a given smart card.
|
||||
# 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 code
|
||||
from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
|
||||
import argparse
|
||||
import tempfile
|
||||
import json
|
||||
import abc
|
||||
import inspect
|
||||
|
||||
import cmd2
|
||||
from cmd2 import CommandSet, with_default_category, with_argparser
|
||||
import argparse
|
||||
|
||||
from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
|
||||
|
||||
from cmd2 import CommandSet, with_default_category
|
||||
from smartcard.util import toBytes
|
||||
|
||||
from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, Hexstr
|
||||
from pySim.construct import filter_dict, parse_construct, build_construct
|
||||
from pySim.exceptions import *
|
||||
from pySim.jsonpath import js_path_find, js_path_modify
|
||||
from osmocom.utils import h2b, b2h, is_hex, auto_int, auto_uint8, auto_uint16, is_hexstr, JsonEncoder
|
||||
from osmocom.tlv import bertlv_parse_one
|
||||
from osmocom.construct import filter_dict, parse_construct, build_construct
|
||||
|
||||
from pySim.utils import sw_match
|
||||
from pySim.jsonpath import js_path_modify
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.exceptions import SwMatchError
|
||||
|
||||
# int: a single service is associated with this file
|
||||
# list: any of the listed services requires this file
|
||||
@@ -65,13 +65,13 @@ class CardFile:
|
||||
Args:
|
||||
fid : File Identifier (4 hex digits)
|
||||
sfid : Short File Identifier (2 hex digits, optional)
|
||||
name : Brief name of the file, lik EF_ICCID
|
||||
name : Brief name of the file, like EF_ICCID
|
||||
desc : Description of the file
|
||||
parent : Parent CardFile object within filesystem hierarchy
|
||||
profile : Card profile that this file should be part of
|
||||
service : Service (SST/UST/IST) associated with the file
|
||||
"""
|
||||
if not isinstance(self, CardADF) and fid == None:
|
||||
if not isinstance(self, CardADF) and fid is None:
|
||||
raise ValueError("fid is mandatory")
|
||||
if fid:
|
||||
fid = fid.lower()
|
||||
@@ -136,6 +136,7 @@ class CardFile:
|
||||
return ret
|
||||
|
||||
def build_select_path_to(self, target: 'CardFile') -> Optional[List['CardFile']]:
|
||||
"""Build the relative sequence of files we need to traverse to get from us to 'target'."""
|
||||
|
||||
# special-case handling for applications. Applications may be selected
|
||||
# any time from any location. If there is an ADF somewhere in the path,
|
||||
@@ -146,7 +147,6 @@ class CardFile:
|
||||
return inter_path[i:]
|
||||
return inter_path
|
||||
|
||||
"""Build the relative sequence of files we need to traverse to get from us to 'target'."""
|
||||
# special-case handling for selecting MF while the MF is selected
|
||||
if target == target.get_mf():
|
||||
return [target]
|
||||
@@ -167,7 +167,7 @@ class CardFile:
|
||||
|
||||
def get_mf(self) -> Optional['CardMF']:
|
||||
"""Return the MF (root) of the file system."""
|
||||
if self.parent == None:
|
||||
if self.parent is None:
|
||||
return None
|
||||
# iterate towards the top. MF has parent == self
|
||||
node = self
|
||||
@@ -179,7 +179,7 @@ class CardFile:
|
||||
"""Return a dict of {'identifier': self} tuples.
|
||||
|
||||
Args:
|
||||
alias : Add an alias with given name to 'self'
|
||||
alias : Add an alias with given name to 'self'
|
||||
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
|
||||
If not specified, all selectables will be returned.
|
||||
Returns:
|
||||
@@ -282,23 +282,33 @@ class CardFile:
|
||||
"""Assuming the provided list of activated services, should this file exist and be activated?."""
|
||||
if self.service is None:
|
||||
return None
|
||||
elif isinstance(self.service, int):
|
||||
if isinstance(self.service, int):
|
||||
# a single service determines the result
|
||||
return self.service in services
|
||||
elif isinstance(self.service, list):
|
||||
if isinstance(self.service, list):
|
||||
# any of the services active -> true
|
||||
for s in self.service:
|
||||
if s in services:
|
||||
return True
|
||||
return False
|
||||
elif isinstance(self.service, tuple):
|
||||
if isinstance(self.service, tuple):
|
||||
# all of the services active -> true
|
||||
for s in self.service:
|
||||
if not s in services:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
raise ValueError("self.service must be either int or list or tuple")
|
||||
raise ValueError("self.service must be either int or list or tuple")
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
"""
|
||||
Export file contents in the form of commandline script. This method is meant to be overloaded by a subclass in
|
||||
case any exportable contents are present. The generated script may contain multiple command lines separated by
|
||||
line breaks ("\n"), where the last commandline shall have no line break at the end
|
||||
(e.g. "update_record 1 112233\nupdate_record 1 445566"). Naturally this export method will always refer to the
|
||||
currently selected file of the presented lchan.
|
||||
"""
|
||||
return "# %s has no exportable contents" % str(lchan.selected_file)
|
||||
|
||||
|
||||
class CardDF(CardFile):
|
||||
@@ -306,15 +316,14 @@ class CardDF(CardFile):
|
||||
|
||||
@with_default_category('DF/ADF Commands')
|
||||
class ShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
pass
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not isinstance(self, CardADF):
|
||||
if not 'fid' in kwargs:
|
||||
if 'fid' not in kwargs:
|
||||
raise TypeError('fid is mandatory for all DF')
|
||||
super().__init__(**kwargs)
|
||||
self.children = dict()
|
||||
self.children = {}
|
||||
self.shell_commands = [self.ShellCommands()]
|
||||
# dict of CardFile affected by service(int), indexed by service
|
||||
self.files_by_service = {}
|
||||
@@ -415,7 +424,7 @@ class CardDF(CardFile):
|
||||
|
||||
def lookup_file_by_name(self, name: Optional[str]) -> Optional[CardFile]:
|
||||
"""Find a file with given name within current DF."""
|
||||
if name == None:
|
||||
if name is None:
|
||||
return None
|
||||
for i in self.children.values():
|
||||
if i.name and i.name == name:
|
||||
@@ -424,7 +433,7 @@ class CardDF(CardFile):
|
||||
|
||||
def lookup_file_by_sfid(self, sfid: Optional[str]) -> Optional[CardFile]:
|
||||
"""Find a file with given short file ID within current DF."""
|
||||
if sfid == None:
|
||||
if sfid is None:
|
||||
return None
|
||||
for i in self.children.values():
|
||||
if i.sfid == int(str(sfid)):
|
||||
@@ -449,7 +458,7 @@ class CardMF(CardDF):
|
||||
# cannot be overridden; use assignment
|
||||
kwargs['parent'] = self
|
||||
super().__init__(**kwargs)
|
||||
self.applications = dict()
|
||||
self.applications = {}
|
||||
|
||||
def __str__(self):
|
||||
return "MF(%s)" % (self.fid)
|
||||
@@ -518,7 +527,7 @@ class CardADF(CardDF):
|
||||
super().__init__(**kwargs)
|
||||
# reference to CardApplication may be set from CardApplication constructor
|
||||
self.application = None # type: Optional[CardApplication]
|
||||
self.aid = aid # Application Identifier
|
||||
self.aid = aid.lower() # Application Identifier
|
||||
self.has_fs = has_fs # Flag to tell whether the ADF supports a filesystem or not
|
||||
mf = self.get_mf()
|
||||
if mf:
|
||||
@@ -533,6 +542,15 @@ class CardADF(CardDF):
|
||||
else:
|
||||
return self.aid
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
"""
|
||||
Export application specific parameters that are not part of the UICC filesystem.
|
||||
"""
|
||||
if not isinstance(lchan.selected_file, CardADF):
|
||||
raise TypeError('currently selected file is not of type CardADF')
|
||||
return lchan.selected_file.application.export(as_json, lchan)
|
||||
|
||||
|
||||
class CardEF(CardFile):
|
||||
"""EF (Entry File) in the smart card filesystem"""
|
||||
@@ -573,13 +591,10 @@ class TransparentEF(CardEF):
|
||||
class ShellCommands(CommandSet):
|
||||
"""Shell commands specific for transparent EFs."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
dec_hex_parser = argparse.ArgumentParser()
|
||||
dec_hex_parser.add_argument('--oneline', action='store_true',
|
||||
help='No JSON pretty-printing, dump as a single line')
|
||||
dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
|
||||
dec_hex_parser.add_argument('HEXSTR', type=is_hexstr, help='Hex-string of encoded data to decode')
|
||||
|
||||
@cmd2.with_argparser(dec_hex_parser)
|
||||
def do_decode_hex(self, opts):
|
||||
@@ -589,14 +604,14 @@ class TransparentEF(CardEF):
|
||||
|
||||
read_bin_parser = argparse.ArgumentParser()
|
||||
read_bin_parser.add_argument(
|
||||
'--offset', type=int, default=0, help='Byte offset for start of read')
|
||||
'--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
|
||||
read_bin_parser.add_argument(
|
||||
'--length', type=int, help='Number of bytes to read')
|
||||
'--length', type=auto_uint16, help='Number of bytes to read')
|
||||
|
||||
@cmd2.with_argparser(read_bin_parser)
|
||||
def do_read_binary(self, opts):
|
||||
"""Read binary data from a transparent EF"""
|
||||
(data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
|
||||
(data, _sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
|
||||
self._cmd.poutput(data)
|
||||
|
||||
read_bin_dec_parser = argparse.ArgumentParser()
|
||||
@@ -606,44 +621,42 @@ class TransparentEF(CardEF):
|
||||
@cmd2.with_argparser(read_bin_dec_parser)
|
||||
def do_read_binary_decoded(self, opts):
|
||||
"""Read + decode data from a transparent EF"""
|
||||
(data, sw) = self._cmd.lchan.read_binary_dec()
|
||||
(data, _sw) = self._cmd.lchan.read_binary_dec()
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
upd_bin_parser = argparse.ArgumentParser()
|
||||
upd_bin_parser.add_argument(
|
||||
'--offset', type=int, default=0, help='Byte offset for start of read')
|
||||
upd_bin_parser.add_argument(
|
||||
'data', help='Data bytes (hex format) to write')
|
||||
'--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
|
||||
upd_bin_parser.add_argument('DATA', type=is_hexstr, help='Data bytes (hex format) to write')
|
||||
|
||||
@cmd2.with_argparser(upd_bin_parser)
|
||||
def do_update_binary(self, opts):
|
||||
"""Update (Write) data of a transparent EF"""
|
||||
(data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
|
||||
(data, _sw) = self._cmd.lchan.update_binary(opts.DATA, opts.offset)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
upd_bin_dec_parser = argparse.ArgumentParser()
|
||||
upd_bin_dec_parser.add_argument(
|
||||
'data', help='Abstract data (JSON format) to write')
|
||||
upd_bin_dec_parser.add_argument('--json-path', type=str,
|
||||
help='JSON path to modify specific element of file only')
|
||||
upd_bin_dec_parser.add_argument('DATA', help='Abstract data (JSON format) to write')
|
||||
|
||||
@cmd2.with_argparser(upd_bin_dec_parser)
|
||||
def do_update_binary_decoded(self, opts):
|
||||
"""Encode + Update (Write) data of a transparent EF"""
|
||||
if opts.json_path:
|
||||
(data_json, sw) = self._cmd.lchan.read_binary_dec()
|
||||
(data_json, _sw) = self._cmd.lchan.read_binary_dec()
|
||||
js_path_modify(data_json, opts.json_path,
|
||||
json.loads(opts.data))
|
||||
json.loads(opts.DATA))
|
||||
else:
|
||||
data_json = json.loads(opts.data)
|
||||
(data, sw) = self._cmd.lchan.update_binary_dec(data_json)
|
||||
data_json = json.loads(opts.DATA)
|
||||
(data, _sw) = self._cmd.lchan.update_binary_dec(data_json)
|
||||
if data:
|
||||
self._cmd.poutput_json(data)
|
||||
|
||||
def do_edit_binary_decoded(self, opts):
|
||||
def do_edit_binary_decoded(self, _opts):
|
||||
"""Edit the JSON representation of the EF contents in an editor."""
|
||||
(orig_json, sw) = self._cmd.lchan.read_binary_dec()
|
||||
(orig_json, _sw) = self._cmd.lchan.read_binary_dec()
|
||||
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
|
||||
filename = '%s/file' % dirname
|
||||
# write existing data as JSON to file
|
||||
@@ -656,7 +669,7 @@ class TransparentEF(CardEF):
|
||||
if edited_json == orig_json:
|
||||
self._cmd.poutput("Data not modified, skipping write")
|
||||
else:
|
||||
(data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
|
||||
(data, _sw) = self._cmd.lchan.update_binary_dec(edited_json)
|
||||
if data:
|
||||
self._cmd.poutput_json(data)
|
||||
|
||||
@@ -697,7 +710,7 @@ class TransparentEF(CardEF):
|
||||
return method(b2h(raw_bin_data))
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
elif self._tlv:
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -724,13 +737,32 @@ class TransparentEF(CardEF):
|
||||
return method(raw_bin_data)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
elif self._tlv:
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
return {'raw': raw_bin_data.hex()}
|
||||
|
||||
def encode_bin(self, abstract_data: dict) -> bytearray:
|
||||
def __get_size(self, total_len: Optional[int] = None) -> Optional[int]:
|
||||
"""Get the size (total length) of the file"""
|
||||
|
||||
# Caller has provided the actual total length of the file, this should be the default case
|
||||
if total_len is not None:
|
||||
return total_len
|
||||
|
||||
if self.size is None:
|
||||
return None
|
||||
|
||||
# Alternatively use the recommended size from the specification
|
||||
if self.size[1] is not None:
|
||||
return self.size[1]
|
||||
# In case no recommended size is specified, use the minimum size
|
||||
if self.size[0] is not None:
|
||||
return self.size[0]
|
||||
|
||||
return None
|
||||
|
||||
def encode_bin(self, abstract_data: dict, total_len: Optional[int] = None) -> bytearray:
|
||||
"""Encode abstract representation into raw (binary) data.
|
||||
|
||||
A derived class would typically provide an _encode_bin() or _encode_hex() method
|
||||
@@ -739,25 +771,26 @@ class TransparentEF(CardEF):
|
||||
|
||||
Args:
|
||||
abstract_data : dict representing the decoded data
|
||||
total_len : expected total length of the encoded data (file size)
|
||||
Returns:
|
||||
binary encoded data
|
||||
"""
|
||||
method = getattr(self, '_encode_bin', None)
|
||||
if callable(method):
|
||||
return method(abstract_data)
|
||||
return method(abstract_data, total_len = self.__get_size(total_len))
|
||||
method = getattr(self, '_encode_hex', None)
|
||||
if callable(method):
|
||||
return h2b(method(abstract_data))
|
||||
return h2b(method(abstract_data, total_len = self.__get_size(total_len)))
|
||||
if self._construct:
|
||||
return build_construct(self._construct, abstract_data)
|
||||
elif self._tlv:
|
||||
return build_construct(self._construct, abstract_data, {'total_len' : self.__get_size(total_len)})
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return t.to_tlv()
|
||||
raise NotImplementedError(
|
||||
"%s encoder not yet implemented. Patches welcome." % self)
|
||||
|
||||
def encode_hex(self, abstract_data: dict) -> str:
|
||||
def encode_hex(self, abstract_data: dict, total_len: Optional[int] = None) -> str:
|
||||
"""Encode abstract representation into raw (hex string) data.
|
||||
|
||||
A derived class would typically provide an _encode_bin() or _encode_hex() method
|
||||
@@ -766,25 +799,45 @@ class TransparentEF(CardEF):
|
||||
|
||||
Args:
|
||||
abstract_data : dict representing the decoded data
|
||||
total_len : expected total length of the encoded data (file size)
|
||||
Returns:
|
||||
hex string encoded data
|
||||
"""
|
||||
method = getattr(self, '_encode_hex', None)
|
||||
if callable(method):
|
||||
return method(abstract_data)
|
||||
return method(abstract_data, total_len = self.__get_size(total_len))
|
||||
method = getattr(self, '_encode_bin', None)
|
||||
if callable(method):
|
||||
raw_bin_data = method(abstract_data)
|
||||
raw_bin_data = method(abstract_data, total_len = self.__get_size(total_len))
|
||||
return b2h(raw_bin_data)
|
||||
if self._construct:
|
||||
return b2h(build_construct(self._construct, abstract_data))
|
||||
elif self._tlv:
|
||||
return b2h(build_construct(self._construct, abstract_data, {'total_len':self.__get_size(total_len)}))
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return b2h(t.to_tlv())
|
||||
raise NotImplementedError(
|
||||
"%s encoder not yet implemented. Patches welcome." % self)
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
"""
|
||||
Export the file contents of a TransparentEF. This method returns a shell command string (See also ShellCommand
|
||||
definition in this class) that can be used to write the file contents back.
|
||||
"""
|
||||
|
||||
if lchan.selected_file_structure() != 'transparent':
|
||||
raise ValueError("selected file has structure type '%s', expecting a file with structure 'transparent'" %
|
||||
lchan.selected_file_structure())
|
||||
export_str = ""
|
||||
if as_json:
|
||||
result = lchan.read_binary_dec()
|
||||
export_str += ("update_binary_decoded '%s'\n" % json.dumps(result[0], cls=JsonEncoder))
|
||||
else:
|
||||
result = lchan.read_binary()
|
||||
export_str += ("update_binary %s\n" % str(result[0]))
|
||||
return export_str.strip()
|
||||
|
||||
|
||||
class LinFixedEF(CardEF):
|
||||
"""Linear Fixed EF (Entry File) in the smart card filesystem.
|
||||
@@ -795,14 +848,10 @@ class LinFixedEF(CardEF):
|
||||
@with_default_category('Linear Fixed EF Commands')
|
||||
class ShellCommands(CommandSet):
|
||||
"""Shell commands specific for Linear Fixed EFs."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
dec_hex_parser = argparse.ArgumentParser()
|
||||
dec_hex_parser.add_argument('--oneline', action='store_true',
|
||||
help='No JSON pretty-printing, dump as a single line')
|
||||
dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
|
||||
dec_hex_parser.add_argument('HEXSTR', type=is_hexstr, help='Hex-string of encoded data to decode')
|
||||
|
||||
@cmd2.with_argparser(dec_hex_parser)
|
||||
def do_decode_hex(self, opts):
|
||||
@@ -812,43 +861,43 @@ class LinFixedEF(CardEF):
|
||||
|
||||
read_rec_parser = argparse.ArgumentParser()
|
||||
read_rec_parser.add_argument(
|
||||
'record_nr', type=int, help='Number of record to be read')
|
||||
'--count', type=auto_uint8, default=1, help='Number of records to be read, beginning at record_nr')
|
||||
read_rec_parser.add_argument(
|
||||
'--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
|
||||
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
|
||||
|
||||
@cmd2.with_argparser(read_rec_parser)
|
||||
def do_read_record(self, opts):
|
||||
"""Read one or multiple records from a record-oriented EF"""
|
||||
for r in range(opts.count):
|
||||
recnr = opts.record_nr + r
|
||||
(data, sw) = self._cmd.lchan.read_record(recnr)
|
||||
if (len(data) > 0):
|
||||
recnr = opts.RECORD_NR + r
|
||||
(data, _sw) = self._cmd.lchan.read_record(recnr)
|
||||
if len(data) > 0:
|
||||
recstr = str(data)
|
||||
else:
|
||||
recstr = "(empty)"
|
||||
self._cmd.poutput("%03d %s" % (recnr, recstr))
|
||||
|
||||
read_rec_dec_parser = argparse.ArgumentParser()
|
||||
read_rec_dec_parser.add_argument(
|
||||
'record_nr', type=int, help='Number of record to be read')
|
||||
read_rec_dec_parser.add_argument('--oneline', action='store_true',
|
||||
help='No JSON pretty-printing, dump as a single line')
|
||||
read_rec_dec_parser.add_argument(
|
||||
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
|
||||
|
||||
@cmd2.with_argparser(read_rec_dec_parser)
|
||||
def do_read_record_decoded(self, opts):
|
||||
"""Read + decode a record from a record-oriented EF"""
|
||||
(data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
read_recs_parser = argparse.ArgumentParser()
|
||||
|
||||
@cmd2.with_argparser(read_recs_parser)
|
||||
def do_read_records(self, opts):
|
||||
def do_read_records(self, _opts):
|
||||
"""Read all records from a record-oriented EF"""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
for recnr in range(1, 1 + num_of_rec):
|
||||
(data, sw) = self._cmd.lchan.read_record(recnr)
|
||||
if (len(data) > 0):
|
||||
(data, _sw) = self._cmd.lchan.read_record(recnr)
|
||||
if len(data) > 0:
|
||||
recstr = str(data)
|
||||
else:
|
||||
recstr = "(empty)"
|
||||
@@ -865,53 +914,51 @@ class LinFixedEF(CardEF):
|
||||
# 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_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
upd_rec_parser = argparse.ArgumentParser()
|
||||
upd_rec_parser.add_argument(
|
||||
'record_nr', type=int, help='Number of record to be read')
|
||||
upd_rec_parser.add_argument(
|
||||
'data', help='Data bytes (hex format) to write')
|
||||
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
|
||||
upd_rec_parser.add_argument('DATA', type=is_hexstr, help='Data bytes (hex format) to write')
|
||||
|
||||
@cmd2.with_argparser(upd_rec_parser)
|
||||
def do_update_record(self, opts):
|
||||
"""Update (write) data to a record-oriented EF"""
|
||||
(data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
|
||||
(data, _sw) = self._cmd.lchan.update_record(opts.RECORD_NR, opts.DATA)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
upd_rec_dec_parser = argparse.ArgumentParser()
|
||||
upd_rec_dec_parser.add_argument(
|
||||
'record_nr', type=int, help='Number of record to be read')
|
||||
upd_rec_dec_parser.add_argument(
|
||||
'data', help='Abstract data (JSON format) to write')
|
||||
upd_rec_dec_parser.add_argument('--json-path', type=str,
|
||||
help='JSON path to modify specific element of record only')
|
||||
upd_rec_dec_parser.add_argument(
|
||||
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
|
||||
upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
|
||||
|
||||
@cmd2.with_argparser(upd_rec_dec_parser)
|
||||
def do_update_record_decoded(self, opts):
|
||||
"""Encode + Update (write) data to a record-oriented EF"""
|
||||
if opts.json_path:
|
||||
(data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
(data_json, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
||||
js_path_modify(data_json, opts.json_path,
|
||||
json.loads(opts.data))
|
||||
else:
|
||||
data_json = json.loads(opts.data)
|
||||
(data, sw) = self._cmd.lchan.update_record_dec(
|
||||
opts.record_nr, data_json)
|
||||
(data, _sw) = self._cmd.lchan.update_record_dec(
|
||||
opts.RECORD_NR, data_json)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
edit_rec_dec_parser = argparse.ArgumentParser()
|
||||
edit_rec_dec_parser.add_argument(
|
||||
'record_nr', type=int, help='Number of record to be edited')
|
||||
'RECORD_NR', type=auto_uint8, help='Number of record to be edited')
|
||||
|
||||
@cmd2.with_argparser(edit_rec_dec_parser)
|
||||
def do_edit_record_decoded(self, opts):
|
||||
"""Edit the JSON representation of one record in an editor."""
|
||||
(orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
(orig_json, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
||||
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
|
||||
filename = '%s/file' % dirname
|
||||
# write existing data as JSON to file
|
||||
@@ -924,8 +971,8 @@ class LinFixedEF(CardEF):
|
||||
if edited_json == orig_json:
|
||||
self._cmd.poutput("Data not modified, skipping write")
|
||||
else:
|
||||
(data, sw) = self._cmd.lchan.update_record_dec(
|
||||
opts.record_nr, edited_json)
|
||||
(data, _sw) = self._cmd.lchan.update_record_dec(
|
||||
opts.RECORD_NR, edited_json)
|
||||
if data:
|
||||
self._cmd.poutput_json(data)
|
||||
|
||||
@@ -970,7 +1017,7 @@ class LinFixedEF(CardEF):
|
||||
return method(raw_bin_data, record_nr=record_nr)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
elif self._tlv:
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -998,13 +1045,32 @@ class LinFixedEF(CardEF):
|
||||
return method(raw_hex_data, record_nr=record_nr)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
elif self._tlv:
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
return {'raw': raw_hex_data}
|
||||
|
||||
def encode_record_hex(self, abstract_data: dict, record_nr: int) -> str:
|
||||
def __get_rec_len(self, total_len: Optional[int] = None) -> Optional[int]:
|
||||
"""Get the length (total length) of the file record"""
|
||||
|
||||
# Caller has provided the actual total length of the record, this should be the default case
|
||||
if total_len is not None:
|
||||
return total_len
|
||||
|
||||
if self.rec_len is None:
|
||||
return None
|
||||
|
||||
# Alternatively use the recommended length from the specification
|
||||
if self.rec_len[1] is not None:
|
||||
return self.rec_len[1]
|
||||
# In case no recommended length is specified, use the minimum length
|
||||
if self.rec_len[0] is not None:
|
||||
return self.rec_len[0]
|
||||
|
||||
return None
|
||||
|
||||
def encode_record_hex(self, abstract_data: dict, record_nr: int, total_len: Optional[int] = None) -> str:
|
||||
"""Encode abstract representation into raw (hex string) data.
|
||||
|
||||
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
|
||||
@@ -1014,26 +1080,27 @@ class LinFixedEF(CardEF):
|
||||
Args:
|
||||
abstract_data : dict representing the decoded data
|
||||
record_nr : record number (1 for first record, ...)
|
||||
total_len : expected total length of the encoded data (record length)
|
||||
Returns:
|
||||
hex string encoded data
|
||||
"""
|
||||
method = getattr(self, '_encode_record_hex', None)
|
||||
if callable(method):
|
||||
return method(abstract_data, record_nr=record_nr)
|
||||
return method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len))
|
||||
method = getattr(self, '_encode_record_bin', None)
|
||||
if callable(method):
|
||||
raw_bin_data = method(abstract_data, record_nr=record_nr)
|
||||
raw_bin_data = method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len))
|
||||
return b2h(raw_bin_data)
|
||||
if self._construct:
|
||||
return b2h(build_construct(self._construct, abstract_data))
|
||||
elif self._tlv:
|
||||
return b2h(build_construct(self._construct, abstract_data, {'total_len':self.__get_rec_len(total_len)}))
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return b2h(t.to_tlv())
|
||||
raise NotImplementedError(
|
||||
"%s encoder not yet implemented. Patches welcome." % self)
|
||||
|
||||
def encode_record_bin(self, abstract_data: dict, record_nr : int) -> bytearray:
|
||||
def encode_record_bin(self, abstract_data: dict, record_nr : int, total_len: Optional[int] = None) -> bytearray:
|
||||
"""Encode abstract representation into raw (binary) data.
|
||||
|
||||
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
|
||||
@@ -1043,24 +1110,73 @@ class LinFixedEF(CardEF):
|
||||
Args:
|
||||
abstract_data : dict representing the decoded data
|
||||
record_nr : record number (1 for first record, ...)
|
||||
total_len : expected total length of the encoded data (record length)
|
||||
Returns:
|
||||
binary encoded data
|
||||
"""
|
||||
method = getattr(self, '_encode_record_bin', None)
|
||||
if callable(method):
|
||||
return method(abstract_data, record_nr=record_nr)
|
||||
return method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len))
|
||||
method = getattr(self, '_encode_record_hex', None)
|
||||
if callable(method):
|
||||
return h2b(method(abstract_data, record_nr=record_nr))
|
||||
return h2b(method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len)))
|
||||
if self._construct:
|
||||
return build_construct(self._construct, abstract_data)
|
||||
elif self._tlv:
|
||||
return build_construct(self._construct, abstract_data, {'total_len':self.__get_rec_len(total_len)})
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return t.to_tlv()
|
||||
raise NotImplementedError(
|
||||
"%s encoder not yet implemented. Patches welcome." % self)
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
"""
|
||||
Export the file contents of a LinFixedEF (or a CyclicEF). This method returns a shell command string (See also
|
||||
ShellCommand definition in this class) that can be used to write the file contents back.
|
||||
"""
|
||||
|
||||
# A CyclicEF is a subclass of LinFixedEF.
|
||||
if lchan.selected_file_structure() != 'linear_fixed' and lchan.selected_file_structure() != 'cyclic':
|
||||
raise ValueError("selected file has structure type '%s', expecting a file with structure 'linear_fixed' or 'cyclic'" %
|
||||
lchan.selected_file_structure())
|
||||
|
||||
export_str = ""
|
||||
|
||||
# Use number of records specified in select response
|
||||
num_of_rec = lchan.selected_file_num_of_rec()
|
||||
if num_of_rec:
|
||||
for r in range(1, num_of_rec + 1):
|
||||
if as_json:
|
||||
result = lchan.read_record_dec(r)
|
||||
export_str += ("update_record_decoded %d '%s'\n" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
||||
else:
|
||||
result = lchan.read_record(r)
|
||||
export_str += ("update_record %d %s\n" % (r, str(result[0])))
|
||||
|
||||
# In case the select response does not return the number of records, read until we hit the first record that
|
||||
# cannot be read.
|
||||
else:
|
||||
r = 1
|
||||
while True:
|
||||
try:
|
||||
if as_json:
|
||||
result = lchan.read_record_dec(r)
|
||||
export_str += ("update_record_decoded %d '%s'\n" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
||||
else:
|
||||
result = lchan.read_record(r)
|
||||
export_str += ("update_record %d %s\n" % (r, str(result[0])))
|
||||
except SwMatchError as e:
|
||||
# We are past the last valid record - stop
|
||||
if e.sw_actual == "9402":
|
||||
break
|
||||
# Some other problem occurred
|
||||
else:
|
||||
raise e
|
||||
r = r + 1
|
||||
|
||||
return export_str.strip()
|
||||
|
||||
|
||||
class CyclicEF(LinFixedEF):
|
||||
"""Cyclic EF (Entry File) in the smart card filesystem"""
|
||||
@@ -1117,7 +1233,7 @@ class TransRecEF(TransparentEF):
|
||||
return method(raw_bin_data)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
elif self._tlv:
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -1144,13 +1260,26 @@ class TransRecEF(TransparentEF):
|
||||
return method(raw_hex_data)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
elif self._tlv:
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
return {'raw': raw_hex_data}
|
||||
|
||||
def encode_record_hex(self, abstract_data: dict) -> str:
|
||||
def __get_rec_len(self, total_len: Optional[int] = None) -> Optional[int]:
|
||||
"""Get the length (total length) of the file record"""
|
||||
|
||||
# Caller has provided the actual total length of the record, this should be the default case
|
||||
if total_len is not None:
|
||||
return total_len
|
||||
|
||||
# Alternatively use the record length from the specification
|
||||
if self.rec_len:
|
||||
return self.rec_len
|
||||
|
||||
return None
|
||||
|
||||
def encode_record_hex(self, abstract_data: dict, total_len: Optional[int] = None) -> str:
|
||||
"""Encode abstract representation into raw (hex string) data.
|
||||
|
||||
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
|
||||
@@ -1159,25 +1288,27 @@ class TransRecEF(TransparentEF):
|
||||
|
||||
Args:
|
||||
abstract_data : dict representing the decoded data
|
||||
total_len : expected total length of the encoded data (record length)
|
||||
Returns:
|
||||
hex string encoded data
|
||||
"""
|
||||
method = getattr(self, '_encode_record_hex', None)
|
||||
if callable(method):
|
||||
return method(abstract_data)
|
||||
return method(abstract_data, total_len = self.__get_rec_len(total_len))
|
||||
method = getattr(self, '_encode_record_bin', None)
|
||||
if callable(method):
|
||||
return b2h(method(abstract_data))
|
||||
return b2h(method(abstract_data, total_len = self.__get_rec_len(total_len)))
|
||||
if self._construct:
|
||||
return b2h(filter_dict(build_construct(self._construct, abstract_data)))
|
||||
elif self._tlv:
|
||||
return b2h(filter_dict(build_construct(self._construct, abstract_data,
|
||||
{'total_len':self.__get_rec_len(total_len)})))
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return b2h(t.to_tlv())
|
||||
raise NotImplementedError(
|
||||
"%s encoder not yet implemented. Patches welcome." % self)
|
||||
|
||||
def encode_record_bin(self, abstract_data: dict) -> bytearray:
|
||||
def encode_record_bin(self, abstract_data: dict, total_len: Optional[int] = None) -> bytearray:
|
||||
"""Encode abstract representation into raw (binary) data.
|
||||
|
||||
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
|
||||
@@ -1186,18 +1317,20 @@ class TransRecEF(TransparentEF):
|
||||
|
||||
Args:
|
||||
abstract_data : dict representing the decoded data
|
||||
total_len : expected total length of the encoded data (record length)
|
||||
Returns:
|
||||
binary encoded data
|
||||
"""
|
||||
method = getattr(self, '_encode_record_bin', None)
|
||||
if callable(method):
|
||||
return method(abstract_data)
|
||||
return method(abstract_data, total_len = self.__get_rec_len(total_len))
|
||||
method = getattr(self, '_encode_record_hex', None)
|
||||
if callable(method):
|
||||
return h2b(method(abstract_data))
|
||||
return h2b(method(abstract_data, total_len = self.__get_rec_len(total_len)))
|
||||
if self._construct:
|
||||
return filter_dict(build_construct(self._construct, abstract_data))
|
||||
elif self._tlv:
|
||||
return filter_dict(build_construct(self._construct, abstract_data,
|
||||
{'total_len':self.__get_rec_len(total_len)}))
|
||||
if self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return t.to_tlv()
|
||||
@@ -1209,8 +1342,8 @@ class TransRecEF(TransparentEF):
|
||||
for i in range(0, len(raw_bin_data), self.rec_len)]
|
||||
return [self.decode_record_bin(x) for x in chunks]
|
||||
|
||||
def _encode_bin(self, abstract_data) -> bytes:
|
||||
chunks = [self.encode_record_bin(x) for x in abstract_data]
|
||||
def _encode_bin(self, abstract_data, **kwargs) -> bytes:
|
||||
chunks = [self.encode_record_bin(x, total_len = kwargs.get('total_len', None)) for x in abstract_data]
|
||||
# FIXME: pad to file size
|
||||
return b''.join(chunks)
|
||||
|
||||
@@ -1227,48 +1360,50 @@ class BerTlvEF(CardEF):
|
||||
class ShellCommands(CommandSet):
|
||||
"""Shell commands specific for BER-TLV EFs."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
retrieve_data_parser = argparse.ArgumentParser()
|
||||
retrieve_data_parser.add_argument(
|
||||
'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
|
||||
'TAG', type=auto_int, help='BER-TLV Tag of value to retrieve')
|
||||
|
||||
@cmd2.with_argparser(retrieve_data_parser)
|
||||
def do_retrieve_data(self, opts):
|
||||
"""Retrieve (Read) data from a BER-TLV EF"""
|
||||
(data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
|
||||
(data, _sw) = self._cmd.lchan.retrieve_data(opts.TAG)
|
||||
self._cmd.poutput(data)
|
||||
|
||||
def do_retrieve_tags(self, opts):
|
||||
def do_retrieve_tags(self, _opts):
|
||||
"""List tags available in a given BER-TLV EF"""
|
||||
tags = self._cmd.lchan.retrieve_tags()
|
||||
self._cmd.poutput(tags)
|
||||
|
||||
set_data_parser = argparse.ArgumentParser()
|
||||
set_data_parser.add_argument(
|
||||
'tag', type=auto_int, help='BER-TLV Tag of value to set')
|
||||
set_data_parser.add_argument(
|
||||
'data', help='Data bytes (hex format) to write')
|
||||
'TAG', type=auto_int, help='BER-TLV Tag of value to set')
|
||||
set_data_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
|
||||
|
||||
@cmd2.with_argparser(set_data_parser)
|
||||
def do_set_data(self, opts):
|
||||
"""Set (Write) data for a given tag in a BER-TLV EF"""
|
||||
(data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
|
||||
(data, _sw) = self._cmd.lchan.set_data(opts.TAG, opts.data)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
del_data_parser = argparse.ArgumentParser()
|
||||
del_data_parser.add_argument(
|
||||
'tag', type=auto_int, help='BER-TLV Tag of value to set')
|
||||
'TAG', type=auto_int, help='BER-TLV Tag of value to set')
|
||||
|
||||
@cmd2.with_argparser(del_data_parser)
|
||||
def do_delete_data(self, opts):
|
||||
"""Delete data for a given tag in a BER-TLV EF"""
|
||||
(data, sw) = self._cmd.lchan.set_data(opts.tag, None)
|
||||
"""Delete data for a given tag in a BER-TLV EF"""
|
||||
(data, _sw) = self._cmd.lchan.set_data(opts.TAG, None)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
def do_delete_all(self, opts):
|
||||
"""Delete all data from a BER-TLV EF"""
|
||||
tags = self._cmd.lchan.retrieve_tags()
|
||||
for tag in tags:
|
||||
self._cmd.lchan.set_data(tag, None)
|
||||
|
||||
def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
|
||||
size: Size = (1, None), **kwargs):
|
||||
"""
|
||||
@@ -1285,6 +1420,34 @@ class BerTlvEF(CardEF):
|
||||
self.size = size
|
||||
self.shell_commands = [self.ShellCommands()]
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
"""
|
||||
Export the file contents of a BerTlvEF. This method returns a shell command string (See also ShellCommand
|
||||
definition in this class) that can be used to write the file contents back.
|
||||
"""
|
||||
|
||||
if lchan.selected_file_structure() != 'ber_tlv':
|
||||
raise ValueError("selected file has structure type '%s', expecting a file with structure 'ber_tlv'" %
|
||||
lchan.selected_file_structure())
|
||||
|
||||
# TODO: Add JSON output as soon as we have a set_data_decoded command and a retrieve_data_dec method.
|
||||
if as_json:
|
||||
raise NotImplementedError("BerTlvEF encoder not yet implemented. Patches welcome.")
|
||||
|
||||
export_str = ""
|
||||
tags = lchan.retrieve_tags()
|
||||
if tags == []:
|
||||
export_str += "# empty file, no tags"
|
||||
else:
|
||||
export_str += "delete_all\n"
|
||||
for t in tags:
|
||||
result = lchan.retrieve_data(t)
|
||||
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
|
||||
export_str += ("set_data 0x%02x %s\n" % (t, b2h(val)))
|
||||
return export_str.strip()
|
||||
|
||||
|
||||
def interpret_sw(sw_data: dict, sw: str):
|
||||
"""Interpret a given status word.
|
||||
|
||||
@@ -1315,9 +1478,11 @@ class CardApplication:
|
||||
adf : ADF name
|
||||
sw : Dict of status word conversions
|
||||
"""
|
||||
if aid:
|
||||
aid = aid.lower()
|
||||
self.name = name
|
||||
self.adf = adf
|
||||
self.sw = sw or dict()
|
||||
self.sw = sw or {}
|
||||
# back-reference from ADF to Applicaiton
|
||||
if self.adf:
|
||||
self.aid = aid or self.adf.aid
|
||||
@@ -1339,6 +1504,15 @@ class CardApplication:
|
||||
"""
|
||||
return interpret_sw(self.sw, sw)
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
"""
|
||||
Export application specific parameters, in the form of commandline script. (see also comment in the export
|
||||
method of class "CardFile")
|
||||
"""
|
||||
return "# %s has no exportable features" % str(lchan.selected_file)
|
||||
|
||||
|
||||
|
||||
class CardModel(abc.ABC):
|
||||
"""A specific card model, typically having some additional vendor-specific files. All
|
||||
@@ -1370,3 +1544,53 @@ class CardModel(abc.ABC):
|
||||
for m in CardModel.__subclasses__():
|
||||
if m.match(scc):
|
||||
m.add_files(rs)
|
||||
|
||||
|
||||
class Path:
|
||||
"""Representation of a file-system path."""
|
||||
def __init__(self, p: Union[str, List[str], List[int]]):
|
||||
# split if given as single string with slahes
|
||||
if isinstance(p, str):
|
||||
p = p.split('/')
|
||||
elif len(p) and isinstance(p[0], int):
|
||||
p = ['%04x' % x for x in p]
|
||||
# make sure internal representation alwas is uppercase only
|
||||
self.list = [x.upper() for x in p]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '/'.join(self.list)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Path(%s)' % (str(self))
|
||||
|
||||
def __eq__(self, other: 'Path') -> bool:
|
||||
return self.list == other.list
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.list[i]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.list)
|
||||
|
||||
def __add__(self, a):
|
||||
if isinstance(a, list):
|
||||
l = self.list + a
|
||||
elif isinstance(a, Path):
|
||||
l = self.list + a.list
|
||||
else:
|
||||
l = self.list + [a]
|
||||
return Path(l)
|
||||
|
||||
def relative_to_mf(self) -> 'Path':
|
||||
"""Return a path relative to MF, i.e. without initial explicit MF."""
|
||||
if len(self.list) and self.list[0] in ['MF', '3F00']:
|
||||
return Path(self.list[1:])
|
||||
return self
|
||||
|
||||
def is_parent(self, other: 'Path') -> bool:
|
||||
"""Is this instance a parent of the given other instance?"""
|
||||
if len(self.list) >= len(other.list):
|
||||
return False
|
||||
if other.list[:len(self.list)] == self.list:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
# coding=utf-8
|
||||
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
|
||||
|
||||
(C) 2022-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 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,
|
||||
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.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
|
||||
|
||||
# GlobalPlatform v2.3.1 Section H.4
|
||||
class ScpInformation(BER_TLV_IE, tag=0xa0):
|
||||
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
|
||||
class CiphersForLFDBEncryption(BER_TLV_IE, tag=0x84):
|
||||
pass
|
||||
class CiphersForTokens(BER_TLV_IE, tag=0x85):
|
||||
pass
|
||||
class CiphersForReceipts(BER_TLV_IE, tag=0x86):
|
||||
pass
|
||||
class CiphersForDAPs(BER_TLV_IE, tag=0x87):
|
||||
pass
|
||||
class KeyParameterReferenceList(BER_TLV_IE, tag=0x88):
|
||||
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 = 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,
|
||||
# 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'])
|
||||
|
||||
# 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__()
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
954
pySim/global_platform/__init__.py
Normal file
954
pySim/global_platform/__init__.py
Normal file
@@ -0,0 +1,954 @@
|
||||
# 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/>.
|
||||
"""
|
||||
|
||||
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 osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.card_key_provider import card_key_provider_get_field
|
||||
from pySim.global_platform.scp import SCP02, SCP03
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class VolatileMinMemoryReq(BER_TLV_IE, tag=0xC7):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class NonVolatileDataMinMemoryReq(BER_TLV_IE, tag=0xC8):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# GPCS Table 11-49: Install Parameter Tags
|
||||
class GlobalServiceParams(BER_TLV_IE, tag=0xCB):
|
||||
pass
|
||||
|
||||
# GPCS Table 11-49: Install Parameter Tags
|
||||
class VolatileReservedMemory(BER_TLV_IE, tag=0xD7):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# GPCS Table 11-49: Install Parameter Tags
|
||||
class NonVolatileReservedMemory(BER_TLV_IE, tag=0xD8):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# GPCS Table 11-49: Install Parameter Tags
|
||||
class Ts102226SpecificParameter(BER_TLV_IE, tag=0xCA):
|
||||
_construct = SimFileAccessAndToolkitAppSpecParams
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class LoadFileDataBlockFormatId(BER_TLV_IE, tag=0xCD):
|
||||
pass
|
||||
|
||||
# GPCS Table 11-50: Make Selectable Parameter Tags
|
||||
class ImplicitSelectionParam(BER_TLV_IE, tag=0xCF):
|
||||
pass
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class LoadFileDtaBlockParameters(BER_TLV_IE, tag=0xDD):
|
||||
pass
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags / 11-49: Install Parameter Tags
|
||||
class SystemSpecificParams(BER_TLV_IE, tag=0xEF,
|
||||
nested=[NonVolatileCodeMinMemoryReq,
|
||||
VolatileMinMemoryReq,
|
||||
NonVolatileDataMinMemoryReq,
|
||||
GlobalServiceParams,
|
||||
VolatileReservedMemory,
|
||||
NonVolatileReservedMemory,
|
||||
Ts102226SpecificParameter,
|
||||
LoadFileDataBlockFormatId,
|
||||
ImplicitSelectionParam,
|
||||
LoadFileDtaBlockParameters]):
|
||||
pass
|
||||
|
||||
# GPCS Table 11-49: Install Parameter Tags
|
||||
class ApplicationSpecificParams(BER_TLV_IE, tag=0xC9):
|
||||
_construct = GreedyBytes
|
||||
|
||||
class Ts102226SpecificTemplate(BER_TLV_IE, tag=0xEA):
|
||||
pass
|
||||
|
||||
class CrtForDigitalSignature(BER_TLV_IE, tag=0xB6):
|
||||
# FIXME: nested
|
||||
pass
|
||||
|
||||
|
||||
class InstallParameters(TLV_IE_Collection, nested=[ApplicationSpecificParams,
|
||||
SystemSpecificParams,
|
||||
Ts102226SpecificTemplate,
|
||||
CrtForDigitalSignature]):
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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) + "00")
|
||||
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=list(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.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) + "00")
|
||||
return data
|
||||
|
||||
get_status_parser = argparse.ArgumentParser()
|
||||
get_status_parser.add_argument('subset', choices=list(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) + "00")
|
||||
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=list(SetStatusScope.ksymapping.values()),
|
||||
help='Defines the scope of the requested status change')
|
||||
set_status_parser.add_argument('status', choices=list(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=list(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))
|
||||
|
||||
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E6%02x%02x%02x%s00" % (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)
|
||||
|
||||
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('--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)')
|
||||
est_scp02_p_k = est_scp02_parser.add_argument_group('Manual key specification')
|
||||
est_scp02_p_k.add_argument('--key-enc', type=is_hexstr, help='Secure Channel Encryption Key')
|
||||
est_scp02_p_k.add_argument('--key-mac', type=is_hexstr, help='Secure Channel MAC Key')
|
||||
est_scp02_p_k.add_argument('--key-dek', type=is_hexstr, help='Data Encryption Key')
|
||||
est_scp02_p_csv = est_scp02_parser.add_argument_group('Obtain keys from CardKeyProvider (e.g. CSV')
|
||||
est_scp02_p_csv.add_argument('--key-provider-suffix', help='Suffix for key names in CardKeyProvider')
|
||||
|
||||
@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 opts.key_provider_suffix:
|
||||
suffix = opts.key_provider_suffix
|
||||
id_field_name = self._cmd.lchan.selected_adf.scp_key_identity
|
||||
identity = self._cmd.rs.identity.get(id_field_name)
|
||||
opts.key_enc = card_key_provider_get_field('SCP02_ENC_' + suffix, key=id_field_name, value=identity)
|
||||
opts.key_mac = card_key_provider_get_field('SCP02_MAC_' + suffix, key=id_field_name, value=identity)
|
||||
opts.key_dek = card_key_provider_get_field('SCP02_DEK_' + suffix, key=id_field_name, value=identity)
|
||||
else:
|
||||
if not opts.key_enc or not opts.key_mac:
|
||||
self._cmd.poutput("Cannot establish SCP02 without at least ENC and MAC keys given!")
|
||||
return
|
||||
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.description = None
|
||||
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 opts.key_provider_suffix:
|
||||
suffix = opts.key_provider_suffix
|
||||
id_field_name = self._cmd.lchan.selected_adf.scp_key_identity
|
||||
identity = self._cmd.rs.identity.get(id_field_name)
|
||||
opts.key_enc = card_key_provider_get_field('SCP03_ENC_' + suffix, key=id_field_name, value=identity)
|
||||
opts.key_mac = card_key_provider_get_field('SCP03_MAC_' + suffix, key=id_field_name, value=identity)
|
||||
opts.key_dek = card_key_provider_get_field('SCP03_DEK_' + suffix, key=id_field_name, value=identity)
|
||||
else:
|
||||
if not opts.key_enc or not opts.key_mac:
|
||||
self._cmd.poutput("Cannot establish SCP03 without at least ENC and MAC keys given!")
|
||||
return
|
||||
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)
|
||||
# the identity (e.g. 'ICCID', 'EID') that should be used as a look-up key to attempt to retrieve
|
||||
# the key material for the security domain from the CardKeyProvider
|
||||
self.adf.scp_key_identity = None
|
||||
|
||||
# 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')
|
||||
self.adf.scp_key_identity = 'ICCID'
|
||||
|
||||
#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]
|
||||
93
pySim/global_platform/http.py
Normal file
93
pySim/global_platform/http.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""GlobalPlatform Remote Application Management over HTTP Card Specification v2.3 - Amendment B.
|
||||
Also known as SCP81 for SIM/USIM/UICC/eUICC/eSIM OTA.
|
||||
"""
|
||||
|
||||
# (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 construct import Struct, Int8ub, Int16ub, Bytes, GreedyBytes, GreedyString, BytesInteger
|
||||
from construct import this, len_, Rebuild, Const
|
||||
from construct import Optional as COptional
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
from pySim import cat
|
||||
|
||||
|
||||
# Table 3-3 + Section 3.8.1
|
||||
class RasConnectionParams(BER_TLV_IE, tag=0x84, nested=cat.OpenChannel.nested_collection_cls.possible_nested):
|
||||
pass
|
||||
|
||||
# Table 3-3 + Section 3.8.2
|
||||
class SecurityParams(BER_TLV_IE, tag=0x85):
|
||||
_test_de_encode = [
|
||||
( '850804deadbeef020040', {'kid': 64,'kvn': 0, 'psk_id': b'\xde\xad\xbe\xef', 'sha_type': None} )
|
||||
]
|
||||
_construct = Struct('_psk_id_len'/Rebuild(Int8ub, len_(this.psk_id)), 'psk_id'/Bytes(this._psk_id_len),
|
||||
'_kid_kvn_len'/Const(2, Int8ub), 'kvn'/Int8ub, 'kid'/Int8ub,
|
||||
'sha_type'/COptional(Int8ub))
|
||||
|
||||
# Table 3-3 + ?
|
||||
class ExtendedSecurityParams(BER_TLV_IE, tag=0xA5):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# Table 3-3 + Section 3.8.3
|
||||
class SessionRetryPolicyParams(BER_TLV_IE, tag=0x86):
|
||||
_construct = Struct('retry_counter'/Int16ub,
|
||||
'retry_waiting_delay'/BytesInteger(5),
|
||||
'retry_report_failure'/COptional(GreedyBytes))
|
||||
|
||||
# Table 3-3 + Section 3.8.4
|
||||
class AdminHostParam(BER_TLV_IE, tag=0x8A):
|
||||
_test_de_encode = [
|
||||
( '8a0a61646d696e2e686f7374', 'admin.host' ),
|
||||
]
|
||||
_construct = GreedyString('utf-8')
|
||||
|
||||
# Table 3-3 + Section 3.8.5
|
||||
class AgentIdParam(BER_TLV_IE, tag=0x8B):
|
||||
_construct = GreedyString('utf-8')
|
||||
|
||||
# Table 3-3 + Section 3.8.6
|
||||
class AdminUriParam(BER_TLV_IE, tag=0x8C):
|
||||
_test_de_encode = [
|
||||
( '8c1668747470733a2f2f61646d696e2e686f73742f757269', 'https://admin.host/uri' ),
|
||||
]
|
||||
_construct = GreedyString('utf-8')
|
||||
|
||||
# Table 3-3
|
||||
class HttpPostParams(BER_TLV_IE, tag=0x89, nested=[AdminHostParam, AgentIdParam, AdminUriParam]):
|
||||
pass
|
||||
|
||||
# Table 3-3
|
||||
class AdmSessionParams(BER_TLV_IE, tag=0x83, nested=[RasConnectionParams, SecurityParams,
|
||||
ExtendedSecurityParams, SessionRetryPolicyParams,
|
||||
HttpPostParams]):
|
||||
pass
|
||||
|
||||
# Table 3-3 + Section 3.11.4
|
||||
class RasFqdn(BER_TLV_IE, tag=0xD6):
|
||||
_construct = GreedyBytes # FIXME: DNS String
|
||||
|
||||
# Table 3-3 + Section 3.11.7
|
||||
class DnsConnectionParams(BER_TLV_IE, tag=0xFA, nested=cat.OpenChannel.nested_collection_cls.possible_nested):
|
||||
pass
|
||||
|
||||
# Table 3-3
|
||||
class DnsResolutionParams(BER_TLV_IE, tag=0xB3, nested=[RasFqdn, DnsConnectionParams]):
|
||||
pass
|
||||
|
||||
# Table 3-3
|
||||
class AdmSessTriggerParams(BER_TLV_IE, tag=0x81, nested=[AdmSessionParams, DnsResolutionParams]):
|
||||
pass
|
||||
572
pySim/global_platform/scp.py
Normal file
572
pySim/global_platform/scp.py
Normal file
@@ -0,0 +1,572 @@
|
||||
# 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 osmocom.utils import b2h
|
||||
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
|
||||
from pySim.utils import parse_command_apdu
|
||||
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]))
|
||||
elif hasattr(self, 'kvn_ranges'):
|
||||
# pylint: disable=no-member
|
||||
if all([not card_keys.kvn in range(x[0], x[1]+1) for x in self.kvn_ranges]):
|
||||
raise ValueError('%s cannot be used with KVN outside permitted ranges %s' %
|
||||
(self.__class__.__name__, self.kvn_ranges))
|
||||
|
||||
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))
|
||||
# The 0x70 is a non-spec special-case of sysmoISIM-SJA2/SJA5 and possibly more sysmocom products
|
||||
kvn_ranges = [[0x20, 0x2f], [0x70, 0x70]]
|
||||
|
||||
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[:8], DES.MODE_ECB)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
||||
cipher = DES.new(self.card_keys.dek[:8], 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 + b'\x00'
|
||||
|
||||
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."""
|
||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
||||
|
||||
if not self.do_cmac:
|
||||
return apdu
|
||||
|
||||
(case, lc, le, data) = parse_command_apdu(apdu)
|
||||
|
||||
# TODO: add support for extended length fields.
|
||||
assert lc <= 256
|
||||
assert le <= 256
|
||||
lc &= 0xFF
|
||||
le &= 0xFF
|
||||
|
||||
# CLA without log. channel can be 80 or 00 only
|
||||
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"
|
||||
|
||||
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]) + data)
|
||||
if self.do_cenc:
|
||||
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
|
||||
data = k.encrypt(pad80(data, 8))
|
||||
lc = len(data)
|
||||
|
||||
lc += 8
|
||||
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
|
||||
|
||||
# Since we attach a signature, we will always send some data. This means that if the APDU is of case #4
|
||||
# or case #2, we must attach an additional Le byte to signal that we expect a response. It is technically
|
||||
# legal to use 0x00 (=256) as Le byte, even when the caller has specified a different value in the original
|
||||
# APDU. This is due to the fact that Le always describes the maximum expected length of the response
|
||||
# (see also ISO/IEC 7816-4, section 5.1). In addition to that, it should also important that depending on
|
||||
# the configuration of the SCP, the response may also contain a signature that makes the response larger
|
||||
# than specified in the Le field of the original APDU.
|
||||
if case == 4 or case == 2:
|
||||
apdu += b'\x00'
|
||||
|
||||
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 counter’s 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 + b'\x00'
|
||||
|
||||
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 SCP03: calculate MAC and encrypt."""
|
||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
||||
|
||||
if not self.do_cmac:
|
||||
return apdu
|
||||
|
||||
cla = apdu[0]
|
||||
ins = apdu[1]
|
||||
p1 = apdu[2]
|
||||
p2 = apdu[3]
|
||||
(case, lc, le, cmd_data) = parse_command_apdu(apdu)
|
||||
|
||||
# TODO: add support for extended length fields.
|
||||
assert lc <= 256
|
||||
assert le <= 256
|
||||
lc &= 0xFF
|
||||
le &= 0xFF
|
||||
|
||||
if self.do_cenc and not skip_cenc:
|
||||
if case <= 2:
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
|
||||
cmac = self.sk.calc_cmac(apdu)
|
||||
apdu += cmac[:self.s_mode]
|
||||
|
||||
# See comment in SCP03._wrap_cmd_apdu()
|
||||
if case == 4 or case == 2:
|
||||
apdu += b'\x00'
|
||||
|
||||
return apdu
|
||||
|
||||
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
|
||||
107
pySim/global_platform/uicc.py
Normal file
107
pySim/global_platform/uicc.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# coding=utf-8
|
||||
"""GlobalPLatform UICC Configuration 1.0 parameters
|
||||
|
||||
(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 construct import Optional as COptional
|
||||
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
|
||||
# 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
|
||||
|
||||
# UICC Configuration v1.0.1 / Section 4.3.2
|
||||
class UiccScp(BER_TLV_IE, tag=0x81):
|
||||
_construct = Struct('scp'/Int8ub, 'i'/Int8ub)
|
||||
|
||||
class AcceptExtradAppsAndElfToSd(BER_TLV_IE, tag=0x82):
|
||||
_construct = GreedyBytes
|
||||
|
||||
class AcceptDelOfAssocSd(BER_TLV_IE, tag=0x83):
|
||||
_construct = GreedyBytes
|
||||
|
||||
class LifeCycleTransitionToPersonalized(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
|
||||
class CasdCapabilityInformation(BER_TLV_IE, tag=0x86):
|
||||
_construct = GreedyBytes
|
||||
|
||||
class AcceptExtradAssocAppsAndElf(BER_TLV_IE, tag=0x87):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# Security Domain Install Parameters (inside C9 during INSTALL [for install])
|
||||
class UiccSdInstallParams(TLV_IE_Collection, nested=[UiccScp, AcceptExtradAppsAndElfToSd, AcceptDelOfAssocSd,
|
||||
LifeCycleTransitionToPersonalized,
|
||||
CasdCapabilityInformation, AcceptExtradAssocAppsAndElf]):
|
||||
def has_scp(self, scp: int) -> bool:
|
||||
"""Determine if SD Installation parameters already specify given SCP."""
|
||||
for c in self.children:
|
||||
if not isinstance(c, UiccScp):
|
||||
continue
|
||||
if c.decoded['scp'] == scp:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_scp(self, scp: int, i: int):
|
||||
"""Add given SCP (and i parameter) to list of SCP of the Security Domain Install Params.
|
||||
Example: add_scp(0x03, 0x70) for SCP03, or add_scp(0x02, 0x55) for SCP02."""
|
||||
if self.has_scp(scp):
|
||||
raise ValueError('SCP%02x already present' % scp)
|
||||
self.children.append(UiccScp(decoded={'scp': scp, 'i': i}))
|
||||
|
||||
def remove_scp(self, scp: int):
|
||||
"""Remove given SCP from list of SCP of the Security Domain Install Params."""
|
||||
for c in self.children:
|
||||
if not isinstance(c, UiccScp):
|
||||
continue
|
||||
if c.decoded['scp'] == scp:
|
||||
self.children.remove(c)
|
||||
return
|
||||
raise ValueError("SCP%02x not present" % scp)
|
||||
|
||||
|
||||
# Key Usage:
|
||||
# KVN 0x01 .. 0x0F reserved for SCP80
|
||||
# KVN 0x11 reserved for DAP specified in ETSI TS 102 226
|
||||
# KVN 0x20 .. 0x2F reserved for SCP02
|
||||
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
|
||||
# KVN 0x30 .. 0x3F reserved for SCP03
|
||||
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
|
||||
# KVN 0x70 KID 0x01: Token key (RSA public or DES)
|
||||
# KVN 0x71 KID 0x01: Receipt key (DES)
|
||||
# KVN 0x73 KID 0x01: DAP verifiation key (RS public or DES)
|
||||
# KVN 0x74 reserved for CASD
|
||||
# KID 0x01: PK.CA.AUT
|
||||
# KID 0x02: SK.CASD.AUT (PK) and KS.CASD.AUT (Non-PK)
|
||||
# KID 0x03: SK.CASD.CT (P) and KS.CASD.CT (Non-PK)
|
||||
# KVN 0x75 KID 0x01: 16-byte DES key for Ciphered Load File Data Block
|
||||
# KVN 0xFF reserved for ISD with SCP02 without SCP80 s upport
|
||||
214
pySim/gsmtap.py
214
pySim/gsmtap.py
@@ -1,214 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Osmocom GSMTAP python implementation.
|
||||
GSMTAP is a packet format used for conveying a number of different
|
||||
telecom-related protocol traces over UDP.
|
||||
"""
|
||||
|
||||
#
|
||||
# Copyright (C) 2022 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
|
||||
# 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 socket
|
||||
from typing import List, Dict, Optional
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
from pySim.construct import *
|
||||
|
||||
# The root definition of GSMTAP can be found at
|
||||
# https://cgit.osmocom.org/cgit/libosmocore/tree/include/osmocom/core/gsmtap.h
|
||||
|
||||
GSMTAP_UDP_PORT = 4729
|
||||
|
||||
# GSMTAP_TYPE_*
|
||||
gsmtap_type_construct = Enum(Int8ub,
|
||||
gsm_um = 0x01,
|
||||
gsm_abis = 0x02,
|
||||
gsm_um_burst = 0x03,
|
||||
sim = 0x04,
|
||||
tetra_i1 = 0x05,
|
||||
tetra_i1_burst = 0x06,
|
||||
wimax_burst = 0x07,
|
||||
gprs_gb_llc = 0x08,
|
||||
gprs_gb_sndcp = 0x09,
|
||||
gmr1_um = 0x0a,
|
||||
umts_rlc_mac = 0x0b,
|
||||
umts_rrc = 0x0c,
|
||||
lte_rrc = 0x0d,
|
||||
lte_mac = 0x0e,
|
||||
lte_mac_framed = 0x0f,
|
||||
osmocore_log = 0x10,
|
||||
qc_diag = 0x11,
|
||||
lte_nas = 0x12,
|
||||
e1_t1 = 0x13)
|
||||
|
||||
|
||||
# TYPE_UM_BURST
|
||||
gsmtap_subtype_burst_construct = Enum(Int8ub,
|
||||
unknown = 0x00,
|
||||
fcch = 0x01,
|
||||
partial_sch = 0x02,
|
||||
sch = 0x03,
|
||||
cts_sch = 0x04,
|
||||
compact_sch = 0x05,
|
||||
normal = 0x06,
|
||||
dummy = 0x07,
|
||||
access = 0x08,
|
||||
none = 0x09)
|
||||
|
||||
gsmtap_subtype_wimax_burst_construct = Enum(Int8ub,
|
||||
cdma_code = 0x10,
|
||||
fch = 0x11,
|
||||
ffb = 0x12,
|
||||
pdu = 0x13,
|
||||
hack = 0x14,
|
||||
phy_attributes = 0x15)
|
||||
|
||||
# GSMTAP_CHANNEL_*
|
||||
gsmtap_subtype_um_construct = Enum(Int8ub,
|
||||
unknown = 0x00,
|
||||
bcch = 0x01,
|
||||
ccch = 0x02,
|
||||
rach = 0x03,
|
||||
agch = 0x04,
|
||||
pch = 0x05,
|
||||
sdcch = 0x06,
|
||||
sdcch4 = 0x07,
|
||||
sdcch8 = 0x08,
|
||||
facch_f = 0x09,
|
||||
facch_h = 0x0a,
|
||||
pacch = 0x0b,
|
||||
cbch52 = 0x0c,
|
||||
pdtch = 0x0d,
|
||||
ptcch = 0x0e,
|
||||
cbch51 = 0x0f,
|
||||
voice_f = 0x10,
|
||||
voice_h = 0x11)
|
||||
|
||||
|
||||
# GSMTAP_SIM_*
|
||||
gsmtap_subtype_sim_construct = Enum(Int8ub,
|
||||
apdu = 0x00,
|
||||
atr = 0x01,
|
||||
pps_req = 0x02,
|
||||
pps_rsp = 0x03,
|
||||
tpdu_hdr = 0x04,
|
||||
tpdu_cmd = 0x05,
|
||||
tpdu_rsp = 0x06,
|
||||
tpdu_sw = 0x07)
|
||||
|
||||
gsmtap_subtype_tetra_construct = Enum(Int8ub,
|
||||
bsch = 0x01,
|
||||
aach = 0x02,
|
||||
sch_hu = 0x03,
|
||||
sch_hd = 0x04,
|
||||
sch_f = 0x05,
|
||||
bnch = 0x06,
|
||||
stch = 0x07,
|
||||
tch_f = 0x08,
|
||||
dmo_sch_s = 0x09,
|
||||
dmo_sch_h = 0x0a,
|
||||
dmo_sch_f = 0x0b,
|
||||
dmo_stch = 0x0c,
|
||||
dmo_tch = 0x0d)
|
||||
|
||||
gsmtap_subtype_gmr1_construct = Enum(Int8ub,
|
||||
unknown = 0x00,
|
||||
bcch = 0x01,
|
||||
ccch = 0x02,
|
||||
pch = 0x03,
|
||||
agch = 0x04,
|
||||
bach = 0x05,
|
||||
rach = 0x06,
|
||||
cbch = 0x07,
|
||||
sdcch = 0x08,
|
||||
tachh = 0x09,
|
||||
gbch = 0x0a,
|
||||
tch3 = 0x10,
|
||||
tch6 = 0x14,
|
||||
tch9 = 0x18)
|
||||
|
||||
gsmtap_subtype_e1t1_construct = Enum(Int8ub,
|
||||
lapd = 0x01,
|
||||
fr = 0x02,
|
||||
raw = 0x03,
|
||||
trau16 = 0x04,
|
||||
trau8 = 0x05)
|
||||
|
||||
gsmtap_arfcn_construct = BitStruct('pcs'/Flag, 'uplink'/Flag, 'arfcn'/BitsInteger(14))
|
||||
|
||||
gsmtap_hdr_construct = Struct('version'/Int8ub,
|
||||
'hdr_len'/Int8ub,
|
||||
'type'/gsmtap_type_construct,
|
||||
'timeslot'/Int8ub,
|
||||
'arfcn'/gsmtap_arfcn_construct,
|
||||
'signal_dbm'/Int8sb,
|
||||
'snr_db'/Int8sb,
|
||||
'frame_nr'/Int32ub,
|
||||
'sub_type'/Switch(this.type, {
|
||||
'gsm_um': gsmtap_subtype_um_construct,
|
||||
'gsm_um_burst': gsmtap_subtype_burst_construct,
|
||||
'sim': gsmtap_subtype_sim_construct,
|
||||
'tetra_i1': gsmtap_subtype_tetra_construct,
|
||||
'tetra_i1_burst': gsmtap_subtype_tetra_construct,
|
||||
'wimax_burst': gsmtap_subtype_wimax_burst_construct,
|
||||
'gmr1_um': gsmtap_subtype_gmr1_construct,
|
||||
'e1_t1': gsmtap_subtype_e1t1_construct,
|
||||
}),
|
||||
'antenna_nr'/Int8ub,
|
||||
'sub_slot'/Int8ub,
|
||||
'res'/Int8ub,
|
||||
'body'/GreedyBytes)
|
||||
|
||||
osmocore_log_ts_construct = Struct('sec'/Int32ub, 'usec'/Int32ub)
|
||||
osmocore_log_level_construct = Enum(Int8ub, debug=1, info=3, notice=5, error=7, fatal=8)
|
||||
gsmtap_osmocore_log_hdr_construct = Struct('ts'/osmocore_log_ts_construct,
|
||||
'proc_name'/PaddedString(16, 'ascii'),
|
||||
'pid'/Int32ub,
|
||||
'level'/osmocore_log_level_construct,
|
||||
Bytes(3),
|
||||
'subsys'/PaddedString(16, 'ascii'),
|
||||
'src_file'/Struct('name'/PaddedString(32, 'ascii'), 'line_nr'/Int32ub))
|
||||
|
||||
|
||||
class GsmtapMessage:
|
||||
"""Class whose objects represent a single GSMTAP message. Can encode and decode messages."""
|
||||
def __init__(self, encoded = None):
|
||||
self.encoded = encoded
|
||||
self.decoded = None
|
||||
|
||||
def decode(self):
|
||||
self.decoded = parse_construct(gsmtap_hdr_construct, self.encoded)
|
||||
return self.decoded
|
||||
|
||||
def encode(self, decoded):
|
||||
self.encoded = gsmtap_hdr_construct.build(decoded)
|
||||
return self.encoded
|
||||
|
||||
class GsmtapSource:
|
||||
def __init__(self, bind_ip:str='127.0.0.1', bind_port:int=4729):
|
||||
self.bind_ip = bind_ip
|
||||
self.bind_port = bind_port
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.bind((self.bind_ip, self.bind_port))
|
||||
|
||||
def read_packet(self) -> GsmtapMessage:
|
||||
data, addr = self.sock.recvfrom(1024)
|
||||
gsmtap_msg = GsmtapMessage(data)
|
||||
gsmtap_msg.decode()
|
||||
if gsmtap_msg.decoded['version'] != 0x02:
|
||||
raise ValueError('Unknown GSMTAP version 0x%02x' % gsmtap_msg.decoded['version'])
|
||||
return gsmtap_msg.decoded, addr
|
||||
@@ -17,11 +17,9 @@ 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 pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.tlv import *
|
||||
from construct import GreedyBytes, GreedyString
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
# Table 91 + Section 8.2.1.2
|
||||
class ApplicationId(BER_TLV_IE, tag=0x4f):
|
||||
|
||||
19
pySim/javacard.py
Normal file
19
pySim/javacard.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# JavaCard related utilities
|
||||
|
||||
import zipfile
|
||||
import struct
|
||||
import sys
|
||||
import io
|
||||
|
||||
def ijc_to_cap(in_file: io.IOBase, out_zip: zipfile.ZipFile, p : str = "foo"):
|
||||
"""Convert an ICJ (Interoperable Java Card) file [back] to a CAP file."""
|
||||
TAGS = ["Header", "Directory", "Applet", "Import", "ConstantPool", "Class", "Method", "StaticField", "RefLocation", "Export", "Descriptor", "Debug"]
|
||||
b = in_file.read()
|
||||
while len(b):
|
||||
tag, size = struct.unpack('!BH', b[0:3])
|
||||
out_zip.writestr(p+"/javacard/"+TAGS[tag-1]+".cap", b[0:3+size])
|
||||
b = b[3+size:]
|
||||
|
||||
# example usage:
|
||||
# with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
|
||||
# ijc_to_cap(f, z)
|
||||
@@ -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
|
||||
|
||||
@@ -7,16 +7,16 @@ from smartcard.util import toBytes
|
||||
from pytlv.TLV import *
|
||||
|
||||
from pySim.cards import SimCardBase, UiccCardBase
|
||||
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_msisdn, enc_msisdn
|
||||
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi
|
||||
from pySim.utils import enc_plmn, get_addr_type
|
||||
from pySim.utils import is_hex, h2b, b2h, h2s, s2h, lpad, rpad
|
||||
from pySim.legacy.utils import enc_ePDGSelection, format_xplmn_w_act, format_xplmn, dec_st, enc_st
|
||||
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv
|
||||
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv, dec_msisdn, enc_msisdn
|
||||
from pySim.legacy.ts_51_011 import EF, DF
|
||||
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
|
||||
from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
|
||||
|
||||
from pySim.ts_51_011 import EF_AD, EF_SPN
|
||||
from pySim.profile.ts_51_011 import EF_AD, EF_SPN
|
||||
|
||||
def format_addr(addr: str, addr_type: str) -> str:
|
||||
"""
|
||||
@@ -753,7 +753,7 @@ class GrcardSim(SimCard):
|
||||
|
||||
# Set the Ki using proprietary command
|
||||
pdu = '80d4020010' + p['ki']
|
||||
data, sw = self._scc._tp.send_apdu(pdu)
|
||||
data, sw = self._scc.send_apdu(pdu)
|
||||
|
||||
# EF.HPLMN
|
||||
r = self._scc.select_path(['3f00', '7f20', '6f30'])
|
||||
@@ -803,7 +803,7 @@ class SysmoUSIMgr1(UsimCard):
|
||||
# TODO: check if verify_chv could be used or what it needs
|
||||
# self._scc.verify_chv(0x0A, [0x33,0x32,0x32,0x31,0x33,0x32,0x33,0x32])
|
||||
# Unlock the card..
|
||||
data, sw = self._scc._tp.send_apdu_checksw(
|
||||
data, sw = self._scc.send_apdu_checksw(
|
||||
"0020000A083332323133323332")
|
||||
|
||||
# TODO: move into SimCardCommands
|
||||
@@ -812,7 +812,7 @@ class SysmoUSIMgr1(UsimCard):
|
||||
enc_iccid(p['iccid']) + # 10b ICCID
|
||||
enc_imsi(p['imsi']) # 9b IMSI_len + id_type(9) + IMSI
|
||||
)
|
||||
data, sw = self._scc._tp.send_apdu_checksw("0099000033" + par)
|
||||
data, sw = self._scc.send_apdu_checksw("0099000033" + par)
|
||||
|
||||
|
||||
class SysmoSIMgr2(SimCard):
|
||||
@@ -851,7 +851,7 @@ class SysmoSIMgr2(SimCard):
|
||||
pin = h2b("4444444444444444")
|
||||
|
||||
pdu = 'A0D43A0508' + b2h(pin)
|
||||
data, sw = self._scc._tp.send_apdu(pdu)
|
||||
data, sw = self._scc.send_apdu(pdu)
|
||||
|
||||
# authenticate as ADM (enough to write file, and can set PINs)
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import Optional, Tuple
|
||||
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
|
||||
from osmocom.utils import swap_nibbles, h2b, b2h
|
||||
|
||||
def hexstr_to_Nbytearr(s, nbytes):
|
||||
return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
|
||||
@@ -146,7 +148,7 @@ def dec_st(st, table="sim") -> str:
|
||||
from pySim.ts_31_102 import EF_UST_map
|
||||
lookup_map = EF_UST_map
|
||||
else:
|
||||
from pySim.ts_51_011 import EF_SST_map
|
||||
from pySim.profile.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)]
|
||||
@@ -330,3 +332,82 @@ def enc_addr_tlv(addr, addr_type='00'):
|
||||
s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def dec_msisdn(ef_msisdn: Hexstr) -> Optional[Tuple[int, int, Optional[str]]]:
|
||||
"""
|
||||
Decode MSISDN from EF.MSISDN or EF.ADN (same structure).
|
||||
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3.
|
||||
"""
|
||||
|
||||
# Convert from str to (kind of) 'bytes'
|
||||
ef_msisdn = h2b(ef_msisdn)
|
||||
|
||||
# Make sure mandatory fields are present
|
||||
if len(ef_msisdn) < 14:
|
||||
raise ValueError("EF.MSISDN is too short")
|
||||
|
||||
# Skip optional Alpha Identifier
|
||||
xlen = len(ef_msisdn) - 14
|
||||
msisdn_lhv = ef_msisdn[xlen:]
|
||||
|
||||
# Parse the length (in bytes) of the BCD encoded number
|
||||
bcd_len = msisdn_lhv[0]
|
||||
# BCD length = length of dial num (max. 10 bytes) + 1 byte ToN and NPI
|
||||
if bcd_len == 0xff:
|
||||
return None
|
||||
elif bcd_len > 11 or bcd_len < 1:
|
||||
raise ValueError(
|
||||
"Length of MSISDN (%d bytes) is out of range" % bcd_len)
|
||||
|
||||
# Parse ToN / NPI
|
||||
ton = (msisdn_lhv[1] >> 4) & 0x07
|
||||
npi = msisdn_lhv[1] & 0x0f
|
||||
bcd_len -= 1
|
||||
|
||||
# No MSISDN?
|
||||
if not bcd_len:
|
||||
return (npi, ton, None)
|
||||
|
||||
msisdn = swap_nibbles(b2h(msisdn_lhv[2:][:bcd_len])).rstrip('f')
|
||||
# International number 10.5.118/3GPP TS 24.008
|
||||
if ton == 0x01:
|
||||
msisdn = '+' + msisdn
|
||||
|
||||
return (npi, ton, msisdn)
|
||||
|
||||
|
||||
def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
|
||||
"""
|
||||
Encode MSISDN as LHV so it can be stored to EF.MSISDN.
|
||||
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3. (The result
|
||||
will not contain the optional Alpha Identifier at the beginning.)
|
||||
|
||||
Default NPI / ToN values:
|
||||
- NPI: ISDN / telephony numbering plan (E.164 / E.163),
|
||||
- ToN: network specific or international number (if starts with '+').
|
||||
"""
|
||||
|
||||
# If no MSISDN is supplied then encode the file contents as all "ff"
|
||||
if msisdn in ["", "+"]:
|
||||
return "ff" * 14
|
||||
|
||||
# Leading '+' indicates International Number
|
||||
if msisdn[0] == '+':
|
||||
msisdn = msisdn[1:]
|
||||
ton = 0x01
|
||||
|
||||
# An MSISDN must not exceed 20 digits
|
||||
if len(msisdn) > 20:
|
||||
raise ValueError("msisdn must not be longer than 20 digits")
|
||||
|
||||
# Append 'f' padding if number of digits is odd
|
||||
if len(msisdn) % 2 > 0:
|
||||
msisdn += 'f'
|
||||
|
||||
# BCD length also includes NPI/ToN header
|
||||
bcd_len = len(msisdn) // 2 + 1
|
||||
npi_ton = (npi & 0x0f) | ((ton & 0x07) << 4) | 0x80
|
||||
bcd = rpad(swap_nibbles(msisdn), 10 * 2) # pad to 10 octets
|
||||
|
||||
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)
|
||||
|
||||
104
pySim/ota.py
104
pySim/ota.py
@@ -1,6 +1,6 @@
|
||||
"""Code related to SIM/UICC OTA according to TS 102 225 + TS 31.115."""
|
||||
|
||||
# (C) 2021-2023 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
|
||||
@@ -15,14 +15,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 pySim.construct import *
|
||||
from pySim.utils import b2h
|
||||
from pySim.sms import UserDataHeader
|
||||
from construct import *
|
||||
import zlib
|
||||
import abc
|
||||
import struct
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
|
||||
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
|
||||
from osmocom.construct import *
|
||||
from osmocom.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
|
||||
@@ -97,6 +99,17 @@ SmsCommandPacket = Struct('cmd_pkt_len'/Int16ub,
|
||||
'tar'/Bytes(3),
|
||||
'secured_data'/GreedyBytes)
|
||||
|
||||
# TS 102 226 Section 8.2.1.3.2.1
|
||||
SimFileAccessAndToolkitAppSpecParams = Struct('access_domain'/Prefixed(Int8ub, GreedyBytes),
|
||||
'prio_level_of_tk_app_inst'/Int8ub,
|
||||
'max_num_of_timers'/Int8ub,
|
||||
'max_text_length_for_menu_entry'/Int8ub,
|
||||
'menu_entries'/PrefixedArray(Int8ub, Struct('id'/Int8ub,
|
||||
'pos'/Int8ub)),
|
||||
'max_num_of_channels'/Int8ub,
|
||||
'msl'/Prefixed(Int8ub, GreedyBytes),
|
||||
'tar_values'/Prefixed(Int8ub, GreedyRange(Bytes(3))))
|
||||
|
||||
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,
|
||||
@@ -112,12 +125,12 @@ class OtaKeyset:
|
||||
@property
|
||||
def auth(self):
|
||||
"""Return an instance of the matching OtaAlgoAuth."""
|
||||
return OtaAlgoAuth.fromKeyset(self)
|
||||
return OtaAlgoAuth.from_keyset(self)
|
||||
|
||||
@property
|
||||
def crypt(self):
|
||||
"""Return an instance of the matching OtaAlgoCrypt."""
|
||||
return OtaAlgoCrypt.fromKeyset(self)
|
||||
return OtaAlgoCrypt.from_keyset(self)
|
||||
|
||||
class OtaCheckError(Exception):
|
||||
pass
|
||||
@@ -128,26 +141,24 @@ class OtaDialect(abc.ABC):
|
||||
def _compute_sig_len(self, spi:SPI):
|
||||
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
|
||||
return 0
|
||||
elif spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||
if spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||
return 4
|
||||
elif spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
|
||||
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
|
||||
else:
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
|
||||
@abc.abstractmethod
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, apdu: bytes) -> bytes:
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decode_resp(self, otak: OtaKeyset, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
|
||||
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."""
|
||||
pass
|
||||
|
||||
|
||||
from Cryptodome.Cipher import DES, DES3, AES
|
||||
@@ -190,7 +201,7 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
||||
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(data)
|
||||
return self._encrypt(padded_data)
|
||||
|
||||
def decrypt(self, data:bytes) -> bytes:
|
||||
"""Decrypt given input bytes using the key material given in constructor."""
|
||||
@@ -199,15 +210,13 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def _encrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _decrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def fromKeyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
|
||||
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:
|
||||
@@ -239,7 +248,7 @@ class OtaAlgoAuth(OtaAlgo, abc.ABC):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def fromKeyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
|
||||
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:
|
||||
@@ -324,6 +333,7 @@ class OtaDialectSms(OtaDialect):
|
||||
'response_status'/ResponseStatus,
|
||||
'cc_rc'/Bytes(this.rhl-10),
|
||||
'secured_data'/GreedyBytes)
|
||||
hdr_construct = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
|
||||
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
# length of signature in octets
|
||||
@@ -334,6 +344,7 @@ class OtaDialectSms(OtaDialect):
|
||||
len_cipher = 6 + len_sig + len(apdu)
|
||||
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
|
||||
pad_cnt = len(padding)
|
||||
apdu = bytes(apdu) # make a copy so we don't modify the input data
|
||||
apdu += padding
|
||||
|
||||
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
|
||||
@@ -344,8 +355,7 @@ class OtaDialectSms(OtaDialect):
|
||||
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})
|
||||
part_head = self.hdr_construct.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
|
||||
#print("part_head: %s" % b2h(part_head))
|
||||
|
||||
# CNTR + PCNTR (CNTR not used)
|
||||
@@ -387,8 +397,54 @@ class OtaDialectSms(OtaDialect):
|
||||
|
||||
#print("envelope_data: %s" % b2h(envelope_data))
|
||||
|
||||
if len(envelope_data) > 140:
|
||||
raise ValueError('Cannot encode command in a single SMS; Fragmentation not implemented')
|
||||
|
||||
return envelope_data
|
||||
|
||||
def decode_cmd(self, otak: OtaKeyset, encoded: bytes) -> Tuple[bytes, dict, bytes]:
|
||||
"""Decode an encoded (encrypted, signed) OTA SMS Command-APDU."""
|
||||
if True: # TODO: how to decide?
|
||||
cpl = int.from_bytes(encoded[:2], 'big')
|
||||
part_head = encoded[2:2+8]
|
||||
ciph = encoded[2+8:]
|
||||
envelope_data = otak.crypt.decrypt(ciph)
|
||||
else:
|
||||
part_head = encoded[:8]
|
||||
envelope_data = encoded[8:]
|
||||
|
||||
hdr_dec = self.hdr_construct.parse(part_head)
|
||||
|
||||
# strip counter part from front of envelope_data
|
||||
part_cnt = envelope_data[:6]
|
||||
cntr = int.from_bytes(part_cnt[:5], 'big')
|
||||
pad_cnt = int.from_bytes(part_cnt[5:], 'big')
|
||||
envelope_data = envelope_data[6:]
|
||||
|
||||
spi = hdr_dec['spi']
|
||||
if spi['rc_cc_ds'] == 'cc':
|
||||
# split cc from front of APDU
|
||||
cc = envelope_data[:8]
|
||||
apdu = envelope_data[8:]
|
||||
# verify CC
|
||||
temp_data = cpl.to_bytes(2, 'big') + part_head + part_cnt + apdu
|
||||
otak.auth.check_sig(temp_data, cc)
|
||||
elif spi['rc_cc_ds'] == 'rc':
|
||||
# CRC32
|
||||
crc32_rx = int.from_bytes(envelope_data[:4], 'big')
|
||||
# FIXME: crc32_computed = zlip.crc32(
|
||||
# FIXME: verify RC
|
||||
raise NotImplementedError
|
||||
apdu = envelope_data[4:]
|
||||
elif spi['rc_cc_ds'] == 'no_rc_cc_ds':
|
||||
apdu = envelope_data
|
||||
else:
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
|
||||
apdu = apdu[:len(apdu)-pad_cnt]
|
||||
return hdr_dec['tar'], spi, apdu
|
||||
|
||||
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
|
||||
if isinstance(data, str):
|
||||
data = h2b(data)
|
||||
@@ -399,7 +455,7 @@ class OtaDialectSms(OtaDialect):
|
||||
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
|
||||
if data[0] != 0x02:
|
||||
raise ValueError('Unexpected UDL=0x%02x' % data[0])
|
||||
udhd, remainder = UserDataHeader.fromBytes(data)
|
||||
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
|
||||
@@ -436,7 +492,7 @@ class OtaDialectSms(OtaDialect):
|
||||
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':
|
||||
if res.response_status == 'por_ok' and len(res['secured_data']):
|
||||
dec = CompactRemoteResp.parse(res['secured_data'])
|
||||
else:
|
||||
dec = None
|
||||
|
||||
77
pySim/pprint.py
Normal file
77
pySim/pprint.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import pprint
|
||||
from pprint import PrettyPrinter
|
||||
from functools import singledispatch, wraps
|
||||
from typing import get_type_hints
|
||||
|
||||
from pySim.utils import b2h
|
||||
|
||||
def common_container_checks(f):
|
||||
type_ = get_type_hints(f)['object']
|
||||
base_impl = type_.__repr__
|
||||
empty_repr = repr(type_()) # {}, [], ()
|
||||
too_deep_repr = f'{empty_repr[0]}...{empty_repr[-1]}' # {...}, [...], (...)
|
||||
@wraps(f)
|
||||
def wrapper(object, context, maxlevels, level):
|
||||
if type(object).__repr__ is not base_impl: # subclassed repr
|
||||
return repr(object)
|
||||
if not object: # empty, short-circuit
|
||||
return empty_repr
|
||||
if maxlevels and level >= maxlevels: # exceeding the max depth
|
||||
return too_deep_repr
|
||||
oid = id(object)
|
||||
if oid in context: # self-reference
|
||||
return pprint._recursion(object)
|
||||
context[oid] = 1
|
||||
result = f(object, context, maxlevels, level)
|
||||
del context[oid]
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
@singledispatch
|
||||
def saferepr(object, context, maxlevels, level):
|
||||
return repr(object)
|
||||
|
||||
@saferepr.register
|
||||
def _handle_bytes(object: bytes, *args):
|
||||
if len(object) <= 40:
|
||||
return '"%s"' % b2h(object)
|
||||
else:
|
||||
return '"%s...%s"' % (b2h(object[:20]), b2h(object[-20:]))
|
||||
|
||||
@saferepr.register
|
||||
@common_container_checks
|
||||
def _handle_dict(object: dict, context, maxlevels, level):
|
||||
level += 1
|
||||
contents = [
|
||||
f'{saferepr(k, context, maxlevels, level)}: '
|
||||
f'{saferepr(v, context, maxlevels, level)}'
|
||||
for k, v in sorted(object.items(), key=pprint._safe_tuple)
|
||||
]
|
||||
return f'{{{", ".join(contents)}}}'
|
||||
|
||||
@saferepr.register
|
||||
@common_container_checks
|
||||
def _handle_list(object: list, context, maxlevels, level):
|
||||
level += 1
|
||||
contents = [
|
||||
f'{saferepr(v, context, maxlevels, level)}'
|
||||
for v in object
|
||||
]
|
||||
return f'[{", ".join(contents)}]'
|
||||
|
||||
@saferepr.register
|
||||
@common_container_checks
|
||||
def _handle_tuple(object: tuple, context, maxlevels, level):
|
||||
level += 1
|
||||
if len(object) == 1:
|
||||
return f'({saferepr(object[0], context, maxlevels, level)},)'
|
||||
contents = [
|
||||
f'{saferepr(v, context, maxlevels, level)}'
|
||||
for v in object
|
||||
]
|
||||
return f'({", ".join(contents)})'
|
||||
|
||||
class HexBytesPrettyPrinter(PrettyPrinter):
|
||||
def format(self, *args):
|
||||
# it doesn't matter what the boolean values are here
|
||||
return saferepr(*args), True, False
|
||||
@@ -21,57 +21,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardApplication, interpret_sw
|
||||
from pySim.utils import all_subclasses
|
||||
import abc
|
||||
import operator
|
||||
from typing import List
|
||||
|
||||
|
||||
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()
|
||||
|
||||
scc.cla_byte = cla_byte
|
||||
scc.sel_ctrl = sel_ctrl
|
||||
rc = True
|
||||
try:
|
||||
for fid in fids:
|
||||
scc.select_file(fid)
|
||||
except:
|
||||
rc = False
|
||||
|
||||
scc.reset_card()
|
||||
scc.cla_byte = cla_byte_bak
|
||||
scc.sel_ctrl = sel_ctrl_bak
|
||||
return rc
|
||||
|
||||
|
||||
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", ["3f00"])
|
||||
|
||||
|
||||
def match_sim(scc: SimCardCommands) -> bool:
|
||||
""" Try to access MF via 2G APDUs (3GPP TS 11.11), if this works, the card
|
||||
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", ["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"])
|
||||
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardApplication, interpret_sw
|
||||
from pySim.utils import all_subclasses
|
||||
|
||||
class CardProfile:
|
||||
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
|
||||
@@ -138,13 +95,39 @@ class CardProfile:
|
||||
return data_hex
|
||||
|
||||
@staticmethod
|
||||
def _mf_select_test(scc: SimCardCommands,
|
||||
cla_byte: str, sel_ctrl: str,
|
||||
fids: List[str]) -> bool:
|
||||
"""Helper function used by some derived _try_match_card() methods."""
|
||||
scc.reset_card()
|
||||
|
||||
scc.cla_byte = cla_byte
|
||||
scc.sel_ctrl = sel_ctrl
|
||||
for fid in fids:
|
||||
scc.select_file(fid)
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def match_with_card(scc: SimCardCommands) -> bool:
|
||||
"""Check if the specific profile matches the card. This method is a
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
"""Try to see if the specific profile matches the card. This method is a
|
||||
placeholder that is overloaded by specific dirived classes. The method
|
||||
actively probes the card to make sure the profile class matches the
|
||||
physical card. This usually also means that the card is reset during
|
||||
the process, so this method must not be called at random times. It may
|
||||
only be called on startup. If there is no exception raised, we assume
|
||||
the card matches the profile.
|
||||
|
||||
Args:
|
||||
scc: SimCardCommands class
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def match_with_card(cls, scc: SimCardCommands) -> bool:
|
||||
"""Check if the specific profile matches the card. The method
|
||||
actively probes the card to make sure the profile class matches the
|
||||
physical card. This usually also means that the card is reset during
|
||||
the process, so this method must not be called at random times. It may
|
||||
only be called on startup.
|
||||
|
||||
Args:
|
||||
@@ -152,7 +135,17 @@ class CardProfile:
|
||||
Returns:
|
||||
match = True, no match = False
|
||||
"""
|
||||
return False
|
||||
sel_backup = scc.sel_ctrl
|
||||
cla_backup = scc.cla_byte
|
||||
try:
|
||||
cls._try_match_card(scc)
|
||||
return True
|
||||
except SwMatchError:
|
||||
return False
|
||||
finally:
|
||||
scc.sel_ctrl = sel_backup
|
||||
scc.cla_byte = cla_backup
|
||||
scc.reset_card()
|
||||
|
||||
@staticmethod
|
||||
def pick(scc: SimCardCommands):
|
||||
@@ -166,7 +159,7 @@ class CardProfile:
|
||||
return None
|
||||
|
||||
def add_addon(self, addon: 'CardProfileAddon'):
|
||||
assert(addon not in self.addons)
|
||||
assert addon not in self.addons
|
||||
# we don't install any additional files, as that is happening in the RuntimeState.
|
||||
self.addons.append(addon)
|
||||
|
||||
@@ -186,7 +179,6 @@ class CardProfileAddon(abc.ABC):
|
||||
self.desc = kw.get("desc", None)
|
||||
self.files_in_mf = kw.get("files_in_mf", [])
|
||||
self.shell_cmdsets = kw.get("shell_cmdsets", [])
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -194,4 +186,3 @@ class CardProfileAddon(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def probe(self, card: 'CardBase') -> bool:
|
||||
"""Probe a given card to determine whether or not this add-on is present/supported."""
|
||||
pass
|
||||
@@ -19,15 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import enum
|
||||
|
||||
from pySim.utils import *
|
||||
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
|
||||
from osmocom.utils import *
|
||||
from osmocom.construct 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 *
|
||||
from construct import *
|
||||
from pySim.profile.ts_51_011 import CardProfileSIM
|
||||
from pySim.profile.ts_51_011 import DF_TELECOM, DF_GSM
|
||||
from pySim.profile.ts_51_011 import EF_ServiceTable
|
||||
|
||||
|
||||
# Mapping between CDMA Service Number and its description
|
||||
@@ -178,20 +178,23 @@ class DF_CDMA(CardDF):
|
||||
class CardProfileRUIM(CardProfile):
|
||||
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
|
||||
|
||||
ORDER = 2
|
||||
ORDER = 20
|
||||
|
||||
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(resp_hex: str) -> object:
|
||||
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(resp_hex)
|
||||
return CardProfileSIM.decode_select_response(data_hex)
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
""" 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."""
|
||||
cls._mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
|
||||
|
||||
@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."""
|
||||
58
pySim/profile/euicc.py
Normal file
58
pySim/profile/euicc.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.euicc import CardApplicationISDR, AID_ISD_R, AID_ECASD, GetCertsReq, GetCertsResp
|
||||
|
||||
class CardProfileEuiccSGP32(CardProfileUICC):
|
||||
ORDER = 5
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='IoT eUICC (SGP.32)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try a command only supported by SGP.32
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
CardApplicationISDR.store_data_tlv(scc, GetCertsReq(), GetCertsResp)
|
||||
|
||||
class CardProfileEuiccSGP22(CardProfileUICC):
|
||||
ORDER = 6
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='Consumer eUICC (SGP.22)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try to read EID from ISD-R
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
eid = CardApplicationISDR.get_eid(scc)
|
||||
# TODO: Store EID identity?
|
||||
|
||||
class CardProfileEuiccSGP02(CardProfileUICC):
|
||||
ORDER = 7
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='M2M eUICC (SGP.02)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ECASD)
|
||||
scc.get_data(0x5a)
|
||||
# TODO: Store EID identity?
|
||||
@@ -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 osmocom.construct import *
|
||||
|
||||
from pySim.profile import CardProfileAddon
|
||||
from pySim.filesystem import *
|
||||
import pySim.ts_51_011
|
||||
|
||||
######################################################################
|
||||
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
|
||||
@@ -77,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):
|
||||
@@ -94,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
|
||||
|
||||
|
||||
@@ -111,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):
|
||||
@@ -131,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
|
||||
|
||||
|
||||
@@ -296,7 +290,7 @@ class EF_Predefined(LinFixedEF):
|
||||
else:
|
||||
return parse_construct(self.construct_others, raw_bin_data)
|
||||
|
||||
def _encode_record_bin(self, abstract_data : dict, record_nr : int) -> bytearray:
|
||||
def _encode_record_bin(self, abstract_data : dict, record_nr : int, **kwargs) -> bytearray:
|
||||
r = None
|
||||
if record_nr == 1:
|
||||
r = self.construct_first.build(abstract_data)
|
||||
394
pySim/profile/ts_102_221.py
Normal file
394
pySim/profile/ts_102_221.py
Normal file
@@ -0,0 +1,394 @@
|
||||
# coding=utf-8
|
||||
"""Card Profile of ETSI TS 102 221, the core UICC spec.
|
||||
|
||||
(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
|
||||
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 construct import Struct, FlagsEnum, GreedyString
|
||||
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile
|
||||
from pySim import iso7816_4
|
||||
from pySim.ts_102_221 import decode_select_response, ts_102_22x_cmdset
|
||||
from pySim.ts_102_221 import AM_DO_EF, SC_DO, AdditionalInterfacesSupport, AdditionalTermCapEuicc
|
||||
from pySim.ts_102_221 import TerminalPowerSupply, ExtendedLchanTerminalSupport, TerminalCapability
|
||||
|
||||
# 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.profile.ts_51_011 import AddonSIM, EF_ICCID, EF_PL
|
||||
from pySim.profile.gsm_r import AddonGSMR
|
||||
from pySim.profile.cdma_ruim import AddonRUIM
|
||||
|
||||
|
||||
# TS 102 221 Section 13.1
|
||||
class EF_DIR(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( '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')
|
||||
|
||||
# see https://github.com/PyCQA/pylint/issues/5794
|
||||
#pylint: disable=undefined-variable
|
||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
|
||||
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
|
||||
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
|
||||
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
|
||||
iso7816_4.ApplicationRelatedDOSet]):
|
||||
pass
|
||||
|
||||
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
|
||||
self._tlv = EF_DIR.ApplicationTemplate
|
||||
|
||||
|
||||
# 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
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
@staticmethod
|
||||
def flatten(inp: list):
|
||||
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
|
||||
def sc_abbreviate(sc):
|
||||
if 'always' in sc:
|
||||
return 'always'
|
||||
elif 'never' in sc:
|
||||
return 'never'
|
||||
elif 'control_reference_template' in sc:
|
||||
return sc['control_reference_template']
|
||||
else:
|
||||
return sc
|
||||
|
||||
by_mode = {}
|
||||
for t in inp:
|
||||
am = t[0]
|
||||
sc = t[1]
|
||||
sc_abbr = sc_abbreviate(sc)
|
||||
if 'access_mode' in am:
|
||||
for m in am['access_mode']:
|
||||
by_mode[m] = sc_abbr
|
||||
elif 'command_header' in am:
|
||||
ins = am['command_header']['INS']
|
||||
if 'CLA' in am['command_header']:
|
||||
cla = am['command_header']['CLA']
|
||||
else:
|
||||
cla = None
|
||||
cmd = ts_102_22x_cmdset.lookup(ins, cla)
|
||||
if cmd:
|
||||
name = cmd.name.lower().replace(' ', '_')
|
||||
by_mode[name] = sc_abbr
|
||||
else:
|
||||
raise ValueError
|
||||
else:
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
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)
|
||||
# we cannot pass the result through flatten() here, as we don't have a related
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
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):
|
||||
@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 = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
def do_read_arr_records(self, opts):
|
||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
# 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 = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
|
||||
# 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,
|
||||
support_uicc_suspend=2)
|
||||
self._construct = Struct(
|
||||
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
|
||||
|
||||
|
||||
class CardProfileUICC(CardProfile):
|
||||
|
||||
ORDER = 10
|
||||
|
||||
def __init__(self, name='UICC'):
|
||||
files = [
|
||||
EF_DIR(),
|
||||
EF_ICCID(),
|
||||
EF_PL(),
|
||||
EF_ARR(),
|
||||
# FIXME: DF.CD
|
||||
EF_UMPC(),
|
||||
]
|
||||
addons = [
|
||||
AddonSIM,
|
||||
AddonGSMR,
|
||||
AddonRUIM,
|
||||
]
|
||||
sw = {
|
||||
'Normal': {
|
||||
'9000': 'Normal ending of the command',
|
||||
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
|
||||
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
|
||||
},
|
||||
'Postponed processing': {
|
||||
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
|
||||
},
|
||||
'Warnings': {
|
||||
'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/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',
|
||||
'62f3': 'Response data available',
|
||||
'63f1': 'More data expected',
|
||||
'63f2': 'More data expected and proactive command pending',
|
||||
'63cx': 'Command successful but after using an internal update retry routine X times',
|
||||
},
|
||||
'Execution errors': {
|
||||
'6400': 'No information given, state of non-volatile memory unchanged',
|
||||
'6500': 'No information given, state of non-volatile memory changed',
|
||||
'6581': 'Memory problem',
|
||||
},
|
||||
'Checking errors': {
|
||||
'6700': 'Wrong length',
|
||||
'67xx': 'The interpretation of this status word is command dependent',
|
||||
'6b00': 'Wrong parameter(s) P1-P2',
|
||||
'6d00': 'Instruction code not supported or invalid',
|
||||
'6e00': 'Class not supported',
|
||||
'6f00': 'Technical problem, no precise diagnosis',
|
||||
'6fxx': 'The interpretation of this status word is command dependent',
|
||||
},
|
||||
'Functions in CLA not supported': {
|
||||
'6800': 'No information given',
|
||||
'6881': 'Logical channel not supported',
|
||||
'6882': 'Secure messaging not supported',
|
||||
},
|
||||
'Command not allowed': {
|
||||
'6900': 'No information given',
|
||||
'6981': 'Command incompatible with file structure',
|
||||
'6982': 'Security status not satisfied',
|
||||
'6983': 'Authentication/PIN method blocked',
|
||||
'6984': 'Referenced data invalidated',
|
||||
'6985': 'Conditions of use not satisfied',
|
||||
'6986': 'Command not allowed (no EF selected)',
|
||||
'6989': 'Command not allowed - secure channel - security not satisfied',
|
||||
},
|
||||
'Wrong parameters': {
|
||||
'6a80': 'Incorrect parameters in the data field',
|
||||
'6a81': 'Function not supported',
|
||||
'6a82': 'File not found',
|
||||
'6a83': 'Record not found',
|
||||
'6a84': 'Not enough memory space',
|
||||
'6a86': 'Incorrect parameters P1 to P2',
|
||||
'6a87': 'Lc inconsistent with P1 to P2',
|
||||
'6a88': 'Referenced data not found',
|
||||
},
|
||||
'Application errors': {
|
||||
'9850': 'INCREASE cannot be performed, max value reached',
|
||||
'9862': 'Authentication error, application specific',
|
||||
'9863': 'Security session or association expired',
|
||||
'9864': 'Minimum UICC suspension time is too long',
|
||||
},
|
||||
}
|
||||
|
||||
super().__init__(name, desc='ETSI TS 102 221', cla="00",
|
||||
sel_ctrl="0004", files_in_mf=files, sw=sw,
|
||||
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
return decode_select_response(data_hex)
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
|
||||
card is considered a UICC card."""
|
||||
cls._mf_select_test(scc, "00", "0004", ["3f00"])
|
||||
|
||||
@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')
|
||||
|
||||
# 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))
|
||||
|
||||
resume_uicc_parser = argparse.ArgumentParser()
|
||||
resume_uicc_parser.add_argument('TOKEN', type=str, help='Token provided during SUSPEND')
|
||||
|
||||
@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)
|
||||
|
||||
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')
|
||||
|
||||
@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)
|
||||
@@ -29,19 +29,21 @@ order to describe the files specified in the relevant ETSI + 3GPP specifications
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from pySim.profile import match_sim
|
||||
import enum
|
||||
from struct import pack, unpack
|
||||
from typing import Tuple
|
||||
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_plmn, enc_plmn, dec_xplmn_w_act
|
||||
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
|
||||
from construct import *
|
||||
from struct import pack, unpack
|
||||
from typing import Tuple
|
||||
from pySim.tlv import *
|
||||
from pySim.utils import *
|
||||
from pySim.profile.gsm_r import AddonGSMR
|
||||
|
||||
# Mapping between SIM Service Number and its description
|
||||
EF_SST_map = {
|
||||
@@ -129,13 +131,13 @@ class ExtendedBcdAdapter(Adapter):
|
||||
|
||||
# TS 51.011 Section 10.5.1
|
||||
class EF_ADN(LinFixedEF):
|
||||
_test_decode = [
|
||||
( '42204841203120536963ffffffff06810628560810ffffffffffffff',
|
||||
_test_de_encode = [
|
||||
( '42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF',
|
||||
{ "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',
|
||||
( '4B756E64656E626574726575756E67FF0791947112122721ffffffffffff',
|
||||
{"alpha_id": "Kundenbetreuung", "len_of_bcd": 7, "ton_npi": {"ext": True, "type_of_number":
|
||||
"international",
|
||||
"numbering_plan_id": "isdn_e164"},
|
||||
@@ -186,20 +188,54 @@ class EF_SMS(LinFixedEF):
|
||||
|
||||
# TS 51.011 Section 10.5.5
|
||||
class EF_MSISDN(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( 'ffffffffffffffffffffffffffffffffffffffff04b12143f5ffffffffffffffffff',
|
||||
{"alpha_id": "", "len_of_bcd": 4, "ton_npi": {"ext": True, "type_of_number": "network_specific",
|
||||
"numbering_plan_id": "isdn_e164"},
|
||||
"dialing_nr": "12345f"}),
|
||||
( '456967656e65205275666e756d6d6572ffffffff0891947172199181f3ffffffffff',
|
||||
{"alpha_id": "Eigene Rufnummer", "len_of_bcd": 8, "ton_npi": {"ext": True, "type_of_number": "international",
|
||||
"numbering_plan_id": "isdn_e164"},
|
||||
"dialing_nr": "4917279119183f"}),
|
||||
]
|
||||
|
||||
# Ensure deprecated representations still work
|
||||
_test_encode = [
|
||||
( 'ffffffffffffffffffffffffffffffffffffffff05b1716662f6ffffffffffffffff',
|
||||
{"msisdn": [ 1, 3, "1766266"]}),
|
||||
( 'ffffffffffffffffffffffffffffffffffffffff06b121436587f9ffffffffffffff',
|
||||
{"msisdn": "123456789"}),
|
||||
]
|
||||
|
||||
_test_no_pad = True
|
||||
|
||||
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), leftpad=True, **kwargs)
|
||||
self._construct = Struct('alpha_id'/COptional(GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-14)))),
|
||||
'len_of_bcd'/Int8ub,
|
||||
'ton_npi'/TonNpi,
|
||||
'dialing_nr'/ExtendedBcdAdapter(BcdAdapter(Rpad(Bytes(10)))),
|
||||
Padding(2, pattern=b'\xff'))
|
||||
|
||||
def _decode_record_hex(self, raw_hex_data, **kwargs):
|
||||
return {'msisdn': dec_msisdn(raw_hex_data)}
|
||||
|
||||
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"
|
||||
return alpha_identifier + encoded_msisdn
|
||||
# Maintain compatibility with deprecated representations
|
||||
def encode_record_hex(self, abstract_data: dict, record_nr: int, total_len: int = None) -> str:
|
||||
if 'msisdn' in abstract_data:
|
||||
msisdn = abstract_data['msisdn']
|
||||
if type(msisdn) == str:
|
||||
npi = 'isdn_e164'
|
||||
ton = 'network_specific'
|
||||
dialing_nr = msisdn + len(msisdn) % 2 * "f"
|
||||
else:
|
||||
npi = msisdn[0]
|
||||
ton = msisdn[1]
|
||||
dialing_nr = msisdn[2] + len(msisdn[2]) % 2 * "f"
|
||||
abstract_data = {'alpha_id' : "",
|
||||
'len_of_bcd' : len(dialing_nr) // 2 + 1,
|
||||
'ton_npi' : {'ext' : True,
|
||||
'type_of_number' : ton,
|
||||
'numbering_plan_id' : npi},
|
||||
'dialing_nr' : dialing_nr}
|
||||
return super().encode_record_hex(abstract_data, record_nr, total_len)
|
||||
|
||||
# TS 51.011 Section 10.5.6
|
||||
class EF_SMSP(LinFixedEF):
|
||||
@@ -355,7 +391,7 @@ class EF_IMSI(TransparentEF):
|
||||
def _decode_hex(self, raw_hex):
|
||||
return {'imsi': dec_imsi(raw_hex)}
|
||||
|
||||
def _encode_hex(self, abstract):
|
||||
def _encode_hex(self, abstract, **kwargs):
|
||||
return enc_imsi(abstract['imsi'])
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
@@ -443,7 +479,7 @@ class EF_ServiceTable(TransparentEF):
|
||||
}
|
||||
return ret
|
||||
|
||||
def _encode_bin(self, in_json):
|
||||
def _encode_bin(self, in_json, **kwargs):
|
||||
# compute the required binary size
|
||||
bin_len = 0
|
||||
for srv in in_json.keys():
|
||||
@@ -805,7 +841,7 @@ class EF_InvScan(TransparentEF):
|
||||
|
||||
# TS 51.011 Section 10.3.46
|
||||
class EF_CFIS(LinFixedEF):
|
||||
_test_decode = [
|
||||
_test_de_encode = [
|
||||
( '0100ffffffffffffffffffffffffffff',
|
||||
{"msp_number": 1, "cfu_indicator_status": { "voice": False, "fax": False, "data": False, "rfu": 0 },
|
||||
"len_of_bcd": 255, "ton_npi": {"ext": True,
|
||||
@@ -955,6 +991,44 @@ class EF_MMSUCP(TransparentEF):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
|
||||
|
||||
# TS 102 221 Section 13.2 / TS 31.101 Section 13 / TS 51.011 Section 10.1.1
|
||||
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))
|
||||
|
||||
def _decode_hex(self, raw_hex):
|
||||
return {'iccid': dec_iccid(raw_hex)}
|
||||
|
||||
def _encode_hex(self, abstract, **kwargs):
|
||||
return enc_iccid(abstract['iccid'])
|
||||
|
||||
# TS 102 221 Section 13.3 / TS 31.101 Secction 13 / TS 51.011 Section 10.1.2
|
||||
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, **kwargs):
|
||||
if bin_data == b'\xff\xff':
|
||||
return None
|
||||
else:
|
||||
return bin_data.decode('ascii')
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
if in_json is None:
|
||||
return b'\xff\xff'
|
||||
else:
|
||||
return in_json.encode('ascii')
|
||||
|
||||
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)
|
||||
@@ -1034,18 +1108,18 @@ class DF_GSM(CardDF):
|
||||
super().__init__()
|
||||
|
||||
authenticate_parser = argparse.ArgumentParser()
|
||||
authenticate_parser.add_argument('rand', help='Random challenge')
|
||||
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)
|
||||
(data, sw) = self._cmd.lchan.scc.run_gsm(opts.RAND)
|
||||
self._cmd.poutput_json(data)
|
||||
|
||||
|
||||
class CardProfileSIM(CardProfile):
|
||||
|
||||
ORDER = 3
|
||||
ORDER = 30
|
||||
|
||||
def __init__(self):
|
||||
sw = {
|
||||
@@ -1085,12 +1159,19 @@ class CardProfileSIM(CardProfile):
|
||||
},
|
||||
}
|
||||
|
||||
files = [
|
||||
EF_ICCID(),
|
||||
EF_PL(),
|
||||
DF_TELECOM(),
|
||||
DF_GSM(),
|
||||
]
|
||||
|
||||
addons = [
|
||||
AddonGSMR,
|
||||
]
|
||||
|
||||
super().__init__('SIM', desc='GSM SIM Card', cla="a0",
|
||||
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw, addons = addons)
|
||||
sel_ctrl="0000", files_in_mf=files, sw=sw, addons = addons)
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(resp_hex: str) -> object:
|
||||
@@ -1143,9 +1224,9 @@ class CardProfileSIM(CardProfile):
|
||||
ret['life_cycle_status_int'] = 'terminated'
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def match_with_card(scc: SimCardCommands) -> bool:
|
||||
return match_sim(scc)
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
cls._mf_select_test(scc, "a0", "0000", ["3f00"])
|
||||
|
||||
|
||||
class AddonSIM(CardProfileAddon):
|
||||
162
pySim/runtime.py
162
pySim/runtime.py
@@ -18,8 +18,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from osmocom.utils import h2b, i2h, is_hex, Hexstr
|
||||
from osmocom.tlv import bertlv_parse_one
|
||||
|
||||
from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
|
||||
from pySim.exceptions import *
|
||||
from pySim.filesystem import *
|
||||
|
||||
@@ -29,11 +30,10 @@ def lchan_nr_from_cla(cla: int) -> int:
|
||||
if cla >> 4 in [0x0, 0xA, 0x8]:
|
||||
# Table 10.3
|
||||
return cla & 0x03
|
||||
elif cla & 0xD0 in [0x40, 0xC0]:
|
||||
if cla & 0xD0 in [0x40, 0xC0]:
|
||||
# Table 10.4a
|
||||
return 4 + (cla & 0x0F)
|
||||
else:
|
||||
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
|
||||
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
|
||||
|
||||
class RuntimeState:
|
||||
"""Represent the runtime state of a session with a card."""
|
||||
@@ -50,6 +50,9 @@ class RuntimeState:
|
||||
self.lchan = {}
|
||||
# the basic logical channel always exists
|
||||
self.lchan[0] = RuntimeLchan(0, self)
|
||||
# this is a dict of card identities which different parts of the code might populate,
|
||||
# typically with something like ICCID, EID, ATR, ...
|
||||
self.identity = {}
|
||||
|
||||
# make sure the class and selection control bytes, which are specified
|
||||
# by the card profile are used
|
||||
@@ -116,7 +119,7 @@ class RuntimeState:
|
||||
# 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)
|
||||
_data, sw = self.card.select_adf_by_aid(f.aid)
|
||||
self.selected_adf = f
|
||||
if sw == "9000":
|
||||
print(" %s: %s" % (f.name, f.aid))
|
||||
@@ -132,13 +135,18 @@ class RuntimeState:
|
||||
"""
|
||||
# delete all lchan != 0 (basic lchan)
|
||||
for lchan_nr in list(self.lchan.keys()):
|
||||
self.lchan[lchan_nr].scc.scp = None
|
||||
if lchan_nr == 0:
|
||||
continue
|
||||
del self.lchan[lchan_nr]
|
||||
atr = i2h(self.card.reset())
|
||||
if cmd_app:
|
||||
cmd_app.lchan = self.lchan[0]
|
||||
# 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
|
||||
# store ATR as part of our card identies dict
|
||||
self.identity['ATR'] = atr
|
||||
return atr
|
||||
|
||||
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
|
||||
@@ -203,6 +211,18 @@ class RuntimeLchan:
|
||||
def selected_file_num_of_rec(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
|
||||
|
||||
def selected_file_record_len(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['file_descriptor'].get('record_len')
|
||||
|
||||
def selected_file_size(self) -> Optional[int]:
|
||||
return self.selected_file_fcp.get('file_size')
|
||||
|
||||
def selected_file_reserved_file_size(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['proprietary_information'].get('reserved_file_size')
|
||||
|
||||
def selected_file_maximum_file_size(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['proprietary_information'].get('maximum_file_size')
|
||||
|
||||
def get_cwd(self) -> CardDF:
|
||||
"""Obtain the current working directory.
|
||||
|
||||
@@ -227,6 +247,42 @@ class RuntimeLchan:
|
||||
node = node.parent
|
||||
return None
|
||||
|
||||
def get_file_by_name(self, name: str) -> CardFile:
|
||||
"""Obtain the file object from the file system tree by its name without actually selecting the file.
|
||||
|
||||
Returns:
|
||||
CardFile() instance or None"""
|
||||
|
||||
# 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'
|
||||
else:
|
||||
pathlist = [name]
|
||||
|
||||
# start in the current working directory (we can still
|
||||
# select any ADF and the MF from here, so those will be
|
||||
# among the selectables).
|
||||
file = self.get_cwd()
|
||||
|
||||
for p in pathlist:
|
||||
# Look for the next file in the path list
|
||||
selectables = file.get_selectables()
|
||||
file = None
|
||||
for selectable in selectables:
|
||||
if selectable == p:
|
||||
file = selectables[selectable]
|
||||
break
|
||||
|
||||
# When we hit none, then the given path must be invalid
|
||||
if file is None:
|
||||
return None
|
||||
|
||||
# Return the file object found at the tip of the path
|
||||
return file
|
||||
|
||||
def interpret_sw(self, sw: str):
|
||||
"""Interpret a given status word relative to the currently selected application
|
||||
or the underlying card profile.
|
||||
@@ -255,7 +311,8 @@ class RuntimeLchan:
|
||||
raise ValueError(
|
||||
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
|
||||
|
||||
self._select_pre(cmd_app)
|
||||
# unregister commands of old file
|
||||
self.unregister_cmds(cmd_app)
|
||||
|
||||
try:
|
||||
# We access the card through the select_file method of the scc object.
|
||||
@@ -264,20 +321,20 @@ class RuntimeLchan:
|
||||
# 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)
|
||||
(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]))
|
||||
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'):
|
||||
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'):
|
||||
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:
|
||||
@@ -288,12 +345,6 @@ class RuntimeLchan:
|
||||
|
||||
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.
|
||||
@@ -304,11 +355,12 @@ class RuntimeLchan:
|
||||
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)
|
||||
self.register_cmds(cmd_app)
|
||||
|
||||
def select_file(self, file: CardFile, cmd_app=None):
|
||||
"""Select a file (EF, DF, ADF, MF, ...).
|
||||
@@ -317,11 +369,31 @@ class RuntimeLchan:
|
||||
file : CardFile [or derived class] instance
|
||||
cmd_app : Command Application State (for unregistering old file commands)
|
||||
"""
|
||||
|
||||
if not isinstance(file, CardADF) and self.selected_adf and self.selected_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 we will first select an arbitrary ADF that has file system support first
|
||||
# and then continue normally.
|
||||
for selectable in self.rs.mf.get_selectables().items():
|
||||
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
|
||||
self.select(selectable[1].name, cmd_app)
|
||||
break
|
||||
|
||||
# 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)
|
||||
|
||||
# unregister commands of old file
|
||||
self.unregister_cmds(cmd_app)
|
||||
|
||||
# be sure the variables that we pass to _select_post contain valid values.
|
||||
selected_file = self.selected_file
|
||||
@@ -337,13 +409,13 @@ class RuntimeLchan:
|
||||
# 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)
|
||||
(data, _sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
|
||||
else:
|
||||
(data, sw) = self.scc.select_file(f.fid)
|
||||
(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)
|
||||
raise swm
|
||||
|
||||
self._select_post(cmd_app, f, data)
|
||||
|
||||
@@ -391,10 +463,11 @@ class RuntimeLchan:
|
||||
|
||||
def status(self):
|
||||
"""Request STATUS (current selected file FCP) from card."""
|
||||
(data, sw) = self.scc.status()
|
||||
(data, _sw) = self.scc.status()
|
||||
return self.selected_file.decode_select_response(data)
|
||||
|
||||
def get_file_for_selectable(self, name: str):
|
||||
def get_file_for_filename(self, name: str):
|
||||
"""Get the related CardFile object for a specified filename."""
|
||||
sels = self.selected_file.get_selectables()
|
||||
return sels[name]
|
||||
|
||||
@@ -415,7 +488,8 @@ class RuntimeLchan:
|
||||
binary data read from the file
|
||||
"""
|
||||
if not isinstance(self.selected_file, TransparentEF):
|
||||
raise TypeError("Only works with TransparentEF")
|
||||
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
return self.scc.read_binary(self.selected_file.fid, length, offset)
|
||||
|
||||
def read_binary_dec(self) -> Tuple[dict, str]:
|
||||
@@ -439,7 +513,8 @@ class RuntimeLchan:
|
||||
offset : Offset into the file from which to write 'data_hex'
|
||||
"""
|
||||
if not isinstance(self.selected_file, TransparentEF):
|
||||
raise TypeError("Only works with TransparentEF")
|
||||
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
|
||||
|
||||
def update_binary_dec(self, data: dict):
|
||||
@@ -449,7 +524,7 @@ class RuntimeLchan:
|
||||
Args:
|
||||
data : abstract data which is to be encoded and written
|
||||
"""
|
||||
data_hex = self.selected_file.encode_hex(data)
|
||||
data_hex = self.selected_file.encode_hex(data, self.selected_file_size())
|
||||
return self.update_binary(data_hex)
|
||||
|
||||
def read_record(self, rec_nr: int = 0):
|
||||
@@ -461,7 +536,8 @@ class RuntimeLchan:
|
||||
hex string of binary data contained in record
|
||||
"""
|
||||
if not isinstance(self.selected_file, LinFixedEF):
|
||||
raise TypeError("Only works with Linear Fixed EF")
|
||||
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
# returns a string of hex nibbles
|
||||
return self.scc.read_record(self.selected_file.fid, rec_nr)
|
||||
|
||||
@@ -484,7 +560,8 @@ class RuntimeLchan:
|
||||
data_hex : Hex string binary data to be written
|
||||
"""
|
||||
if not isinstance(self.selected_file, LinFixedEF):
|
||||
raise TypeError("Only works with Linear Fixed EF")
|
||||
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
|
||||
conserve=self.rs.conserve_write,
|
||||
leftpad=self.selected_file.leftpad)
|
||||
@@ -497,7 +574,7 @@ class RuntimeLchan:
|
||||
rec_nr : Record number to read
|
||||
data_hex : Abstract data to be written
|
||||
"""
|
||||
data_hex = self.selected_file.encode_record_hex(data, rec_nr)
|
||||
data_hex = self.selected_file.encode_record_hex(data, rec_nr, self.selected_file_record_len())
|
||||
return self.update_record(rec_nr, data_hex)
|
||||
|
||||
def retrieve_data(self, tag: int = 0):
|
||||
@@ -520,9 +597,10 @@ class RuntimeLchan:
|
||||
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))
|
||||
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
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):
|
||||
@@ -533,14 +611,18 @@ class RuntimeLchan:
|
||||
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")
|
||||
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
|
||||
|
||||
def register_cmds(self, cmd_app=None):
|
||||
"""Register command set that is associated with the currently selected 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 unregister_cmds(self, cmd_app=None):
|
||||
"""Unregister all file specific commands."""
|
||||
"""Unregister command set that is associated with the currently selected file"""
|
||||
if cmd_app and self.selected_file.shell_commands:
|
||||
for c in self.selected_file.shell_commands:
|
||||
cmd_app.unregister_command_set(c)
|
||||
|
||||
|
||||
|
||||
|
||||
39
pySim/secure_channel.py
Normal file
39
pySim/secure_channel.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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 osmocom.utils import b2h, h2b, Hexstr
|
||||
|
||||
from pySim.utils import ResTuple
|
||||
|
||||
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
|
||||
62
pySim/sms.py
62
pySim/sms.py
@@ -20,12 +20,11 @@
|
||||
import typing
|
||||
import abc
|
||||
from bidict import bidict
|
||||
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger, Flag
|
||||
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 osmocom.construct import HexAdapter, BcdAdapter, TonNpi
|
||||
from osmocom.utils import Hexstr, h2b, b2h
|
||||
|
||||
from smpp.pdu import pdu_types, operations
|
||||
|
||||
@@ -51,13 +50,13 @@ class UserDataHeader:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
|
||||
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 toBytes(self) -> bytes:
|
||||
def to_bytes(self) -> bytes:
|
||||
return self._construct.build({'ies':self.ies, 'data':b''})
|
||||
|
||||
|
||||
@@ -117,7 +116,7 @@ class AddressField:
|
||||
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
|
||||
|
||||
@classmethod
|
||||
def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
|
||||
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)
|
||||
@@ -129,16 +128,16 @@ class AddressField:
|
||||
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
|
||||
|
||||
@classmethod
|
||||
def fromSmpp(cls, addr, ton, npi) -> 'AddressField':
|
||||
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 toSmpp(self):
|
||||
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 toBytes(self) -> bytes:
|
||||
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:
|
||||
@@ -185,13 +184,12 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
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 fromBytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
|
||||
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)
|
||||
flags = inb[0]
|
||||
d = SMS_DELIVER.flags_construct.parse(inb)
|
||||
oa, remainder = AddressField.fromBytes(inb[1:])
|
||||
oa, remainder = AddressField.from_bytes(inb[1:])
|
||||
d['tp_oa'] = oa
|
||||
offset = 0
|
||||
d['tp_pid'] = remainder[offset]
|
||||
@@ -206,7 +204,7 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
d['tp_ud'] = remainder[offset:]
|
||||
return cls(**d)
|
||||
|
||||
def toBytes(self) -> bytes:
|
||||
def to_bytes(self) -> bytes:
|
||||
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
|
||||
outb = bytearray()
|
||||
d = {
|
||||
@@ -215,7 +213,7 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
}
|
||||
flags = SMS_DELIVER.flags_construct.build(d)
|
||||
outb.extend(flags)
|
||||
outb.extend(self.tp_oa.toBytes())
|
||||
outb.extend(self.tp_oa.to_bytes())
|
||||
outb.append(self.tp_pid)
|
||||
outb.append(self.tp_dcs)
|
||||
outb.extend(self.tp_scts)
|
||||
@@ -225,18 +223,18 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
return outb
|
||||
|
||||
@classmethod
|
||||
def fromSmpp(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||
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.fromSmppSubmit(smpp_pdu)
|
||||
return cls.from_smpp_submit(smpp_pdu)
|
||||
else:
|
||||
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
||||
|
||||
@classmethod
|
||||
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||
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.fromSmpp(smpp_pdu.params['source_addr'],
|
||||
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']
|
||||
@@ -276,7 +274,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
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 fromBytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
|
||||
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):
|
||||
@@ -285,7 +283,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
offset += 1
|
||||
d['tp_mr']= inb[offset]
|
||||
offset += 1
|
||||
da, remainder = AddressField.fromBytes(inb[2:])
|
||||
da, remainder = AddressField.from_bytes(inb[2:])
|
||||
d['tp_da'] = da
|
||||
|
||||
offset = 0
|
||||
@@ -303,12 +301,10 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
# TODO: further decode
|
||||
d['tp_vp'] = remainder[offset:offset+7]
|
||||
offset += 7
|
||||
pass
|
||||
elif d['tp_vpf'] == 'absolute':
|
||||
# TODO: further decode
|
||||
d['tp_vp'] = remainder[offset:offset+7]
|
||||
offset += 7
|
||||
pass
|
||||
else:
|
||||
raise ValueError('Invalid VPF: %s' % d['tp_vpf'])
|
||||
d['tp_udl'] = remainder[offset]
|
||||
@@ -316,7 +312,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
d['tp_ud'] = remainder[offset:]
|
||||
return cls(**d)
|
||||
|
||||
def toBytes(self) -> bytes:
|
||||
def to_bytes(self) -> bytes:
|
||||
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
|
||||
outb = bytearray()
|
||||
d = {
|
||||
@@ -326,7 +322,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
flags = SMS_SUBMIT.flags_construct.build(d)
|
||||
outb.extend(flags)
|
||||
outb.append(self.tp_mr)
|
||||
outb.extend(self.tp_da.toBytes())
|
||||
outb.extend(self.tp_da.to_bytes())
|
||||
outb.append(self.tp_pid)
|
||||
outb.append(self.tp_dcs)
|
||||
if self.tp_vpf != 'none':
|
||||
@@ -336,20 +332,20 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
return outb
|
||||
|
||||
@classmethod
|
||||
def fromSmpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||
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.fromSmppSubmit(smpp_pdu)
|
||||
return cls.from_smpp_submit(smpp_pdu)
|
||||
else:
|
||||
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
||||
|
||||
@classmethod
|
||||
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||
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.fromSmpp(smpp_pdu.params['destination_addr'],
|
||||
smpp_pdu.params['dest_addr_ton'],
|
||||
smpp_pdu.params['dest_addr_npi'])
|
||||
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:
|
||||
@@ -370,7 +366,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
}
|
||||
return cls(**d)
|
||||
|
||||
def toSmpp(self) -> pdu_types.PDU:
|
||||
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)
|
||||
@@ -382,7 +378,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
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.toSmpp()
|
||||
(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,
|
||||
|
||||
@@ -17,14 +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 pytlv.TLV import *
|
||||
from struct import pack, unpack
|
||||
from pySim.utils import *
|
||||
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, Const
|
||||
from construct import Optional as COptional
|
||||
from osmocom.utils import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.runtime import RuntimeState
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.construct import *
|
||||
from construct import *
|
||||
import pySim
|
||||
|
||||
key_type2str = {
|
||||
@@ -70,7 +72,7 @@ class EF_PIN(TransparentEF):
|
||||
'attempts_remaining'/Int8ub,
|
||||
'maximum_attempts'/Int8ub,
|
||||
'pin'/HexAdapter(Rpad(Bytes(8))),
|
||||
'puk'/Optional(PukStruct))
|
||||
'puk'/COptional(PukStruct))
|
||||
|
||||
|
||||
class EF_MILENAGE_CFG(TransparentEF):
|
||||
@@ -179,7 +181,7 @@ class DF_SYSTEM(CardDF):
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, resp_hex):
|
||||
return pySim.ts_102_221.CardProfileUICC.decode_select_response(resp_hex)
|
||||
return CardProfileUICC.decode_select_response(resp_hex)
|
||||
|
||||
|
||||
class EF_USIM_SQN(TransparentEF):
|
||||
@@ -205,6 +207,18 @@ class EF_USIM_SQN(TransparentEF):
|
||||
|
||||
|
||||
class EF_USIM_AUTH_KEY(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '141898d827f70120d33b3e7462ee5fd6fe6ca53d7a0a804561646816d7b0c702fb',
|
||||
{ "cfg": { "only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True, "algorithm": "milenage" },
|
||||
"key": "1898d827f70120d33b3e7462ee5fd6fe", "op_opc": "6ca53d7a0a804561646816d7b0c702fb" } ),
|
||||
( '160a04101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f000102030405060708090a0b0c0d0e0f',
|
||||
{ "cfg" : { "algorithm" : "tuak", "key_length" : 128, "sres_deriv_func_in_2g" : 1, "use_opc_instead_of_op" : True },
|
||||
"tuak_cfg" : { "ck_and_ik_size" : 128, "mac_size" : 128, "res_size" : 128 },
|
||||
"num_of_keccak_iterations" : 4,
|
||||
"k" : "000102030405060708090a0b0c0d0e0f",
|
||||
"op_opc" : "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
|
||||
} ),
|
||||
]
|
||||
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
|
||||
super().__init__(fid, name=name, desc='USIM authentication key')
|
||||
Algorithm = Enum(Nibble, milenage=4, sha1_aka=5, tuak=6, xor=15)
|
||||
@@ -239,7 +253,7 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
||||
else:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
|
||||
def _encode_bin(self, abstract_data: dict) -> bytearray:
|
||||
def _encode_bin(self, abstract_data: dict, **kwargs) -> bytearray:
|
||||
if abstract_data['cfg']['algorithm'] == 'tuak':
|
||||
return build_construct(self._constr_tuak, abstract_data)
|
||||
else:
|
||||
|
||||
458
pySim/tlv.py
458
pySim/tlv.py
@@ -1,458 +0,0 @@
|
||||
"""object-oriented TLV parser/encoder library."""
|
||||
|
||||
# (C) 2021 by Harald Welte <laforge@osmocom.org>
|
||||
# All Rights Reserved
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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_tag_raw, comprehensiontlv_parse_tag_raw
|
||||
|
||||
from pySim.construct import build_construct, parse_construct, LV, HexAdapter, BcdAdapter, BitsRFU, GsmStringAdapter
|
||||
from pySim.exceptions import *
|
||||
|
||||
import inspect
|
||||
import abc
|
||||
import re
|
||||
|
||||
def camel_to_snake(name):
|
||||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
||||
|
||||
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)
|
||||
# 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))
|
||||
nested = namespace.get('nested', kwargs.get('nested', None))
|
||||
if nested is None or inspect.isclass(nested) and issubclass(nested, TLV_IE_Collection):
|
||||
# caller has specified TLV_IE_Collection sub-class, we can directly reference it
|
||||
x.nested_collection_cls = nested
|
||||
else:
|
||||
# caller passed list of other TLV classes that might possibly appear within us,
|
||||
# build a dynamically-created TLV_IE_Collection sub-class and reference it
|
||||
name = 'auto_collection_%s' % (name)
|
||||
cls = type(name, (TLV_IE_Collection,), {'nested': nested})
|
||||
x.nested_collection_cls = cls
|
||||
return x
|
||||
|
||||
|
||||
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)
|
||||
# this becomes a _class_ variable, not an instance variable
|
||||
x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
|
||||
return x
|
||||
|
||||
|
||||
class Transcodable(abc.ABC):
|
||||
_construct = None
|
||||
"""Base class for something that can be encoded + encoded. Decoding and Encoding happens either
|
||||
* via a 'construct' object stored in a derived class' _construct variable, or
|
||||
* via a 'construct' object stored in an instance _construct variable, or
|
||||
* via a derived class' _{to,from}_bytes() methods."""
|
||||
|
||||
def __init__(self):
|
||||
self.encoded = None
|
||||
self.decoded = None
|
||||
self._construct = None
|
||||
|
||||
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 self.decoded == None:
|
||||
do = b''
|
||||
elif self._construct:
|
||||
do = build_construct(self._construct, self.decoded, context)
|
||||
elif self.__class__._construct:
|
||||
do = build_construct(self.__class__._construct, self.decoded, context)
|
||||
else:
|
||||
do = self._to_bytes()
|
||||
self.encoded = do
|
||||
return do
|
||||
|
||||
# not an abstractmethod, as it is only required if no _construct exists
|
||||
def _to_bytes(self):
|
||||
raise NotImplementedError('%s._to_bytes' % type(self).__name__)
|
||||
|
||||
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, context=context)
|
||||
elif self.__class__._construct:
|
||||
self.decoded = parse_construct(self.__class__._construct, do, context=context)
|
||||
else:
|
||||
self.decoded = self._from_bytes(do)
|
||||
return self.decoded
|
||||
|
||||
# not an abstractmethod, as it is only required if no _construct exists
|
||||
def _from_bytes(self, do: bytes):
|
||||
raise NotImplementedError('%s._from_bytes' % type(self).__name__)
|
||||
|
||||
|
||||
class IE(Transcodable, metaclass=TlvMeta):
|
||||
# we specify the metaclass so any downstream subclasses will automatically use it
|
||||
"""Base class for various Information Elements. We understand the notion of a hierarchy
|
||||
of IEs on top of the Transcodable class."""
|
||||
# this is overridden by the TlvMeta metaclass, if it is used to create subclasses
|
||||
nested_collection_cls = None
|
||||
tag = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
self.nested_collection = None
|
||||
if self.nested_collection_cls:
|
||||
self.nested_collection = self.nested_collection_cls()
|
||||
# if we are a constructed IE, [ordered] list of actual child-IE instances
|
||||
self.children = kwargs.get('children', [])
|
||||
self.decoded = kwargs.get('decoded', None)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representing the [nested] IE data (for print)."""
|
||||
if len(self.children):
|
||||
member_strs = [repr(x) for x in self.children]
|
||||
return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
|
||||
else:
|
||||
return '%s(%s)' % (type(self).__name__, self.decoded)
|
||||
|
||||
def to_dict(self):
|
||||
"""Return a JSON-serializable dict representing the [nested] IE data."""
|
||||
if len(self.children):
|
||||
v = [x.to_dict() for x in self.children]
|
||||
else:
|
||||
v = self.decoded
|
||||
return {camel_to_snake(type(self).__name__): v}
|
||||
|
||||
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[expected_key_name])
|
||||
else:
|
||||
self.children = []
|
||||
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
|
||||
|
||||
@abc.abstractmethod
|
||||
def to_ie(self, context: dict = {}) -> bytes:
|
||||
"""Convert the internal representation to entire IE including IE header."""
|
||||
|
||||
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(context=context)
|
||||
return out
|
||||
else:
|
||||
return super().to_bytes(context=context)
|
||||
|
||||
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, context=context)
|
||||
else:
|
||||
self.children = []
|
||||
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
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
"""Obtain the raw TAG at the start of the bytes provided by the user."""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
"""Obtain the length encoded at the start of the bytes provided by the user."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _encode_tag(self) -> bytes:
|
||||
"""Encode the tag part. Must be provided by derived (TLV format specific) class."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _encode_len(self, val: bytes) -> bytes:
|
||||
"""Encode the length part assuming a certain binary value. Must be provided by
|
||||
derived (TLV format specific) class."""
|
||||
|
||||
def to_ie(self, context: dict = {}):
|
||||
return self.to_tlv(context=context)
|
||||
|
||||
def to_tlv(self, context: dict = {}):
|
||||
"""Convert the internal representation to binary TLV bytes."""
|
||||
val = self.to_bytes(context=context)
|
||||
return self._encode_tag() + self._encode_len(val) + val
|
||||
|
||||
def from_tlv(self, do: bytes, context: dict = {}):
|
||||
if len(do) == 0:
|
||||
return {}, b''
|
||||
(rawtag, remainder) = self.__class__._parse_tag_raw(do)
|
||||
if rawtag:
|
||||
if rawtag != self._compute_tag():
|
||||
raise ValueError("%s: Encountered tag %s doesn't match our supported tag %s" %
|
||||
(self, rawtag, self.tag))
|
||||
(length, remainder) = self.__class__._parse_len(remainder)
|
||||
value = remainder[:length]
|
||||
remainder = remainder[length:]
|
||||
else:
|
||||
value = do
|
||||
remainder = b''
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
return bertlv_parse_tag_raw(do)
|
||||
|
||||
@classmethod
|
||||
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
return bertlv_parse_len(do)
|
||||
|
||||
def _encode_tag(self) -> bytes:
|
||||
return bertlv_encode_tag(self._compute_tag())
|
||||
|
||||
def _encode_len(self, val: bytes) -> bytes:
|
||||
return bertlv_encode_len(len(val))
|
||||
|
||||
|
||||
class COMPR_TLV_IE(TLV_IE):
|
||||
"""TLV_IE formated as COMPREHENSION-TLV as described in ETSI TS 101 220."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.comprehension = False
|
||||
|
||||
@classmethod
|
||||
def _decode_tag(cls, do: bytes) -> Tuple[dict, bytes]:
|
||||
return comprehensiontlv_parse_tag(do)
|
||||
|
||||
@classmethod
|
||||
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
return comprehensiontlv_parse_tag_raw(do)
|
||||
|
||||
@classmethod
|
||||
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
return bertlv_parse_len(do)
|
||||
|
||||
def _encode_tag(self) -> bytes:
|
||||
return comprehensiontlv_encode_tag(self._compute_tag())
|
||||
|
||||
def _encode_len(self, val: bytes) -> bytes:
|
||||
return bertlv_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.
|
||||
A given encoded DO may contain any of them in any order, and may contain multiple instances
|
||||
of each DO."""
|
||||
# this is overridden by the TlvCollectionMeta metaclass, if it is used to create subclasses
|
||||
possible_nested = []
|
||||
|
||||
def __init__(self, desc=None, **kwargs):
|
||||
self.desc = desc
|
||||
#print("possible_nested: ", self.possible_nested)
|
||||
self.members = kwargs.get('nested', self.possible_nested)
|
||||
self.members_by_tag = {}
|
||||
self.members_by_name = {}
|
||||
self.members_by_tag = {m.tag: 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
|
||||
|
||||
def __str__(self):
|
||||
member_strs = [str(x) for x in self.members]
|
||||
return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
|
||||
|
||||
def __repr__(self):
|
||||
member_strs = [repr(x) for x in self.members]
|
||||
return '%s(%s)' % (self.__class__, ','.join(member_strs))
|
||||
|
||||
def __add__(self, other):
|
||||
"""Extending TLV_IE_Collections with other TLV_IE_Collections or TLV_IEs."""
|
||||
if isinstance(other, TLV_IE_Collection):
|
||||
# adding one collection to another
|
||||
members = self.members + other.members
|
||||
return TLV_IE_Collection(self.desc, nested=members)
|
||||
elif inspect.isclass(other) and issubclass(other, TLV_IE):
|
||||
# adding a member to a collection
|
||||
return TLV_IE_Collection(self.desc, nested=self.members + [other])
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
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
|
||||
Returns:
|
||||
list of instances of TLV_IE sub-classes containing parsed data
|
||||
"""
|
||||
self.encoded = binary
|
||||
# list of instances of TLV_IE collection member classes appearing in the data
|
||||
res = []
|
||||
remainder = binary
|
||||
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:
|
||||
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, context=context)
|
||||
res.append(inst)
|
||||
else:
|
||||
# unknown tag; create the related class on-the-fly using the same base class
|
||||
name = 'unknown_%s_%X' % (first.__base__.__name__, tag)
|
||||
cls = type(name, (first.__base__,), {'tag': tag, 'possible_nested': [],
|
||||
'nested_collection_cls': None})
|
||||
cls._from_bytes = lambda s, a: {'raw': a.hex()}
|
||||
cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
|
||||
# create an instance and parse accordingly
|
||||
inst = cls()
|
||||
dec, remainder = inst.from_tlv(remainder, context=context)
|
||||
res.append(inst)
|
||||
self.children = res
|
||||
return res
|
||||
|
||||
def from_dict(self, decoded: List[dict]) -> List[TLV_IE]:
|
||||
"""Create a list of TLV_IE instances from the collection based on an array
|
||||
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()
|
||||
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, 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, context: dict = {}):
|
||||
out = b''
|
||||
context['siblings'] = self.children
|
||||
for c in self.children:
|
||||
out += c.to_tlv(context=context)
|
||||
return out
|
||||
|
||||
def from_tlv(self, do, context: dict = {}):
|
||||
return self.from_bytes(do, context=context)
|
||||
|
||||
def to_tlv(self, context: dict = {}):
|
||||
return self.to_bytes(context=context)
|
||||
|
||||
|
||||
def flatten_dict_lists(inp):
|
||||
"""hierarchically flatten each list-of-dicts into a single dict. This is useful to
|
||||
make the output of hierarchical TLV decoder structures flatter and more easy to read."""
|
||||
def are_all_elements_dict(l):
|
||||
for e in l:
|
||||
if not isinstance(e, dict):
|
||||
return False
|
||||
return True
|
||||
|
||||
def are_elements_unique(lod):
|
||||
set_of_keys = set([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) and are_elements_unique(inp):
|
||||
# flatten into one shared dict
|
||||
newdict = {}
|
||||
for e in inp:
|
||||
key = list(e.keys())[0]
|
||||
newdict[key] = e[key]
|
||||
inp = newdict
|
||||
# process result as any native dict
|
||||
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
|
||||
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()}
|
||||
else:
|
||||
return inp
|
||||
@@ -8,10 +8,10 @@ import abc
|
||||
import argparse
|
||||
from typing import Optional, Tuple
|
||||
from construct import Construct
|
||||
from osmocom.utils import b2h, h2b, i2h, Hexstr
|
||||
|
||||
from pySim.exceptions import *
|
||||
from pySim.construct import filter_dict
|
||||
from pySim.utils import sw_match, b2h, h2b, i2h, Hexstr, SwHexstr, SwMatchstr, ResTuple
|
||||
from pySim.utils import SwHexstr, SwMatchstr, ResTuple, sw_match, parse_command_apdu
|
||||
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
|
||||
|
||||
#
|
||||
@@ -40,6 +40,18 @@ class ApduTracer:
|
||||
def trace_response(self, cmd, sw, resp):
|
||||
pass
|
||||
|
||||
def trace_reset(self):
|
||||
pass
|
||||
|
||||
class StdoutApduTracer(ApduTracer):
|
||||
"""Minimalistic APDU tracer, printing commands to stdout."""
|
||||
def trace_response(self, cmd, sw, resp):
|
||||
print("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||
print("<- %s: %s" % (sw, resp))
|
||||
|
||||
def trace_reset(self):
|
||||
print("-- RESET")
|
||||
|
||||
class ProactiveHandler(abc.ABC):
|
||||
"""Abstract base class representing the interface of some code that handles
|
||||
the proactive commands, as returned by the card in responses to the FETCH
|
||||
@@ -57,7 +69,18 @@ class ProactiveHandler(abc.ABC):
|
||||
"""Default handler for not otherwise handled proactive commands."""
|
||||
raise NotImplementedError('No handler method for %s' % pcmd.decoded)
|
||||
|
||||
|
||||
def prepare_response(self, pcmd: ProactiveCommand, general_result: str = 'performed_successfully'):
|
||||
# The Command Details are echoed from the command that has been processed.
|
||||
(command_details,) = [c for c in pcmd.children if isinstance(c, CommandDetails)]
|
||||
# invert the device identities
|
||||
(command_dev_ids,) = [c for c in pcmd.children if isinstance(c, DeviceIdentities)]
|
||||
rsp_dev_ids = DeviceIdentities()
|
||||
rsp_dev_ids.from_dict({'device_identities': {
|
||||
'dest_dev_id': command_dev_ids.decoded['source_dev_id'],
|
||||
'source_dev_id': command_dev_ids.decoded['dest_dev_id']}})
|
||||
result = Result()
|
||||
result.from_dict({'result': {'general_result': general_result, 'additional_information': ''}})
|
||||
return [command_details, rsp_dev_ids, result]
|
||||
|
||||
class LinkBase(abc.ABC):
|
||||
"""Base class for link/transport to card."""
|
||||
@@ -67,14 +90,16 @@ class LinkBase(abc.ABC):
|
||||
self.sw_interpreter = sw_interpreter
|
||||
self.apdu_tracer = apdu_tracer
|
||||
self.proactive_handler = proactive_handler
|
||||
self.apdu_strict = False
|
||||
|
||||
@abc.abstractmethod
|
||||
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 _send_apdu(self, apdu: Hexstr) -> ResTuple:
|
||||
"""Implementation specific method for sending the APDU. This method must accept APDUs as defined in
|
||||
ISO/IEC 7816-3, section 12.1 """
|
||||
|
||||
def set_sw_interpreter(self, interp):
|
||||
"""Set an (optional) status word interpreter."""
|
||||
@@ -100,62 +125,62 @@ class LinkBase(abc.ABC):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset_card(self):
|
||||
def _reset_card(self):
|
||||
"""Resets the card (power down/up)
|
||||
"""
|
||||
|
||||
def send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
def reset_card(self):
|
||||
"""Resets the card (power down/up)
|
||||
"""
|
||||
if self.apdu_tracer:
|
||||
self.apdu_tracer.trace_reset()
|
||||
return self._reset_card()
|
||||
|
||||
def send_apdu(self, apdu: Hexstr) -> ResTuple:
|
||||
"""Sends an APDU with minimal processing
|
||||
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
apdu : string of hexadecimal characters (ex. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12.1)
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
|
||||
# To make sure that no invalid APDUs can be passed further down into the transport layer, we parse the APDU.
|
||||
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
|
||||
|
||||
if self.apdu_tracer:
|
||||
self.apdu_tracer.trace_command(pdu)
|
||||
(data, sw) = self._send_apdu_raw(pdu)
|
||||
self.apdu_tracer.trace_command(apdu)
|
||||
|
||||
# Handover APDU to concrete transport layer implementation
|
||||
(data, sw) = self._send_apdu(apdu)
|
||||
|
||||
if self.apdu_tracer:
|
||||
self.apdu_tracer.trace_response(pdu, sw, data)
|
||||
self.apdu_tracer.trace_response(apdu, sw, data)
|
||||
|
||||
# The APDU case (See aso ISO/IEC 7816-3, table 12) dictates if we should receive a response or not. If we
|
||||
# receive a response in an APDU case that does not allow the reception of a respnse we print a warning to
|
||||
# make the user/caller aware of the problem. Since the transaction is over at this point and data was received
|
||||
# we count it as a successful transaction anyway, even though the spec was violated. The problem is most likely
|
||||
# caused by a missing Le field in the APDU. This is an error that the caller/user should correct to avoid
|
||||
# problems at some later point when a different transport protocol or transport layer implementation is used.
|
||||
# All APDUs passed to this function must comply to ISO/IEC 7816-3, section 12.
|
||||
if len(data) > 0 and (case == 3 or case == 1):
|
||||
exeption_str = 'received unexpected response data, incorrect APDU-case ' + \
|
||||
'(%d, should be %d, missing Le field?)!' % (case, case + 1)
|
||||
if self.apdu_strict:
|
||||
raise ValueError(exeption_str)
|
||||
else:
|
||||
print('Warning: %s' % exeption_str)
|
||||
|
||||
return (data, sw)
|
||||
|
||||
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")
|
||||
"""
|
||||
data, sw = self.send_apdu_raw(pdu)
|
||||
|
||||
# When we have sent the first APDU, the SW may indicate that there are response bytes
|
||||
# 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):
|
||||
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]
|
||||
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]
|
||||
data, sw = self.send_apdu_raw(pdu_gr)
|
||||
|
||||
return data, sw
|
||||
|
||||
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
|
||||
def send_apdu_checksw(self, apdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
|
||||
"""Sends an APDU and check returned SW
|
||||
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
apdu : string of hexadecimal characters (ex. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12.1)
|
||||
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
|
||||
digits using a '?' to add some ambiguity if needed.
|
||||
Returns:
|
||||
@@ -163,7 +188,7 @@ class LinkBase(abc.ABC):
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
rv = self.send_apdu(pdu)
|
||||
rv = self.send_apdu(apdu)
|
||||
last_sw = rv[1]
|
||||
|
||||
while sw == '9000' and sw_match(last_sw, '91xx'):
|
||||
@@ -182,31 +207,26 @@ class LinkBase(abc.ABC):
|
||||
pcmd = ProactiveCommand()
|
||||
parsed = pcmd.from_tlv(h2b(fetch_rv[0]))
|
||||
print("FETCH: %s (%s)" % (fetch_rv[0], type(parsed).__name__))
|
||||
result = Result()
|
||||
if self.proactive_handler:
|
||||
# Extension point: If this does return a list of TLV objects,
|
||||
# they could be appended after the Result; if the first is a
|
||||
# Result, that cuold replace the one built here.
|
||||
self.proactive_handler.receive_fetch_raw(pcmd, parsed)
|
||||
result.from_dict({'general_result': 'performed_successfully', 'additional_information': ''})
|
||||
ti_list = self.proactive_handler.receive_fetch_raw(pcmd, parsed)
|
||||
if not ti_list:
|
||||
ti_list = self.proactive_handler.prepare_response(pcmd, 'FIXME')
|
||||
else:
|
||||
result.from_dict({'general_result': 'command_beyond_terminal_capability', 'additional_information': ''})
|
||||
ti_list = self.proactive_handler.prepare_response(pcmd, 'command_beyond_terminal_capability')
|
||||
|
||||
# Send response immediately, thus also flushing out any further
|
||||
# proactive commands that the card already wants to send
|
||||
#
|
||||
# Structure as per TS 102 223 V4.4.0 Section 6.8
|
||||
|
||||
# The Command Details are echoed from the command that has been processed.
|
||||
(command_details,) = [c for c in pcmd.decoded.children if isinstance(c, CommandDetails)]
|
||||
# The Device Identities are fixed. (TS 102 223 V4.0.0 Section 6.8.2)
|
||||
device_identities = DeviceIdentities()
|
||||
device_identities.from_dict({'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'})
|
||||
|
||||
# Testing hint: The value of tail does not influence the behavior
|
||||
# of an SJA2 that sent ans SMS, so this is implemented only
|
||||
# following TS 102 223, and not fully tested.
|
||||
tail = command_details.to_tlv() + device_identities.to_tlv() + result.to_tlv()
|
||||
ti_list_bin = [x.to_tlv() for x in ti_list]
|
||||
tail = b''.join(ti_list_bin)
|
||||
# Testing hint: In contrast to the above, this part is positively
|
||||
# essential to get the SJA2 to provide the later parts of a
|
||||
# multipart SMS in response to an OTA RFM command.
|
||||
@@ -219,55 +239,88 @@ class LinkBase(abc.ABC):
|
||||
raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter)
|
||||
return rv
|
||||
|
||||
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.
|
||||
|
||||
class LinkBaseTpdu(LinkBase):
|
||||
|
||||
# Use the T=0 TPDU format by default as this is the most commonly used transport protocol.
|
||||
protocol = 0
|
||||
|
||||
def set_tpdu_format(self, protocol: int):
|
||||
"""Set TPDU format. Each transport protocol has its specific TPDU format. This method allows the
|
||||
concrete transport layer implementation to set the TPDU format it expects. (This method must not be
|
||||
called by higher layers. Switching the TPDU format does not switch the transport protocol that the
|
||||
reader uses on the wire)
|
||||
|
||||
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)
|
||||
protocol : number of the transport protocol used. (0 => T=0, 1 => T=1)
|
||||
"""
|
||||
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)))
|
||||
self.protocol = protocol
|
||||
|
||||
@abc.abstractmethod
|
||||
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
|
||||
"""Implementation specific method for sending the resulting TPDU. This method must accept TPDUs as defined in
|
||||
ETSI TS 102 221, section 7.3.1 and 7.3.2, depending on the protocol selected. """
|
||||
|
||||
def _send_apdu(self, apdu: Hexstr) -> ResTuple:
|
||||
"""Transforms APDU into a TPDU and sends it. The response TPDU is returned as APDU back to the caller.
|
||||
|
||||
Args:
|
||||
apdu : string of hexadecimal characters (eg. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12)
|
||||
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.protocol == 0:
|
||||
return self.__send_apdu_T0(apdu)
|
||||
elif self.protocol == 1:
|
||||
return self.__send_apdu_transparent(apdu)
|
||||
raise ValueError('unspported protocol selected (T=%d)' % self.protocol)
|
||||
|
||||
def __send_apdu_T0(self, apdu: Hexstr) -> ResTuple:
|
||||
# Transform the given APDU to the T=0 TPDU format and send it. Automatically fetch the response (case #4 APDUs)
|
||||
# (see also ETSI TS 102 221, section 7.3.1.1)
|
||||
|
||||
# Transform APDU to T=0 TPDU (see also ETSI TS 102 221, section 7.3.1)
|
||||
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
|
||||
|
||||
if case == 1:
|
||||
# Attach an Le field to all case #1 APDUs (see also ETSI TS 102 221, section 7.3.1.1.1)
|
||||
tpdu = apdu + '00'
|
||||
elif case == 4:
|
||||
# Remove the Le field from all case #4 APDUs (see also ETSI TS 102 221, section 7.3.1.1.4)
|
||||
tpdu = apdu[:-2]
|
||||
else:
|
||||
rsp = None
|
||||
return (rsp, sw)
|
||||
tpdu = apdu
|
||||
|
||||
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.
|
||||
prev_tpdu = tpdu
|
||||
data, sw = self.send_tpdu(tpdu)
|
||||
|
||||
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)
|
||||
# When we have sent the first APDU, the SW may indicate that there are response bytes
|
||||
# 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:
|
||||
while (sw[0:2] in ['9f', '61', '62', '63']):
|
||||
# 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
|
||||
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
|
||||
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
|
||||
prev_tpdu = tpdu_gr
|
||||
d, sw = self.send_tpdu(tpdu_gr)
|
||||
data += d
|
||||
if sw[0:2] == '6c':
|
||||
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
||||
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
|
||||
data, sw = self.send_tpdu(tpdu_gr)
|
||||
|
||||
return data, sw
|
||||
|
||||
def __send_apdu_transparent(self, apdu: Hexstr) -> ResTuple:
|
||||
# In cases where the TPDU format is the same as the APDU format, we may pass the given APDU through without modification
|
||||
# (This is the case for T=1, see also ETSI TS 102 221, section 7.3.2.0.)
|
||||
return self.send_tpdu(apdu)
|
||||
|
||||
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
||||
"""Add all reader related arguments to the given argparse.Argumentparser instance."""
|
||||
@@ -275,11 +328,15 @@ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
||||
from pySim.transport.pcsc import PcscSimLink
|
||||
from pySim.transport.modem_atcmd import ModemATCommandLink
|
||||
from pySim.transport.calypso import CalypsoSimLink
|
||||
from pySim.transport.wsrc import WsrcSimLink
|
||||
|
||||
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)
|
||||
WsrcSimLink.argparse_add_reader_args(arg_parser)
|
||||
arg_parser.add_argument('--apdu-trace', action='store_true',
|
||||
help='Trace the command/response APDUs exchanged with the card')
|
||||
|
||||
return arg_parser
|
||||
|
||||
@@ -288,6 +345,9 @@ def init_reader(opts, **kwargs) -> LinkBase:
|
||||
"""
|
||||
Init card reader driver
|
||||
"""
|
||||
if opts.apdu_trace and not 'apdu_tracer' in kwargs:
|
||||
kwargs['apdu_tracer'] = StdoutApduTracer()
|
||||
|
||||
if opts.pcsc_dev is not None or opts.pcsc_regex is not None:
|
||||
from pySim.transport.pcsc import PcscSimLink
|
||||
sl = PcscSimLink(opts, **kwargs)
|
||||
@@ -297,6 +357,9 @@ def init_reader(opts, **kwargs) -> LinkBase:
|
||||
elif opts.modem_dev is not None:
|
||||
from pySim.transport.modem_atcmd import ModemATCommandLink
|
||||
sl = ModemATCommandLink(opts, **kwargs)
|
||||
elif opts.wsrc_server_url is not None:
|
||||
from pySim.transport.wsrc import WsrcSimLink
|
||||
sl = WsrcSimLink(opts, **kwargs)
|
||||
else: # Serial reader is default
|
||||
print("No reader/driver specified; falling back to default (Serial reader)")
|
||||
from pySim.transport.serial import SerialSimLink
|
||||
|
||||
@@ -22,10 +22,11 @@ import socket
|
||||
import os
|
||||
import argparse
|
||||
from typing import Optional
|
||||
from osmocom.utils import h2b, b2h, Hexstr
|
||||
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.exceptions import *
|
||||
from pySim.utils import h2b, b2h, Hexstr, ResTuple
|
||||
from pySim.transport import LinkBaseTpdu
|
||||
from pySim.exceptions import ReaderError, ProtocolError
|
||||
from pySim.utils import ResTuple
|
||||
|
||||
|
||||
class L1CTLMessage:
|
||||
@@ -58,9 +59,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):
|
||||
@@ -69,12 +70,12 @@ class L1CTLMessageSIM(L1CTLMessage):
|
||||
L1CTL_SIM_REQ = 0x16
|
||||
L1CTL_SIM_CONF = 0x17
|
||||
|
||||
def __init__(self, pdu):
|
||||
super(L1CTLMessageSIM, self).__init__(self.L1CTL_SIM_REQ)
|
||||
self.data += pdu
|
||||
def __init__(self, tpdu):
|
||||
super().__init__(self.L1CTL_SIM_REQ)
|
||||
self.data += tpdu
|
||||
|
||||
|
||||
class CalypsoSimLink(LinkBase):
|
||||
class CalypsoSimLink(LinkBaseTpdu):
|
||||
"""Transport Link for Calypso based phones."""
|
||||
name = 'Calypso-based (OsmocomBB) reader'
|
||||
|
||||
@@ -108,7 +109,7 @@ class CalypsoSimLink(LinkBase):
|
||||
rsp = self.sock.recv(exp_len)
|
||||
return rsp
|
||||
|
||||
def reset_card(self):
|
||||
def _reset_card(self):
|
||||
# Request FULL reset
|
||||
req_msg = L1CTLMessageReset()
|
||||
self.sock.send(req_msg.gen_msg())
|
||||
@@ -128,10 +129,10 @@ class CalypsoSimLink(LinkBase):
|
||||
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
|
||||
|
||||
# Request FULL reset
|
||||
req_msg = L1CTLMessageSIM(h2b(pdu))
|
||||
# Request sending of TPDU
|
||||
req_msg = L1CTLMessageSIM(h2b(tpdu))
|
||||
self.sock.send(req_msg.gen_msg())
|
||||
|
||||
# Read message length first
|
||||
|
||||
@@ -17,21 +17,22 @@
|
||||
#
|
||||
|
||||
import logging as log
|
||||
import serial
|
||||
import time
|
||||
import re
|
||||
import argparse
|
||||
from typing import Optional
|
||||
import serial
|
||||
from osmocom.utils import Hexstr
|
||||
|
||||
from pySim.utils import Hexstr, ResTuple
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.exceptions import *
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.transport import LinkBaseTpdu
|
||||
from pySim.exceptions import ReaderError, ProtocolError
|
||||
|
||||
# HACK: if somebody needs to debug this thing
|
||||
# log.root.setLevel(log.DEBUG)
|
||||
|
||||
|
||||
class ModemATCommandLink(LinkBase):
|
||||
class ModemATCommandLink(LinkBaseTpdu):
|
||||
"""Transport Link for 3GPP TS 27.007 compliant modems."""
|
||||
name = "modem for Generic SIM Access (3GPP TS 27.007)"
|
||||
|
||||
@@ -57,7 +58,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
|
||||
@@ -67,9 +68,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
|
||||
@@ -91,8 +92,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
|
||||
@@ -120,13 +120,12 @@ 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):
|
||||
def _reset_card(self):
|
||||
# Reset the modem, just to be sure
|
||||
if self.send_at_cmd('ATZ') != [b'OK']:
|
||||
raise ReaderError('Failed to reset the modem')
|
||||
@@ -135,7 +134,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 ...
|
||||
@@ -146,12 +145,12 @@ class ModemATCommandLink(LinkBase):
|
||||
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
|
||||
# Make sure pdu has upper case hex digits [A-F]
|
||||
pdu = pdu.upper()
|
||||
tpdu = tpdu.upper()
|
||||
|
||||
# Prepare the command as described in 8.17
|
||||
cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)
|
||||
cmd = 'AT+CSIM=%d,\"%s\"' % (len(tpdu), tpdu)
|
||||
log.debug('Sending command: %s', cmd)
|
||||
|
||||
# Send AT+CSIM command to the modem
|
||||
@@ -165,14 +164,14 @@ 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_tpdu_len, rsp_tpdu) = 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)
|
||||
data = rsp_tpdu[:-4].decode().lower()
|
||||
sw = rsp_tpdu[-4:].decode().lower()
|
||||
log.debug('Command response: %s, %s', data, sw)
|
||||
return data, sw
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -19,19 +19,22 @@
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
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 smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
|
||||
|
||||
from osmocom.utils import h2i, i2h, Hexstr
|
||||
|
||||
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.utils import h2i, i2h, Hexstr, ResTuple
|
||||
from pySim.transport import LinkBaseTpdu
|
||||
from pySim.utils import ResTuple
|
||||
|
||||
|
||||
class PcscSimLink(LinkBase):
|
||||
class PcscSimLink(LinkBaseTpdu):
|
||||
""" pySim: PCSC reader transport link."""
|
||||
name = 'PC/SC'
|
||||
|
||||
@@ -56,6 +59,8 @@ class PcscSimLink(LinkBase):
|
||||
raise ReaderError('No matching reader found for regex %s' % opts.pcsc_regex)
|
||||
|
||||
self._con = self._reader.createConnection()
|
||||
if not getattr(opts, "pcsc_shared", False):
|
||||
self._con = ExclusiveConnectCardConnection(self._con)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
@@ -63,15 +68,14 @@ class PcscSimLink(LinkBase):
|
||||
self._con.disconnect()
|
||||
except:
|
||||
pass
|
||||
return
|
||||
|
||||
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):
|
||||
@@ -80,12 +84,23 @@ class PcscSimLink(LinkBase):
|
||||
# is disconnected
|
||||
self.disconnect()
|
||||
|
||||
# Explicitly select T=0 communication protocol
|
||||
self._con.connect(CardConnection.T0_protocol)
|
||||
except CardConnectionException:
|
||||
raise ProtocolError()
|
||||
except NoCardException:
|
||||
raise NoCardError()
|
||||
# Make card connection and select a suitable communication protocol
|
||||
self._con.connect()
|
||||
supported_protocols = self._con.getProtocol();
|
||||
self.disconnect()
|
||||
if (supported_protocols & CardConnection.T0_protocol):
|
||||
protocol = CardConnection.T0_protocol
|
||||
self.set_tpdu_format(0)
|
||||
elif (supported_protocols & CardConnection.T1_protocol):
|
||||
protocol = CardConnection.T1_protocol
|
||||
self.set_tpdu_format(1)
|
||||
else:
|
||||
raise ReaderError('Unsupported card protocol')
|
||||
self._con.connect(protocol)
|
||||
except CardConnectionException as exc:
|
||||
raise ProtocolError() from exc
|
||||
except NoCardException as exc:
|
||||
raise NoCardError() from exc
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return self._con.getATR()
|
||||
@@ -93,17 +108,13 @@ class PcscSimLink(LinkBase):
|
||||
def disconnect(self):
|
||||
self._con.disconnect()
|
||||
|
||||
def reset_card(self):
|
||||
def _reset_card(self):
|
||||
self.disconnect()
|
||||
self.connect()
|
||||
return 1
|
||||
|
||||
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
|
||||
apdu = h2i(pdu)
|
||||
|
||||
data, sw1, sw2 = self._con.transmit(apdu)
|
||||
|
||||
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
|
||||
data, sw1, sw2 = self._con.transmit(h2i(tpdu))
|
||||
sw = [sw1, sw2]
|
||||
|
||||
# Return value
|
||||
@@ -120,6 +131,8 @@ access smart card readers, and is available on a variety of operating systems, s
|
||||
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. """)
|
||||
pcsc_group.add_argument('--pcsc-shared', action='store_true',
|
||||
help='Open PC/SC reaer in SHARED access (default: EXCLUSIVE)')
|
||||
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')
|
||||
|
||||
@@ -16,18 +16,19 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import serial
|
||||
import time
|
||||
import os
|
||||
import argparse
|
||||
from typing import Optional
|
||||
import serial
|
||||
from osmocom.utils import h2b, b2h, Hexstr
|
||||
|
||||
from pySim.exceptions import NoCardError, ProtocolError
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.utils import h2b, b2h, Hexstr, ResTuple
|
||||
from pySim.transport import LinkBaseTpdu
|
||||
from pySim.utils import ResTuple
|
||||
|
||||
|
||||
class SerialSimLink(LinkBase):
|
||||
class SerialSimLink(LinkBaseTpdu):
|
||||
""" pySim: Transport Link for serial (RS232) based readers included with simcard"""
|
||||
name = 'Serial'
|
||||
|
||||
@@ -51,7 +52,7 @@ class SerialSimLink(LinkBase):
|
||||
self._atr = None
|
||||
|
||||
def __del__(self):
|
||||
if (hasattr(self, "_sl")):
|
||||
if hasattr(self, "_sl"):
|
||||
self._sl.close()
|
||||
|
||||
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
||||
@@ -62,8 +63,7 @@ class SerialSimLink(LinkBase):
|
||||
self.reset_card()
|
||||
if not newcardonly:
|
||||
return
|
||||
else:
|
||||
existing = True
|
||||
existing = True
|
||||
except NoCardError:
|
||||
pass
|
||||
|
||||
@@ -86,7 +86,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 ...
|
||||
@@ -101,15 +101,15 @@ class SerialSimLink(LinkBase):
|
||||
def disconnect(self):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
def reset_card(self):
|
||||
rv = self._reset_card()
|
||||
def _reset_card(self):
|
||||
rv = self.__reset_card()
|
||||
if rv == 0:
|
||||
raise NoCardError()
|
||||
elif rv < 0:
|
||||
if rv < 0:
|
||||
raise ProtocolError()
|
||||
return rv
|
||||
|
||||
def _reset_card(self):
|
||||
def __reset_card(self):
|
||||
self._atr = None
|
||||
rst_meth_map = {
|
||||
'rts': self._sl.setRTS,
|
||||
@@ -120,8 +120,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
|
||||
@@ -187,13 +187,13 @@ class SerialSimLink(LinkBase):
|
||||
def _rx_byte(self):
|
||||
return self._sl.read()
|
||||
|
||||
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
|
||||
|
||||
pdu = h2b(pdu)
|
||||
data_len = pdu[4] # P3
|
||||
tpdu = h2b(tpdu)
|
||||
data_len = tpdu[4] # P3
|
||||
|
||||
# Send first CLASS,INS,P1,P2,P3
|
||||
self._tx_string(pdu[0:5])
|
||||
self._tx_string(tpdu[0:5])
|
||||
|
||||
# Wait ack which can be
|
||||
# - INS: Command acked -> go ahead
|
||||
@@ -201,9 +201,9 @@ class SerialSimLink(LinkBase):
|
||||
# - SW1: The card can apparently proceed ...
|
||||
while True:
|
||||
b = self._rx_byte()
|
||||
if ord(b) == pdu[1]:
|
||||
if ord(b) == tpdu[1]:
|
||||
break
|
||||
elif b != '\x60':
|
||||
if b != '\x60':
|
||||
# Ok, it 'could' be SW1
|
||||
sw1 = b
|
||||
sw2 = self._rx_byte()
|
||||
@@ -214,15 +214,15 @@ class SerialSimLink(LinkBase):
|
||||
raise ProtocolError()
|
||||
|
||||
# Send data (if any)
|
||||
if len(pdu) > 5:
|
||||
self._tx_string(pdu[5:])
|
||||
if len(tpdu) > 5:
|
||||
self._tx_string(tpdu[5:])
|
||||
|
||||
# Receive data (including SW !)
|
||||
# length = [P3 - tx_data (=len(pdu)-len(hdr)) + 2 (SW1//2) ]
|
||||
to_recv = data_len - len(pdu) + 5 + 2
|
||||
# length = [P3 - tx_data (=len(tpdu)-len(hdr)) + 2 (SW1//2) ]
|
||||
to_recv = data_len - len(tpdu) + 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
|
||||
|
||||
99
pySim/transport/wsrc.py
Normal file
99
pySim/transport/wsrc.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# Copyright (C) 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
|
||||
# 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 typing import Optional
|
||||
|
||||
from osmocom.utils import h2i, i2h, Hexstr, is_hexstr
|
||||
|
||||
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.wsrc import WSRC_DEFAULT_PORT_USER
|
||||
from pySim.wsrc.client_blocking import WsClientBlocking
|
||||
|
||||
class UserWsClientBlocking(WsClientBlocking):
|
||||
def __init__(self, ws_uri: str, **kwargs):
|
||||
super().__init__('user', ws_uri, **kwargs)
|
||||
|
||||
def select_card(self, id_type:str, id_str:str):
|
||||
rx = self.transceive_json('select_card', {'id_type': id_type, 'id_str': id_str},
|
||||
'select_card_ack')
|
||||
return rx
|
||||
|
||||
def reset_card(self):
|
||||
self.transceive_json('reset_req', {}, 'reset_resp')
|
||||
|
||||
def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
|
||||
rx = self.transceive_json('c_apdu', {'command': cmd}, 'r_apdu')
|
||||
return rx['response'], rx['sw']
|
||||
|
||||
|
||||
class WsrcSimLink(LinkBase):
|
||||
""" pySim: WSRC (WebSocket Remote Card) reader transport link."""
|
||||
name = 'WSRC'
|
||||
|
||||
def __init__(self, opts: argparse.Namespace, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.identities = {}
|
||||
self.server_url = opts.wsrc_server_url
|
||||
if opts.wsrc_eid:
|
||||
self.id_type = 'eid'
|
||||
self.id_str = opts.wsrc_eid
|
||||
elif opts.wsrc_iccid:
|
||||
self.id_type = 'iccid'
|
||||
self.id_str = opts.wsrc_iccid
|
||||
self.client = UserWsClientBlocking(self.server_url)
|
||||
self.client.connect()
|
||||
|
||||
def __del__(self):
|
||||
# FIXME: disconnect from server
|
||||
pass
|
||||
|
||||
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
rx = self.client.select_card(self.id_type, self.id_str)
|
||||
self.identities = rx['identities']
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return h2i(self.identities['ATR'])
|
||||
|
||||
def disconnect(self):
|
||||
self.__delete__()
|
||||
|
||||
def _reset_card(self):
|
||||
self.client.reset_card()
|
||||
return 1
|
||||
|
||||
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
return self.client.xceive_apdu_raw(pdu)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "WSRC[%s=%s]" % (self.id_type, self.id_str)
|
||||
|
||||
@staticmethod
|
||||
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
||||
wsrc_group = arg_parser.add_argument_group('WebSocket Remote Card',
|
||||
"""WebSocket Remote Card (WSRC) is a protoocl by which remot cards / card readers
|
||||
can be accessed via a network.""")
|
||||
wsrc_group.add_argument('--wsrc-server-url', default='ws://localhost:%u' % WSRC_DEFAULT_PORT_USER,
|
||||
help='URI of the WSRC server to connect to')
|
||||
wsrc_group.add_argument('--wsrc-iccid', type=is_hexstr,
|
||||
help='ICCID of the card to open via WSRC')
|
||||
wsrc_group.add_argument('--wsrc-eid', type=is_hexstr,
|
||||
help='EID of the card to open via WSRC')
|
||||
@@ -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,24 +16,18 @@ 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 construct import *
|
||||
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
|
||||
|
||||
# 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, AddonSIM
|
||||
from pySim.gsm_r import AddonGSMR
|
||||
from pySim.cdma_ruim import AddonRUIM
|
||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, ValidationError
|
||||
from construct import Optional as COptional, Computed
|
||||
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import BER_TLV_IE, flatten_dict_lists
|
||||
from pySim.utils import *
|
||||
|
||||
#from pySim.filesystem import *
|
||||
#from pySim.profile import CardProfile
|
||||
|
||||
ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
|
||||
# TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte"
|
||||
@@ -80,6 +74,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)
|
||||
@@ -90,27 +88,23 @@ 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 = [
|
||||
( '82027921', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "ber_tlv" }, "record_len": None, "num_of_rec": None } ),
|
||||
( '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):
|
||||
data = obj.read()
|
||||
if data == b'\x01\x01\x01\x00\x00\x01':
|
||||
def _decode(self, obj, context, path):
|
||||
if obj == 0x39:
|
||||
return 'ber_tlv'
|
||||
raise ValidationError
|
||||
def _build(self, obj, context, path):
|
||||
def _encode(self, obj, context, path):
|
||||
if obj == 'ber_tlv':
|
||||
return b'\x01\x01\x01\x00\x00\x01'
|
||||
return 0x39
|
||||
raise ValidationError
|
||||
|
||||
FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6)))),
|
||||
FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6))), 'file_type'/Computed('working_ef')),
|
||||
BitStruct(Const(0, Bit), 'shareable'/Flag, 'file_type'/Enum(BitsInteger(3), working_ef=0, internal_ef=1, df=7),
|
||||
'structure'/Enum(BitsInteger(3), no_info_given=0, transparent=1, linear_fixed=2, cyclic=6))
|
||||
)
|
||||
@@ -131,7 +125,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)
|
||||
|
||||
@@ -169,17 +163,40 @@ class SpecificUiccEnvironmentConditions(BER_TLV_IE, tag=0x88):
|
||||
class Platform2PlatformCatSecuredApdu(BER_TLV_IE, tag=0x89):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# TS 102 222 Table 4a + 5
|
||||
class SpecialFileInfo(BER_TLV_IE, tag=0xC0):
|
||||
_construct = FlagsEnum(Byte, high_update_activity=0x80, readable_and_updatable_when_deactivated=0x40)
|
||||
|
||||
# TS 102 222 Table 4a
|
||||
class FillingPattern(BER_TLV_IE, tag=0xC1):
|
||||
# The first W-1 bytes of the transparent EF or the first W-1 bytes of each record of a record
|
||||
# oriented EF shall be initialized with the first W-1 bytes of the Filling Pattern. All remaining
|
||||
# bytes (if any) shall be initialized with the value of the last byte of the Filling Pattern. If
|
||||
# the file or record length is shorter than the Filling Pattern, the Filling Pattern shall be
|
||||
# truncated accordingly.
|
||||
_construct = GreedyBytes
|
||||
|
||||
# TS 102 222 Table 4a
|
||||
class RepeatPattern(BER_TLV_IE, tag=0xC2):
|
||||
# The first X bytes of the transparent EF or the first X bytes of each record of a record oriented
|
||||
# EF shall be initialized with the X bytes of the Repeat Pattern. This shall be repeated
|
||||
# consecutively for all remaining blocks of X bytes of data in the file or in a record. If
|
||||
# necessary, the Repeat Pattern shall be truncated at the end of the file or at the end of each
|
||||
# record to initialize the remaining bytes.
|
||||
_construct = GreedyBytes
|
||||
|
||||
# sysmoISIM-SJA2 specific
|
||||
class ToolkitAccessConditions(BER_TLV_IE, tag=0xD2):
|
||||
_construct = FlagsEnum(Byte, rfm_create=1, rfm_delete_terminate=2, other_applet_create=4,
|
||||
other_applet_delete_terminate=8)
|
||||
|
||||
# ETSI TS 102 221 11.1.1.4.6.0
|
||||
# ETSI TS 102 221 11.1.1.4.6.0 + TS 102 222 Table 4A
|
||||
class ProprietaryInformation(BER_TLV_IE, tag=0xA5,
|
||||
nested=[UiccCharacteristics, ApplicationPowerConsumption,
|
||||
MinApplicationClockFrequency, AvailableMemory,
|
||||
FileDetails, ReservedFileSize, MaximumFileSize,
|
||||
SupportedFilesystemCommands, SpecificUiccEnvironmentConditions,
|
||||
SpecialFileInfo, FillingPattern, RepeatPattern,
|
||||
ToolkitAccessConditions]):
|
||||
pass
|
||||
|
||||
@@ -234,20 +251,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):
|
||||
@@ -266,6 +282,12 @@ class FcpTemplate(BER_TLV_IE, tag=0x62, nested=[FileSize, TotalFileSize, FileDes
|
||||
PinStatusTemplate_DO]):
|
||||
pass
|
||||
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
t = FcpTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fcp_template'])
|
||||
|
||||
def tlv_key_replace(inmap, indata):
|
||||
def newkey(inmap, key):
|
||||
@@ -284,6 +306,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):
|
||||
@@ -481,12 +530,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:]
|
||||
@@ -520,7 +568,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
|
||||
|
||||
@@ -586,323 +634,3 @@ NOT_DO = Nested_DO('not', 0xaf, NOT_Template)
|
||||
SC_DO = DataObjectChoice('security_condition', 'Security Condition',
|
||||
members=[Always_DO, Never_DO, SecCondByte_DO(), SecCondByte_DO(0x9e), CRT_DO(),
|
||||
OR_DO, AND_DO, NOT_DO])
|
||||
|
||||
# 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')
|
||||
|
||||
# see https://github.com/PyCQA/pylint/issues/5794
|
||||
#pylint: disable=undefined-variable
|
||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
|
||||
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
|
||||
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
|
||||
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
|
||||
iso7816_4.ApplicationRelatedDOSet]):
|
||||
pass
|
||||
|
||||
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
|
||||
self._tlv = EF_DIR.ApplicationTemplate
|
||||
|
||||
# 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))
|
||||
|
||||
def _decode_hex(self, raw_hex):
|
||||
return {'iccid': dec_iccid(raw_hex)}
|
||||
|
||||
def _encode_hex(self, abstract):
|
||||
return enc_iccid(abstract['iccid'])
|
||||
|
||||
# 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, **kwargs):
|
||||
if bin_data == b'\xff\xff':
|
||||
return None
|
||||
else:
|
||||
return bin_data.decode('ascii')
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
if in_json is None:
|
||||
return b'\xff\xff'
|
||||
else:
|
||||
return in_json.encode('ascii')
|
||||
|
||||
|
||||
# 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
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
@staticmethod
|
||||
def flatten(inp: list):
|
||||
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
|
||||
def sc_abbreviate(sc):
|
||||
if 'always' in sc:
|
||||
return 'always'
|
||||
elif 'never' in sc:
|
||||
return 'never'
|
||||
elif 'control_reference_template' in sc:
|
||||
return sc['control_reference_template']
|
||||
else:
|
||||
return sc
|
||||
|
||||
by_mode = {}
|
||||
for t in inp:
|
||||
am = t[0]
|
||||
sc = t[1]
|
||||
sc_abbr = sc_abbreviate(sc)
|
||||
if 'access_mode' in am:
|
||||
for m in am['access_mode']:
|
||||
by_mode[m] = sc_abbr
|
||||
elif 'command_header' in am:
|
||||
ins = am['command_header']['INS']
|
||||
if 'CLA' in am['command_header']:
|
||||
cla = am['command_header']['CLA']
|
||||
else:
|
||||
cla = None
|
||||
cmd = ts_102_22x_cmdset.lookup(ins, cla)
|
||||
if cmd:
|
||||
name = cmd.name.lower().replace(' ', '_')
|
||||
by_mode[name] = sc_abbr
|
||||
else:
|
||||
raise ValueError
|
||||
else:
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
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)
|
||||
# we cannot pass the result through flatten() here, as we don't have a related
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
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 = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
def do_read_arr_records(self, opts):
|
||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
# 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 = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
|
||||
# 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,
|
||||
support_uicc_suspend=2)
|
||||
self._construct = Struct(
|
||||
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
|
||||
|
||||
|
||||
class CardProfileUICC(CardProfile):
|
||||
|
||||
ORDER = 1
|
||||
|
||||
def __init__(self, name='UICC'):
|
||||
files = [
|
||||
EF_DIR(),
|
||||
EF_ICCID(),
|
||||
EF_PL(),
|
||||
EF_ARR(),
|
||||
# FIXME: DF.CD
|
||||
EF_UMPC(),
|
||||
]
|
||||
addons = [
|
||||
AddonSIM,
|
||||
AddonGSMR,
|
||||
AddonRUIM,
|
||||
]
|
||||
sw = {
|
||||
'Normal': {
|
||||
'9000': 'Normal ending of the command',
|
||||
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
|
||||
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
|
||||
},
|
||||
'Postponed processing': {
|
||||
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
|
||||
},
|
||||
'Warnings': {
|
||||
'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',
|
||||
'6284': 'Selected file in termination state',
|
||||
'62f1': 'More data available',
|
||||
'62f2': 'More data available and proactive command pending',
|
||||
'62f3': 'Response data available',
|
||||
'63f1': 'More data expected',
|
||||
'63f2': 'More data expected and proactive command pending',
|
||||
'63cx': 'Command successful but after using an internal update retry routine X times',
|
||||
},
|
||||
'Execution errors': {
|
||||
'6400': 'No information given, state of non-volatile memory unchanged',
|
||||
'6500': 'No information given, state of non-volatile memory changed',
|
||||
'6581': 'Memory problem',
|
||||
},
|
||||
'Checking errors': {
|
||||
'6700': 'Wrong length',
|
||||
'67xx': 'The interpretation of this status word is command dependent',
|
||||
'6b00': 'Wrong parameter(s) P1-P2',
|
||||
'6d00': 'Instruction code not supported or invalid',
|
||||
'6e00': 'Class not supported',
|
||||
'6f00': 'Technical problem, no precise diagnosis',
|
||||
'6fxx': 'The interpretation of this status word is command dependent',
|
||||
},
|
||||
'Functions in CLA not supported': {
|
||||
'6800': 'No information given',
|
||||
'6881': 'Logical channel not supported',
|
||||
'6882': 'Secure messaging not supported',
|
||||
},
|
||||
'Command not allowed': {
|
||||
'6900': 'No information given',
|
||||
'6981': 'Command incompatible with file structure',
|
||||
'6982': 'Security status not satisfied',
|
||||
'6983': 'Authentication/PIN method blocked',
|
||||
'6984': 'Referenced data invalidated',
|
||||
'6985': 'Conditions of use not satisfied',
|
||||
'6986': 'Command not allowed (no EF selected)',
|
||||
'6989': 'Command not allowed - secure channel - security not satisfied',
|
||||
},
|
||||
'Wrong parameters': {
|
||||
'6a80': 'Incorrect parameters in the data field',
|
||||
'6a81': 'Function not supported',
|
||||
'6a82': 'File not found',
|
||||
'6a83': 'Record not found',
|
||||
'6a84': 'Not enough memory space',
|
||||
'6a86': 'Incorrect parameters P1 to P2',
|
||||
'6a87': 'Lc inconsistent with P1 to P2',
|
||||
'6a88': 'Referenced data not found',
|
||||
},
|
||||
'Application errors': {
|
||||
'9850': 'INCREASE cannot be performed, max value reached',
|
||||
'9862': 'Authentication error, application specific',
|
||||
'9863': 'Security session or association expired',
|
||||
'9864': 'Minimum UICC suspension time is too long',
|
||||
},
|
||||
}
|
||||
|
||||
super().__init__(name, desc='ETSI TS 102 221', cla="00",
|
||||
sel_ctrl="0004", files_in_mf=files, sw=sw,
|
||||
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(resp_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
t = FcpTemplate()
|
||||
t.from_tlv(h2b(resp_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fcp_template'])
|
||||
|
||||
@staticmethod
|
||||
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')
|
||||
|
||||
# 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))
|
||||
|
||||
resume_uicc_parser = argparse.ArgumentParser()
|
||||
resume_uicc_parser.add_argument('token', type=str, help='Token provided during SUSPEND')
|
||||
|
||||
@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)
|
||||
|
||||
@@ -18,13 +18,10 @@
|
||||
# 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.exceptions import *
|
||||
from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
|
||||
import cmd2
|
||||
from cmd2 import CommandSet, with_default_category
|
||||
from osmocom.utils import b2h, auto_uint8, auto_uint16, is_hexstr
|
||||
|
||||
from pySim.ts_102_221 import *
|
||||
|
||||
@@ -32,9 +29,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!')
|
||||
@@ -48,8 +42,8 @@ class Ts102222Commands(CommandSet):
|
||||
if not opts.force_delete:
|
||||
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.lchan.scc.delete_file(f.fid)
|
||||
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
|
||||
(_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"""
|
||||
@@ -69,8 +63,8 @@ class Ts102222Commands(CommandSet):
|
||||
if not opts.force:
|
||||
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.lchan.scc.terminate_df(f.fid)
|
||||
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
|
||||
(_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"""
|
||||
@@ -85,8 +79,8 @@ class Ts102222Commands(CommandSet):
|
||||
if not opts.force:
|
||||
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.lchan.scc.terminate_ef(f.fid)
|
||||
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
|
||||
(_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"""
|
||||
@@ -104,21 +98,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.lchan.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._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')
|
||||
create_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
|
||||
|
||||
@cmd2.with_argparser(create_parser)
|
||||
def do_create_ef(self, opts):
|
||||
@@ -149,26 +143,26 @@ class Ts102222Commands(CommandSet):
|
||||
ShortFileIdentifier(decoded=opts.short_file_id),
|
||||
]
|
||||
fcp = FcpTemplate(children=ies)
|
||||
(data, sw) = self._cmd.lchan.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._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')
|
||||
createdf_sja_optional.add_argument('--permit-other-applet-delete-terminate', action='store_true')
|
||||
createdf_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
|
||||
|
||||
@cmd2.with_argparser(createdf_parser)
|
||||
def do_create_df(self, opts):
|
||||
@@ -192,32 +186,32 @@ 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.lchan.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=int, help='Size of file in octets')
|
||||
resize_ef_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
|
||||
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
|
||||
|
||||
@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)
|
||||
f = self._cmd.lchan.get_file_for_filename(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()))
|
||||
(_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)
|
||||
|
||||
|
||||
@@ -17,13 +17,12 @@ 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 osmocom.construct import *
|
||||
|
||||
#from pySim.utils import *
|
||||
from osmocom.tlv import BER_TLV_IE, TLV_IE_Collection
|
||||
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):
|
||||
|
||||
@@ -10,7 +10,7 @@ Various constants from 3GPP TS 31.102 V17.9.0
|
||||
|
||||
#
|
||||
# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
|
||||
# Copyright (C) 2021-2023 Harald Welte <laforge@osmocom.org>
|
||||
# Copyright (C) 2021-2024 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
|
||||
@@ -26,25 +26,30 @@ Various constants from 3GPP TS 31.102 V17.9.0
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Mapping between USIM Service Number and its description
|
||||
import pySim.ts_102_221
|
||||
from pySim.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
|
||||
from pySim.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
|
||||
from pySim.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
|
||||
from pySim.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
|
||||
from pySim.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
|
||||
from pySim.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
|
||||
from pySim.ts_102_221 import EF_ARR
|
||||
from pySim.tlv import *
|
||||
import enum
|
||||
|
||||
from construct import Optional as COptional
|
||||
from construct import Int32ub, Nibble, GreedyRange, Struct, FlagsEnum, Switch, this, Int16ub, Padding
|
||||
from construct import Bytewise, Int24ub, Int24sb, PaddedString, PrefixedArray, If
|
||||
|
||||
from osmocom.utils import is_hexstr
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.profile.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
|
||||
from pySim.profile.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
|
||||
from pySim.profile.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
|
||||
from pySim.profile.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
|
||||
from pySim.profile.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
|
||||
from pySim.profile.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
|
||||
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
|
||||
from pySim.filesystem import *
|
||||
from pySim.ts_31_102_telecom import DF_PHONEBOOK, EF_UServiceTable
|
||||
from pySim.construct import *
|
||||
from pySim.ts_31_103_shared import EF_IMSConfigData, EF_XCAPConfigData, EF_MuDMiDConfigData
|
||||
from pySim.ts_31_103_shared import EF_AC_GBAUAPI, EF_IMSDCI
|
||||
from pySim.cat import SMS_TPDU, DeviceIdentities, SMSPPDownload
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
from typing import Tuple
|
||||
from struct import unpack, pack
|
||||
import enum
|
||||
|
||||
# Mapping between USIM Service Number and its description
|
||||
EF_UST_map = {
|
||||
1: 'Local Phone Book',
|
||||
2: 'Fixed Dialling Numbers (FDN)',
|
||||
@@ -338,7 +343,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
out.append({k: v})
|
||||
return out
|
||||
|
||||
def _encode_hex(self, in_json):
|
||||
def _encode_hex(self, in_json, **kwargs):
|
||||
out_bytes = self._encode_prot_scheme_id_list(
|
||||
in_json['prot_scheme_id_list'])
|
||||
d = self._expand_pubkey_list(in_json['hnet_pubkey_list'])
|
||||
@@ -390,8 +395,8 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
'hnet_pubkey_list': hnet_pubkey_list
|
||||
}
|
||||
|
||||
def _encode_bin(self, in_json):
|
||||
return h2b(self._encode_hex(in_json))
|
||||
def _encode_bin(self, in_json, **kwargs):
|
||||
return h2b(self._encode_hex(in_json, **kwargs))
|
||||
|
||||
|
||||
class EF_LI(TransRecEF):
|
||||
@@ -407,7 +412,7 @@ class EF_LI(TransRecEF):
|
||||
return in_bin.decode('ascii')
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
if in_json == None:
|
||||
if in_json is None:
|
||||
return b'\xff\xff'
|
||||
else:
|
||||
# officially this is 7-bit GSM alphabet with one padding bit in each byte
|
||||
@@ -437,9 +442,6 @@ class EF_UST(EF_UServiceTable):
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def do_ust_service_activate(self, arg):
|
||||
"""Activate a service within EF.UST"""
|
||||
selected_file = self._cmd.lchan.selected_file
|
||||
@@ -450,7 +452,7 @@ class EF_UST(EF_UServiceTable):
|
||||
selected_file = self._cmd.lchan.selected_file
|
||||
selected_file.ust_update(self._cmd, [], [int(arg)])
|
||||
|
||||
def do_ust_service_check(self, arg):
|
||||
def do_ust_service_check(self, _arg):
|
||||
"""Check consistency between services of this file and files present/activated.
|
||||
|
||||
Many services determine if one or multiple files shall be present/activated or if they shall be
|
||||
@@ -637,9 +639,6 @@ class EF_EST(EF_UServiceTable):
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def do_est_service_enable(self, arg):
|
||||
"""Enable a service within EF.EST"""
|
||||
selected_file = self._cmd.lchan.selected_file
|
||||
@@ -853,7 +852,7 @@ class EF_IPS(CyclicEF):
|
||||
self._construct = Struct('status'/PaddedString(2, 'ascii'),
|
||||
'link_to_ef_ipd'/Int8ub, 'rfu'/Byte)
|
||||
|
||||
# TS 31.102 Section 4.2.103
|
||||
# TS 31.102 Section 4.2.103 (Rel 13)
|
||||
class EF_ePDGId(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '801100657064672e6f736d6f636f6d2e6f7267', {'e_pdg_id': {"type_of_ePDG_address": "FQDN", "ePDG_address" : "epdg.osmocom.org" } } ),
|
||||
@@ -871,7 +870,7 @@ class EF_ePDGId(TransparentEF):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_ePDGId.ePDGId
|
||||
|
||||
# TS 31.102 Section 4.2.104
|
||||
# TS 31.102 Section 4.2.104 (Rel 13)
|
||||
class EF_ePDGSelection(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '800600f110000100', {'e_pdg_selection': [{'plmn': '001-01', 'epdg_priority': 1, 'epdg_fqdn_format': 'operator_identified' }] }),
|
||||
@@ -886,20 +885,76 @@ class EF_ePDGSelection(TransparentEF):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_ePDGSelection.ePDGSelection
|
||||
|
||||
# TS 31.102 Section 4.2.106
|
||||
# TS 31.102 Section 4.2.106 (Rel 14)
|
||||
class EF_FromPreferred(TransparentEF):
|
||||
def __init__(self, fid='6ff7', sfid=None, name='EF.FromPreferred', size=(1, 1),
|
||||
desc='From Preferred', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
self._construct = BitStruct('rfu'/BitsRFU(7), 'from_preferred'/Flag)
|
||||
|
||||
# TS 31.102 Section 4.2.114
|
||||
# TS 31.102 Section 4.2.112 + TS 23.032 Section 6.1
|
||||
GadPoint = Struct('latitude'/Int24sb, 'longitude'/Int24sb)
|
||||
|
||||
# TS 31.102 Section 4.2.112 (Rel ??)
|
||||
class EF_EARFCNList(TransparentEF):
|
||||
_test_de_encode = [
|
||||
# single data object with one EARFCN + one area of 3 points
|
||||
('a01a8004000100008112000001100001000002100002000003100003',
|
||||
[{'earfcn_list_tlv': [{'earfcn': 65536},
|
||||
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
|
||||
{'latitude': 2, 'longitude': 1048578},
|
||||
{'latitude': 3, 'longitude': 1048579}] }]}] ),
|
||||
# single data object with one EARFCN + two areas of 3 + 4 points
|
||||
('a03480040001000081120000011000010000021000020000031000038118000001100001000002100002000003100003000004100004',
|
||||
[{'earfcn_list_tlv': [{'earfcn': 65536},
|
||||
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
|
||||
{'latitude': 2, 'longitude': 1048578},
|
||||
{'latitude': 3, 'longitude': 1048579}] },
|
||||
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
|
||||
{'latitude': 2, 'longitude': 1048578},
|
||||
{'latitude': 3, 'longitude': 1048579},
|
||||
{'latitude': 4, 'longitude': 1048580}] }
|
||||
] }] ),
|
||||
# two concatenated data objects with 3 points each
|
||||
('a01a8004000100008112000001100001000002100002000003100003a01a8004000200008112000011100011000012100012000013100013',
|
||||
[{'earfcn_list_tlv': [{'earfcn': 65536},
|
||||
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
|
||||
{'latitude': 2, 'longitude': 1048578},
|
||||
{'latitude': 3, 'longitude': 1048579}] }]},
|
||||
{'earfcn_list_tlv': [{'earfcn': 131072},
|
||||
{'geographical_area': [{'latitude': 17, 'longitude': 1048593},
|
||||
{'latitude': 18, 'longitude': 1048594},
|
||||
{'latitude': 19, 'longitude': 1048595}] }]} ]),
|
||||
]
|
||||
class Earfcn(BER_TLV_IE, tag=0x80):
|
||||
_construct = Int32ub
|
||||
class GeographicalArea(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyRange(GadPoint)
|
||||
class EarfcnListTlv(BER_TLV_IE, tag=0xa0, nested=[Earfcn,GeographicalArea]):
|
||||
pass
|
||||
# we need a collection as there might be multiple concatenated instances
|
||||
class EarfcnListTlvCollection(TLV_IE_Collection, nested=[EarfcnListTlv]):
|
||||
pass
|
||||
def __init__(self, fid='6ffd', sfid=None, name='EF.EARFCNList', size=(30, 100),
|
||||
desc='EARFCN list for MTC/NB-IOT UEs', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
self._tlv = self.EarfcnListTlvCollection
|
||||
|
||||
# TS 31.102 Section 4.2.114 (Rel 18)
|
||||
class EF_eAKA(TransparentEF):
|
||||
def __init__(self, fid='6f01', sfid=None, name='EF.eAKA', size=(1, 1),
|
||||
desc='enhanced AKA support', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
self._construct = BitStruct('rfu'/BitsRFU(7), 'enhanced_sqn_calculation_supported'/Flag)
|
||||
|
||||
# TS 31.102 Section 4.2.115 (Rel 18)
|
||||
class EF_OCST(TransparentEF):
|
||||
def __init__(self, fid='6f02', sfid=None, name='EF.OCST', size=(2, 100),
|
||||
desc='Operator controlled signal threshold per access technology', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
self._construct = Struct('sense'/FlagsEnum(Byte, sense_enabled=1),
|
||||
'ocst_tlv'/GreedyBytes)
|
||||
|
||||
|
||||
######################################################################
|
||||
# DF.GSM-ACCESS
|
||||
@@ -995,10 +1050,10 @@ class EF_OCSGL(LinFixedEF):
|
||||
|
||||
|
||||
######################################################################
|
||||
# DF.5GS
|
||||
# DF.5GS (Rel 15)
|
||||
######################################################################
|
||||
|
||||
# TS 31.102 Section 4.4.11.2
|
||||
# TS 31.102 Section 4.4.11.2 (Rel 15)
|
||||
class EF_5GS3GPPLOCI(TransparentEF):
|
||||
def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20),
|
||||
desc='5S 3GP location information', **kwargs):
|
||||
@@ -1009,7 +1064,7 @@ class EF_5GS3GPPLOCI(TransparentEF):
|
||||
'last_visited_registered_tai_in_5gs'/HexAdapter(Bytes(6)),
|
||||
'5gs_update_status'/upd_status_constr)
|
||||
|
||||
# TS 31.102 Section 4.4.11.7
|
||||
# TS 31.102 Section 4.4.11.7 (Rel 15)
|
||||
class EF_UAC_AIC(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '03', { "uac_access_id_config": { "multimedia_priority_service": True,
|
||||
@@ -1022,7 +1077,7 @@ class EF_UAC_AIC(TransparentEF):
|
||||
mission_critical_service=2)
|
||||
self._construct = Struct('uac_access_id_config'/cfg_constr)
|
||||
|
||||
# TS 31.102 Section 4.4.11.9
|
||||
# TS 31.102 Section 4.4.11.9 (Rel 15)
|
||||
class EF_OPL5G(LinFixedEF):
|
||||
def __init__(self, fid='4f08', sfid=0x08, name='EF.OPL5G', desc='5GS Operator PLMN List', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=(10, None), **kwargs)
|
||||
@@ -1030,7 +1085,7 @@ class EF_OPL5G(LinFixedEF):
|
||||
'tac_max'/HexAdapter(Bytes(3)))
|
||||
self._construct = Struct('tai'/Tai, 'pnn_record_id'/Int8ub)
|
||||
|
||||
# TS 31.102 Section 4.4.11.10
|
||||
# TS 31.102 Section 4.4.11.10 (Rel 15)
|
||||
class EF_SUPI_NAI(TransparentEF):
|
||||
class NetworkSpecificIdentifier(TLV_IE, tag=0x80):
|
||||
# RFC 7542 encoded as UTF-8 string
|
||||
@@ -1052,7 +1107,7 @@ class EF_SUPI_NAI(TransparentEF):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_SUPI_NAI.NAI_TLV_Collection
|
||||
|
||||
# TS 31.102 Section 4.4.11.11
|
||||
# TS 31.102 Section 4.4.11.11 (Rel 15)
|
||||
class EF_Routing_Indicator(TransparentEF):
|
||||
def __init__(self, fid='4f0a', sfid=0x0a, name='EF.Routing_Indicator', desc='Routing Indicator', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
@@ -1064,7 +1119,7 @@ class EF_Routing_Indicator(TransparentEF):
|
||||
self._construct = Struct('routing_indicator'/Rpad(BcdAdapter(Bytes(2)), 'f', 2),
|
||||
'rfu'/HexAdapter(Bytes(2)))
|
||||
|
||||
# TS 31.102 Section 4.4.11.13
|
||||
# TS 31.102 Section 4.4.11.13 (Rel 16)
|
||||
class EF_TN3GPPSNN(TransparentEF):
|
||||
class ServingNetworkName(BER_TLV_IE, tag=0x80):
|
||||
_construct = Utf8Adapter(GreedyBytes)
|
||||
@@ -1138,9 +1193,6 @@ class EF_5G_PROSE_ST(EF_UServiceTable):
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def do_prose_service_activate(self, arg):
|
||||
"""Activate a service within EF.5G_PROSE_ST"""
|
||||
selected_file = self._cmd.lchan.selected_file
|
||||
@@ -1284,6 +1336,56 @@ class EF_5G_PROSE_UIR(TransparentEF):
|
||||
# contains TLV structure despite being TransparentEF, not BER-TLV ?!?
|
||||
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUeToNetworkRelayUE
|
||||
|
||||
# TS 31.102 Section 4.4.13.8 (Rel 18)
|
||||
class EF_5G_PROSE_U2URU(TransparentEF):
|
||||
class ValidityTimer(BER_TLV_IE, tag=0x85):
|
||||
_construct = Bytes(5)
|
||||
class ServedByNGRAN(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
class NotServedByNGRAN(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyBytes
|
||||
class DefaultDstL2IdsForRxDisc(BER_TLV_IE, tag=0x99):
|
||||
_construct = GreedyBytes
|
||||
class UserInforIdForDiscovery(BER_TLV_IE, tag=0x8e):
|
||||
_construct = GreedyBytes
|
||||
class RSCInfoList(BER_TLV_IE, tag=0x8b):
|
||||
_construct = GreedyBytes
|
||||
class DefaultDstL2IdsForTxRxDirect(BER_TLV_IE, tag=0x9a):
|
||||
_construct = GreedyBytes
|
||||
class ProSeConfigDataForU2URelayUE(BER_TLV_IE, tag=0xa0,
|
||||
nested=[ValidityTimer, ServedByNGRAN, NotServedByNGRAN,
|
||||
DefaultDstL2IdsForRxDisc, UserInforIdForDiscovery,
|
||||
RSCInfoList, DefaultDstL2IdsForTxRxDirect]):
|
||||
pass
|
||||
def __init__(self, fid='4f07', sfid=0x07, name='EF.5G_PROSE_U2URU',
|
||||
desc='5G ProSe configuration data for UE-to-UE relay UE', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_5G_PROSE_U2URU.ProSeConfigDataForU2URelayUE
|
||||
|
||||
# TS 31.102 Section 4.4.13.9 (Rel 18)
|
||||
class EF_5G_PROSE_EU(TransparentEF):
|
||||
class PKMFAddressInformation(BER_TLV_IE, tag=0x93):
|
||||
Ipv4AddrList = PrefixedArray(Int8ub, Int32ub)
|
||||
Ipv6AddrList = PrefixedArray(Int8ub, Bytes(16))
|
||||
_construct = Struct('flags'/FlagsEnum(Byte, ipv4=1, ipv6=2, fqdn=4),
|
||||
'ipv4_addr_list'/If(this.flags.ipv4, Ipv4AddrList),
|
||||
'ipv6_addr_list'/If(this.flags.ipv6, Ipv6AddrList),
|
||||
'fqdn'/Prefixed(Int8ub, Utf8Adapter(GreedyBytes)))
|
||||
class ProSeConfigDataForEndUE(BER_TLV_IE, tag=0xa0,
|
||||
nested=[EF_5G_PROSE_U2URU.ValidityTimer,
|
||||
EF_5G_PROSE_U2URU.ServedByNGRAN,
|
||||
EF_5G_PROSE_U2URU.NotServedByNGRAN,
|
||||
EF_5G_PROSE_U2URU.DefaultDstL2IdsForRxDisc,
|
||||
EF_5G_PROSE_U2URU.UserInforIdForDiscovery,
|
||||
EF_5G_PROSE_U2URU.RSCInfoList,
|
||||
EF_5G_PROSE_U2URU.DefaultDstL2IdsForTxRxDirect,
|
||||
PKMFAddressInformation]):
|
||||
pass
|
||||
def __init__(self, fid='4f08', sfid=0x08, name='EF.5G_PROSE_EU',
|
||||
desc='5G ProSe configuration data for end UE', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_5G_PROSE_EU.ProSeConfigDataForEndUE
|
||||
|
||||
# TS 31.102 Section 4.4.13 (Rel 17)
|
||||
class DF_5G_ProSe(CardDF):
|
||||
def __init__(self, fid='5ff0', name='DF.5G_ProSe', desc='Files for 5G ProSe purpose', **kwargs):
|
||||
@@ -1295,9 +1397,62 @@ class DF_5G_ProSe(CardDF):
|
||||
EF_5G_PROSE_U2NRU(service=3),
|
||||
EF_5G_PROSE_RU(service=4),
|
||||
EF_5G_PROSE_UIR(service=5),
|
||||
# Rel 18 additions
|
||||
EF_5G_PROSE_U2URU(service=6),
|
||||
EF_5G_PROSE_EU(service=7),
|
||||
]
|
||||
self.add_files(files)
|
||||
|
||||
# TS 31.102 Section 4.4.14.2 (Rel 18)
|
||||
class EF_5MBSUECONFIG(TransparentEF):
|
||||
|
||||
class Plmn(BER_TLV_IE, tag=0x80):
|
||||
_construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
|
||||
'nid'/COptional(Bytes(6)))
|
||||
class Tmgi(BER_TLV_IE, tag=0x81):
|
||||
TmgiEntry = Struct('tmgi'/Bytes(6),
|
||||
'usd_fid'/HexAdapter(Bytes(2)),
|
||||
'service_type'/FlagsEnum(Byte, mbs_service_announcement=1, mbs_user_service=2))
|
||||
_construct = GreedyRange(TmgiEntry)
|
||||
class NrArfcnList(BER_TLV_IE, tag=0x82):
|
||||
_construct = GreedyRange(Bytes(4))
|
||||
class DNN(BER_TLV_IE, tag=0x83):
|
||||
_construct = GreedyBytes
|
||||
class SNSSAI(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
class PduInfoList(BER_TLV_IE, tag=0xa1, nested=[DNN, SNSSAI]):
|
||||
pass
|
||||
class Plmn5mbsPreconfiguration(BER_TLV_IE, tag=0xa0,
|
||||
nested=[Plmn, Tmgi, NrArfcnList, PduInfoList]):
|
||||
pass
|
||||
def __init__(self, fid='4f01', sfid=None, name='EF.5MBSUECONFIG',
|
||||
desc='5MBS UE pre-configuration', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_5MBSUECONFIG.Plmn5mbsPreconfiguration
|
||||
|
||||
# TS 31.102 Section 4.4.14.3 (Rel 18)
|
||||
class EF_5MBSUSD(TransparentEF):
|
||||
"""There can be any number of these files with undefined FID; the FIDs are contained
|
||||
in EF.5BMSUECONFIG. FID range is 4f08...4fff"""
|
||||
class USD(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
def __init__(self, fid, sfid=None, name='EF.5MBSUSD',
|
||||
desc='5MBS User Service Description', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = USD
|
||||
|
||||
|
||||
# TS 31.102 Section 4.4.14 (Rel 18)
|
||||
class DF_5MBSUECONFIG(CardDF):
|
||||
def __init__(self, fid='5ff1', name='DF.5MBSUECONFIG', desc='', **kwargs):
|
||||
super().__init__(fid=fid, name=name, desc=desc, **kwargs)
|
||||
files = [
|
||||
EF_5MBSUECONFIG(),
|
||||
# EF_5MBSUSD() wouild have to be dynamically registered based on EF_5MBSUECONFIG content
|
||||
]
|
||||
self.add_files(files)
|
||||
|
||||
|
||||
# TS 31.102 Section 4.4.11.18 (Rel 17)
|
||||
class EF_5GSEDRX(TransparentEF):
|
||||
def __init__(self, fid='4f10', sfid=0x10, name='EF.5GSEDRX',
|
||||
@@ -1449,14 +1604,13 @@ class DF_SAIP(CardDF):
|
||||
|
||||
class ADF_USIM(CardADF):
|
||||
def __init__(self, aid='a0000000871002', has_fs=True, name='ADF.USIM', fid=None, sfid=None,
|
||||
desc='USIM Application'):
|
||||
desc='USIM Application', has_imsi=True):
|
||||
super().__init__(aid=aid, has_fs=has_fs, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||
# add those commands to the general commands of a TransparentEF
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
files = [
|
||||
EF_LI(sfid=0x02),
|
||||
EF_IMSI(sfid=0x07),
|
||||
EF_Keys(),
|
||||
EF_Keys('6f09', 0x09, 'EF.KeysPS',
|
||||
desc='Ciphering and Integrity Keys for PS domain'),
|
||||
@@ -1533,7 +1687,7 @@ class ADF_USIM(CardADF):
|
||||
EF_VGCS('6fb3', None, 'EF.VBS', desc='Voice Broadcast Service', service=58),
|
||||
EF_VGCSS('6fb4', None, 'EF.VBSS', desc='Voice Broadcast Service Status', service=58),
|
||||
EF_VGCSCA(service=64),
|
||||
EF_VGCSCA('6fd5', None, 'EF.VBCSCA', desc='Voice Broadcast Service Ciphering Algorithm', service=65),
|
||||
EF_VGCSCA('6fd5', None, 'EF.VBSCA', desc='Voice Broadcast Service Ciphering Algorithm', service=65),
|
||||
EF_GBABP(service=68),
|
||||
EF_MSK(service=69),
|
||||
EF_MUK(service=69),
|
||||
@@ -1563,7 +1717,17 @@ class ADF_USIM(CardADF):
|
||||
EF_ePDGSelection('6ff6', None, 'EF.ePDGSelectionEm',
|
||||
desc='ePDG Selection Information for Emergency Services', service=(110, 111)),
|
||||
EF_FromPreferred(service=114),
|
||||
EF_IMSConfigData(service=115),
|
||||
# TODO: EF.TVCONFIG
|
||||
# TODO: EF.3GPPPSDATAOFF
|
||||
# TODO: EF.3GPPPSDATAOFFservicelist
|
||||
EF_XCAPConfigData(service=120),
|
||||
EF_EARFCNList(service=121),
|
||||
EF_MuDMiDConfigData(service=134),
|
||||
EF_eAKA(),
|
||||
EF_OCST(service=148),
|
||||
EF_AC_GBAUAPI(service=68),
|
||||
EF_IMSDCI(service=150),
|
||||
# FIXME: DF_SoLSA service=23
|
||||
DF_PHONEBOOK(),
|
||||
DF_GSM_ACCESS(),
|
||||
@@ -1576,30 +1740,32 @@ class ADF_USIM(CardADF):
|
||||
DF_SNPN(service=[143,146]),
|
||||
DF_5G_ProSe(service=139),
|
||||
DF_SAIP(),
|
||||
DF_5MBSUECONFIG(service=147),
|
||||
]
|
||||
|
||||
if has_imsi:
|
||||
files.append(EF_IMSI(sfid=0x07))
|
||||
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
|
||||
return CardProfileUICC.decode_select_response(data_hex)
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
authenticate_parser = argparse.ArgumentParser()
|
||||
authenticate_parser.add_argument('rand', help='Random challenge')
|
||||
authenticate_parser.add_argument('autn', help='Authentication Nonce')
|
||||
authenticate_parser.add_argument('RAND', type=is_hexstr, help='Random challenge')
|
||||
authenticate_parser.add_argument('AUTN', type=is_hexstr, help='Authentication Nonce')
|
||||
#authenticate_parser.add_argument('--context', help='Authentication context', default='3G')
|
||||
|
||||
@cmd2.with_argparser(authenticate_parser)
|
||||
def do_authenticate(self, opts):
|
||||
"""Perform Authentication and Key Agreement (AKA)."""
|
||||
(data, sw) = self._cmd.lchan.scc.authenticate(opts.rand, opts.autn)
|
||||
(data, _sw) = self._cmd.lchan.scc.authenticate(opts.RAND, opts.AUTN)
|
||||
self._cmd.poutput_json(data)
|
||||
|
||||
term_prof_parser = argparse.ArgumentParser()
|
||||
term_prof_parser.add_argument('PROFILE', help='Hexstring of encoded terminal profile')
|
||||
term_prof_parser.add_argument('PROFILE', type=is_hexstr, help='Hexstring of encoded terminal profile')
|
||||
|
||||
@cmd2.with_argparser(term_prof_parser)
|
||||
def do_terminal_profile(self, opts):
|
||||
@@ -1613,7 +1779,7 @@ class ADF_USIM(CardADF):
|
||||
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
|
||||
|
||||
envelope_parser = argparse.ArgumentParser()
|
||||
envelope_parser.add_argument('PAYLOAD', help='Hexstring of encoded payload to ENVELOPE')
|
||||
envelope_parser.add_argument('PAYLOAD', type=is_hexstr, help='Hexstring of encoded payload to ENVELOPE')
|
||||
|
||||
@cmd2.with_argparser(envelope_parser)
|
||||
def do_envelope(self, opts):
|
||||
@@ -1625,7 +1791,7 @@ class ADF_USIM(CardADF):
|
||||
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
|
||||
|
||||
envelope_sms_parser = argparse.ArgumentParser()
|
||||
envelope_sms_parser.add_argument('TPDU', help='Hexstring of encoded SMS TPDU')
|
||||
envelope_sms_parser.add_argument('TPDU', type=is_hexstr, help='Hexstring of encoded SMS TPDU')
|
||||
|
||||
@cmd2.with_argparser(envelope_sms_parser)
|
||||
def do_envelope_sms(self, opts):
|
||||
@@ -1643,7 +1809,8 @@ class ADF_USIM(CardADF):
|
||||
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
|
||||
|
||||
get_id_parser = argparse.ArgumentParser()
|
||||
get_id_parser.add_argument("--nswo-context", action='store_true')
|
||||
get_id_parser.add_argument("--nswo-context", action='store_true',
|
||||
help='use SUCI 5G Non-Seamless WLAN Offload context')
|
||||
|
||||
@cmd2.with_argparser(get_id_parser)
|
||||
def do_get_identity(self, opts):
|
||||
@@ -1653,7 +1820,7 @@ class ADF_USIM(CardADF):
|
||||
context = 0x01 # SUCI
|
||||
if opts.nswo_context:
|
||||
context = 0x02 # SUCI 5G NSWO
|
||||
(data, sw) = self._cmd.lchan.scc.get_identity(context)
|
||||
(data, _sw) = self._cmd.lchan.scc.get_identity(context)
|
||||
do = SUCI_TlvDataObject()
|
||||
do.from_tlv(h2b(data))
|
||||
do_d = do.to_dict()
|
||||
@@ -1675,3 +1842,10 @@ sw_usim = {
|
||||
class CardApplicationUSIM(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__('USIM', adf=ADF_USIM(), sw=sw_usim)
|
||||
|
||||
# TS 31.102 Annex N + TS 102 220 Annex E
|
||||
class CardApplicationUSIMnonIMSI(CardApplication):
|
||||
def __init__(self):
|
||||
adf = ADF_USIM(aid='a000000087100b', name='ADF.USIM-non-IMSI', has_imsi=False,
|
||||
desc='3GPP USIM (non-IMSI SUPI Type) - TS 31.102 Annex N')
|
||||
super().__init__('USIM-non-IMSI', adf=adf, sw=sw_usim)
|
||||
|
||||
@@ -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 pySim.tlv import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.construct import *
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
from construct import Struct, Int16ub, Int32ub
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.filesystem 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):
|
||||
@@ -58,7 +59,7 @@ class EF_UServiceTable(TransparentEF):
|
||||
ret[service_nr]['description'] = self.table[service_nr]
|
||||
return ret
|
||||
|
||||
def _encode_bin(self, in_json):
|
||||
def _encode_bin(self, in_json, **kwargs):
|
||||
# compute the required binary size
|
||||
bin_len = 0
|
||||
for srv in in_json.keys():
|
||||
@@ -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']:
|
||||
@@ -121,7 +122,7 @@ class EF_UServiceTable(TransparentEF):
|
||||
return num_problems
|
||||
|
||||
def ust_update(self, cmd, activate=[], deactivate=[]):
|
||||
service_data, sw = cmd.lchan.read_binary()
|
||||
service_data, _sw = cmd.lchan.read_binary()
|
||||
service_data = h2b(service_data)
|
||||
|
||||
for service in activate:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Various constants from 3GPP TS 31.103 V16.1.0
|
||||
Various constants from 3GPP TS 31.103 V18.1.0
|
||||
"""
|
||||
|
||||
#
|
||||
# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
|
||||
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
|
||||
# Copyright (C) 2021-2024 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
|
||||
@@ -22,15 +22,16 @@ 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 osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.utils import *
|
||||
from pySim.tlv import *
|
||||
from pySim.ts_51_011 import EF_AD, EF_SMS, EF_SMSS, EF_SMSR, EF_SMSP
|
||||
from pySim.profile.ts_51_011 import EF_AD, EF_SMS, EF_SMSS, EF_SMSR, EF_SMSP
|
||||
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 *
|
||||
from pySim.ts_31_103_shared import *
|
||||
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
|
||||
|
||||
# Mapping between ISIM Service Number and its description
|
||||
EF_IST_map = {
|
||||
@@ -55,6 +56,7 @@ EF_IST_map = {
|
||||
19: 'XCAP Configuration Data',
|
||||
20: 'WebRTC URI',
|
||||
21: 'MuD and MiD configuration data',
|
||||
22: 'IMS Data Channel indication',
|
||||
}
|
||||
|
||||
# TS 31.103 Section 4.2.2
|
||||
@@ -188,96 +190,6 @@ class EF_NAFKCA(LinFixedEF):
|
||||
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 = 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)
|
||||
self._tlv = EF_UICCIARI.iari
|
||||
|
||||
# TS 31.103 Section 4.2.18
|
||||
class EF_IMSConfigData(BerTlvEF):
|
||||
class ImsConfigDataEncoding(BER_TLV_IE, tag=0x80):
|
||||
_construct = HexAdapter(Bytes(1))
|
||||
class ImsConfigData(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyString
|
||||
# pylint: disable=undefined-variable
|
||||
class ImsConfigDataCollection(TLV_IE_Collection, nested=[ImsConfigDataEncoding, ImsConfigData]):
|
||||
pass
|
||||
def __init__(self, fid='6ff8', sfid=None, name='EF.IMSConfigData', desc='IMS Configuration Data', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_IMSConfigData.ImsConfigDataCollection
|
||||
|
||||
# TS 31.103 Section 4.2.19
|
||||
class EF_XCAPConfigData(BerTlvEF):
|
||||
class Access(BER_TLV_IE, tag=0x81):
|
||||
pass
|
||||
class ApplicationName(BER_TLV_IE, tag=0x82):
|
||||
pass
|
||||
class ProviderID(BER_TLV_IE, tag=0x83):
|
||||
pass
|
||||
class URI(BER_TLV_IE, tag=0x84):
|
||||
pass
|
||||
class XcapAuthenticationUserName(BER_TLV_IE, tag=0x85):
|
||||
pass
|
||||
class XcapAuthenticationPassword(BER_TLV_IE, tag=0x86):
|
||||
pass
|
||||
class XcapAuthenticationType(BER_TLV_IE, tag=0x87):
|
||||
pass
|
||||
class AddressType(BER_TLV_IE, tag=0x88):
|
||||
pass
|
||||
class Address(BER_TLV_IE, tag=0x89):
|
||||
pass
|
||||
class PDPAuthenticationType(BER_TLV_IE, tag=0x8a):
|
||||
pass
|
||||
class PDPAuthenticationName(BER_TLV_IE, tag=0x8b):
|
||||
pass
|
||||
class PDPAuthenticationSecret(BER_TLV_IE, tag=0x8c):
|
||||
pass
|
||||
|
||||
class AccessForXCAP(BER_TLV_IE, tag=0x81):
|
||||
pass
|
||||
class NumberOfXcapConnParPolicy(BER_TLV_IE, tag=0x82):
|
||||
_construct = Int8ub
|
||||
# pylint: disable=undefined-variable
|
||||
class XcapConnParamsPolicyPart(BER_TLV_IE, tag=0xa1, nested=[Access, ApplicationName, ProviderID, URI,
|
||||
XcapAuthenticationUserName, XcapAuthenticationPassword,
|
||||
XcapAuthenticationType, AddressType, Address, PDPAuthenticationType,
|
||||
PDPAuthenticationName, PDPAuthenticationSecret]):
|
||||
pass
|
||||
class XcapConnParamsPolicy(BER_TLV_IE, tag=0xa0, nested=[AccessForXCAP, NumberOfXcapConnParPolicy, XcapConnParamsPolicyPart]):
|
||||
pass
|
||||
class XcapConnParamsPolicyDO(BER_TLV_IE, tag=0x80, nested=[XcapConnParamsPolicy]):
|
||||
pass
|
||||
def __init__(self, fid='6ffc', sfid=None, name='EF.XCAPConfigData', desc='XCAP Configuration Data', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_XCAPConfigData.XcapConnParamsPolicy
|
||||
|
||||
# TS 31.103 Section 4.2.20
|
||||
class EF_WebRTCURI(TransparentEF):
|
||||
class uri(BER_TLV_IE, tag=0x80):
|
||||
_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)
|
||||
self._tlv = EF_WebRTCURI.uri
|
||||
|
||||
# TS 31.103 Section 4.2.21
|
||||
class EF_MuDMiDConfigData(BerTlvEF):
|
||||
class MudMidConfigDataEncoding(BER_TLV_IE, tag=0x80):
|
||||
_construct = HexAdapter(Bytes(1))
|
||||
class MudMidConfigData(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyString
|
||||
# pylint: disable=undefined-variable
|
||||
class MudMidConfigDataCollection(TLV_IE_Collection, nested=[MudMidConfigDataEncoding, MudMidConfigData]):
|
||||
pass
|
||||
def __init__(self, fid='6ffe', sfid=None, name='EF.MuDMiDConfigData',
|
||||
desc='MuD and MiD Configuration Data', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._tlv = EF_MuDMiDConfigData.MudMidConfigDataCollection
|
||||
|
||||
|
||||
class ADF_ISIM(CardADF):
|
||||
def __init__(self, aid='a0000000871004', has_fs=True, name='ADF.ISIM', fid=None, sfid=None,
|
||||
@@ -305,13 +217,15 @@ class ADF_ISIM(CardADF):
|
||||
EF_XCAPConfigData(service=19),
|
||||
EF_WebRTCURI(service=20),
|
||||
EF_MuDMiDConfigData(service=21),
|
||||
EF_AC_GBAUAPI(service=2),
|
||||
EF_IMSDCI(service=22),
|
||||
]
|
||||
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)
|
||||
return CardProfileUICC.decode_select_response(data_hex)
|
||||
|
||||
|
||||
# TS 31.103 Section 7.1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user