mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-17 02:48:34 +03:00
Compare commits
1 Commits
laforge/wi
...
laforge/sm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef85dbc28 |
@@ -1,73 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/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)
|
||||
@@ -45,8 +45,7 @@ case "$JOB_TYPE" in
|
||||
--disable E1102 \
|
||||
--disable E0401 \
|
||||
--enable W0301 \
|
||||
pySim tests/*.py *.py \
|
||||
contrib/es2p_client.py
|
||||
pySim *.py
|
||||
;;
|
||||
"docs")
|
||||
rm -rf docs/_build
|
||||
|
||||
@@ -19,11 +19,8 @@ support for profile personalization yet.
|
||||
|
||||
osmo-smdpp currently
|
||||
|
||||
* uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your osmo-smdppp
|
||||
would be running at the host name `testsmdpplus1.example.com`
|
||||
* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
|
||||
of the EID or whether it was donwloaded before
|
||||
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical
|
||||
* always provides the exact same profile to every request. The profile always has the same IMSI and
|
||||
ICCID.
|
||||
* **is absolutely insecure**, as it
|
||||
|
||||
* does not perform any certificate verification
|
||||
@@ -84,8 +81,7 @@ and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
|
||||
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
|
||||
used; they are copied from GSMA SGP.26 v2.
|
||||
|
||||
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
|
||||
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
|
||||
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used.
|
||||
|
||||
|
||||
DNS setup for your LPA
|
||||
@@ -95,20 +91,3 @@ 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.
|
||||
|
||||
@@ -400,19 +400,7 @@ verify_chv
|
||||
|
||||
deactivate_file
|
||||
~~~~~~~~~~~~~~~
|
||||
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).
|
||||
Deactivate the currently selected file. This used to be called INVALIDATE in TS 11.11.
|
||||
|
||||
|
||||
activate_file
|
||||
@@ -473,18 +461,7 @@ 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
|
||||
@@ -952,70 +929,6 @@ 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
|
||||
--------------------
|
||||
|
||||
340
osmo-smdpp.py
340
osmo-smdpp.py
@@ -35,10 +35,7 @@ import asn1tools
|
||||
from pySim.utils import h2b, b2h, swap_nibbles
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim import saip
|
||||
from pySim.esim.es8p import *
|
||||
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
|
||||
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
|
||||
|
||||
# HACK: make this configurable
|
||||
DATA_DIR = './smdpp-data'
|
||||
@@ -73,12 +70,18 @@ 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 Encoding, PublicFormat, PrivateFormat, NoEncryption
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key, 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
|
||||
@@ -87,6 +90,52 @@ 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):
|
||||
@@ -98,6 +147,27 @@ 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()
|
||||
|
||||
@@ -136,7 +206,6 @@ 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)
|
||||
@@ -173,6 +242,36 @@ class SmDppHttpServer:
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def b64decode_members(indict: Dict, keys: List[str]):
|
||||
"""base64-decoder all members of 'indict' whose key is in 'keys'."""
|
||||
for key in keys:
|
||||
if key in indict:
|
||||
indict[key] = b64decode(indict[key])
|
||||
|
||||
@staticmethod
|
||||
def b64encode_members(indict: Dict, keys: List[str]):
|
||||
"""base64-encoder (to string!) all members of 'indict' whose key is in 'keys'."""
|
||||
for key in keys:
|
||||
if key in indict:
|
||||
indict[key] = b64encode2str(indict[key])
|
||||
|
||||
@staticmethod
|
||||
def asn1decode_member(indict: Dict, key: str, typename: Optional[str]):
|
||||
"""decode indict[key] using RSP ASN.1 decoder for type 'typename'."""
|
||||
if not typename:
|
||||
typename = capitalize_first_char(key)
|
||||
if key in indict:
|
||||
indict[key] = rsp.asn1.decode(typename, indict[key])
|
||||
|
||||
@staticmethod
|
||||
def asn1encode_member(indict: Dict, key: str, typename: Optional[str]):
|
||||
"""encode indict[key] using RSP ASN.1 decoder for type 'typename'."""
|
||||
if not typename:
|
||||
typename = capitalize_first_char(key)
|
||||
if key in indict:
|
||||
indict[key] = rsp.asn1.encode(typename, indict[key])
|
||||
|
||||
@staticmethod
|
||||
def rsp_api_wrapper(func):
|
||||
"""Wrapper that can be used as decorator in order to perform common REST API endpoint entry/exit
|
||||
@@ -183,18 +282,16 @@ class SmDppHttpServer:
|
||||
# TODO: reject any non-JSON Content-type
|
||||
|
||||
content = json.loads(request.content.read())
|
||||
print("Rx JSON: %s" % json.dumps(content))
|
||||
print("Rx JSON: %s" % content)
|
||||
set_headers(request)
|
||||
|
||||
output = func(self, request, content) or {}
|
||||
|
||||
build_resp_header(output)
|
||||
print("Tx JSON: %s" % json.dumps(output))
|
||||
print("Tx JSON: %s" % output)
|
||||
return json.dumps(output)
|
||||
return _api_wrapper
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/initiateAuthentication', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def initiateAutentication(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ InitiateAuthentication SGP.22 Section 5.6.1"""
|
||||
# Verify that the received address matches its own SM-DP+ address, where the comparison SHALL be
|
||||
@@ -202,12 +299,11 @@ class SmDppHttpServer:
|
||||
if content['smdpAddress'] != self.server_hostname:
|
||||
raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
|
||||
|
||||
euiccChallenge = b64decode(content['euiccChallenge'])
|
||||
euiccChallenge = content['euiccChallenge']
|
||||
if len(euiccChallenge) != 16:
|
||||
raise ValueError
|
||||
|
||||
euiccInfo1_bin = b64decode(content['euiccInfo1'])
|
||||
euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
|
||||
euiccInfo1 = content['euiccInfo1']
|
||||
print("Rx euiccInfo1: %s" % euiccInfo1)
|
||||
#euiccInfo1['svn']
|
||||
|
||||
@@ -217,17 +313,7 @@ class SmDppHttpServer:
|
||||
if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
|
||||
pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
|
||||
# verify it supports one of the keys indicated by euiccCiPKIdListForSigning
|
||||
ci_cert = None
|
||||
for x in pkid_list:
|
||||
ci_cert = self.ci_get_cert_for_pkid(x)
|
||||
# we already support multiple CI certificates but only one set of DPauth + DPpb keys. So we must
|
||||
# make sure we choose a CI key-id which has issued both the eUICC as well as our own SM-DP side
|
||||
# certs.
|
||||
if ci_cert and cert_get_subject_key_id(ci_cert) == self.dp_auth.get_authority_key_identifier().key_identifier:
|
||||
break
|
||||
else:
|
||||
ci_cert = None
|
||||
if not ci_cert:
|
||||
if not any(self.ci_get_cert_for_pkid(x) for x in pkid_list):
|
||||
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:
|
||||
@@ -256,33 +342,46 @@ class SmDppHttpServer:
|
||||
serverSigned1_bin = rsp.asn1.encode('ServerSigned1', serverSigned1)
|
||||
print("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin))
|
||||
output = {}
|
||||
output['serverSigned1'] = b64encode2str(serverSigned1_bin)
|
||||
output['serverSigned1'] = serverSigned1
|
||||
|
||||
# Generate a signature (serverSignature1) as described in section 5.7.13 "ES10b.AuthenticateServer" using the SK related to the selected CERT.DPauth.SIG.
|
||||
# serverSignature1 SHALL be created using the private key associated to the RSP Server Certificate for authentication, and verified by the eUICC using the contained public key as described in section 2.6.9. serverSignature1 SHALL apply on serverSigned1 data object.
|
||||
output['serverSignature1'] = b64encode2str(b'\x5f\x37\x40' + self.dp_auth.ecdsa_sign(serverSigned1_bin))
|
||||
output['serverSignature1'] = b'\x5f\x37\x40' + self.dp_auth.ecdsa_sign(serverSigned1_bin)
|
||||
|
||||
output['transactionId'] = transactionId
|
||||
server_cert_aki = self.dp_auth.get_authority_key_identifier()
|
||||
output['euiccCiPKIdToBeUsed'] = b64encode2str(b'\x04\x14' + server_cert_aki.key_identifier)
|
||||
output['serverCertificate'] = b64encode2str(self.dp_auth.get_cert_as_der()) # CERT.DPauth.SIG
|
||||
output['euiccCiPKIdToBeUsed'] = b'\x04\x14' + server_cert_aki.key_identifier
|
||||
output['serverCertificate'] = self.dp_auth.get_cert_as_der() # CERT.DPauth.SIG
|
||||
# FIXME: add those certificate
|
||||
#output['otherCertsInChain'] = b64encode2str()
|
||||
#output['otherCertsInChain'] = ...
|
||||
|
||||
# create SessionState and store it in rss
|
||||
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge,
|
||||
cert_get_subject_key_id(ci_cert))
|
||||
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge)
|
||||
|
||||
return output
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/authenticateClient', methods=['POST'])
|
||||
@app.route('/gsma/rsp2/es9plus/initiateAuthentication', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_initiateAuthentication(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
# convert from JSON/BASE64 to decoded ASN.1
|
||||
b64decode_members(content, ['euiccChallenge', 'euiccInfo1', 'lpaRspCapability'])
|
||||
asn1decode_member(content, 'eUICCInfo1')
|
||||
|
||||
# do the actual processing
|
||||
output = self.initiateAuthentication(request, content)
|
||||
|
||||
# convert from decoded ASN.1 to base64 to JSON
|
||||
asn1encode_member(output, 'serverSigned1')
|
||||
b64encode_members(output, ['serverSigned1', 'serverSignature1', 'euiccCiPKIdToBeUsed',
|
||||
'serverCertificate', 'otherCertsInChain', 'crlList'])
|
||||
return output
|
||||
|
||||
def authenticateClient(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ AuthenticateClient in SGP.22 Section 5.6.3"""
|
||||
transactionId = content['transactionId']
|
||||
|
||||
authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
|
||||
authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
|
||||
authenticateServerResp = content['authenticateServerResponse']
|
||||
print("Rx %s: %s" % authenticateServerResp)
|
||||
if authenticateServerResp[0] == 'authenticateResponseError':
|
||||
r_err = authenticateServerResp[1]
|
||||
@@ -306,35 +405,29 @@ class SmDppHttpServer:
|
||||
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
|
||||
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
|
||||
|
||||
# Verify that the transactionId is known and relates to an ongoing RSP session. Otherwise, the SM-DP+
|
||||
# SHALL return a status code "TransactionId - Unknown"
|
||||
ss = self.rss.get(transactionId, None)
|
||||
if ss is None:
|
||||
raise ApiError('8.10.1', '3.9', 'Unknown')
|
||||
ss.euicc_cert = euicc_cert
|
||||
ss.eum_cert = eum_cert # TODO: do we need this in the state?
|
||||
|
||||
# Verify that the Root Certificate of the eUICC certificate chain corresponds to the
|
||||
# euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3
|
||||
if cert_get_auth_key_id(eum_cert) != ss.ci_cert_id:
|
||||
raise ApiError('8.11.1', '3.9', 'Unknown')
|
||||
|
||||
# Verify the validity of the eUICC certificate chain
|
||||
cs = CertificateSet(self.ci_get_cert_for_pkid(ss.ci_cert_id))
|
||||
cs.add_intermediate_cert(eum_cert)
|
||||
# TODO v3: otherCertsInChain
|
||||
try:
|
||||
cs.verify_cert_chain(euicc_cert)
|
||||
except VerifyError:
|
||||
raise ApiError('8.1.3', '6.1', 'Verification failed')
|
||||
# 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?
|
||||
|
||||
# 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
|
||||
@@ -346,32 +439,8 @@ class SmDppHttpServer:
|
||||
if euiccSigned1['serverChallenge'] != ss.serverChallenge:
|
||||
raise ApiError('8.1', '6.1', 'Verification failed')
|
||||
|
||||
# If ctxParams1 contains a ctxParamsForCommonAuthentication data object, the SM-DP+ Shall [...]
|
||||
# TODO: We really do a very simplistic job here, this needs to be properly implemented later,
|
||||
# considering all the various cases, profile state, etc.
|
||||
if euiccSigned1['ctxParams1'][0] == 'ctxParamsForCommonAuthentication':
|
||||
cpca = euiccSigned1['ctxParams1'][1]
|
||||
matchingId = cpca.get('matchingId', None)
|
||||
if not matchingId:
|
||||
# TODO: check if any pending profile downloads for the EID
|
||||
raise ApiError('8.2.6', '3.8', 'Refused')
|
||||
if matchingId:
|
||||
# look up profile based on matchingID. We simply check if a given file exists for now..
|
||||
path = os.path.join(self.upp_dir, matchingId) + '.der'
|
||||
# prevent directory traversal attack
|
||||
if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
|
||||
raise ApiError('8.2.6', '3.8', 'Refused')
|
||||
if not os.path.isfile(path) or not os.access(path, os.R_OK):
|
||||
raise ApiError('8.2.6', '3.8', 'Refused')
|
||||
ss.matchingId = matchingId
|
||||
with open(path, 'rb') as f:
|
||||
pes = saip.ProfileElementSequence.from_der(f.read())
|
||||
iccid_str = b2h(pes.get_pe_for_type('header').decoded['iccid'])
|
||||
|
||||
# FIXME: we actually want to perform the profile binding herr, and read the profile metadat from the profile
|
||||
|
||||
# Put together profileMetadata + _bin
|
||||
ss.profileMetadata = ProfileMetadata(iccid_bin=h2b(swap_nibbles(iccid_str)), spn="OsmocomSPN", profile_name=matchingId)
|
||||
ss.profileMetadata = ProfileMetadata(iccid_bin= h2b(swap_nibbles('89000123456789012358')), spn="OsmocomSPN", profile_name="OsmocomProfile")
|
||||
profileMetadata_bin = ss.profileMetadata.gen_store_metadata_request()
|
||||
|
||||
# Put together smdpSigned2 + _bin
|
||||
@@ -388,14 +457,26 @@ class SmDppHttpServer:
|
||||
self.rss[transactionId] = ss
|
||||
return {
|
||||
'transactionId': transactionId,
|
||||
'profileMetadata': b64encode2str(profileMetadata_bin),
|
||||
'smdpSigned2': b64encode2str(smdpSigned2_bin),
|
||||
'smdpSignature2': b64encode2str(ss.smdpSignature2_do),
|
||||
'smdpCertificate': b64encode2str(self.dp_pb.get_cert_as_der()), # CERT.DPpb.SIG
|
||||
'profileMetadata': profileMetadata,
|
||||
'smdpSigned2': smdpSigned2,
|
||||
'smdpSignature2': ss.smdpSignature2_do,
|
||||
'smdpCertificate': self.dp_pb.get_cert_as_der(), # CERT.DPpb.SIG
|
||||
}
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST'])
|
||||
@app.route('/gsma/rsp2/es9plus/authenticateClient', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_authenticateClient(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['authenticateServerResponse', 'deleteNotifciationForDc'])
|
||||
asn1decode_member(content, 'authenticateServerResponse')
|
||||
|
||||
output = self.authenticateClient(requeset, content)
|
||||
|
||||
asn1encode_member(output, 'smdpSigned2')
|
||||
b64encode_members(output, ['profileMetadata', 'smdpSigned2', 'smdpSignature2', 'smdpCertificate'])
|
||||
return output
|
||||
|
||||
|
||||
def getBoundProfilePackage(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ GetBoundProfilePackage SGP.22 Section 5.6.2"""
|
||||
transactionId = content['transactionId']
|
||||
@@ -405,8 +486,7 @@ class SmDppHttpServer:
|
||||
if not ss:
|
||||
raise ApiError('8.10.1', '3.9', 'The RSP session identified by the TransactionID is unknown')
|
||||
|
||||
prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
|
||||
prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
|
||||
prepDownloadResp = content['prepareDownloadResponse']
|
||||
print("Rx %s: %s" % prepDownloadResp)
|
||||
|
||||
if prepDownloadResp[0] == 'downloadResponseError':
|
||||
@@ -451,7 +531,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(self.upp_dir, ss.matchingId)+'.der', 'rb') as f:
|
||||
with open(os.path.join(DATA_DIR, 'upp', 'TS48 V2 eSIM_GTP_SAIP2.1_NoBERTLV.rename2der'), '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
|
||||
@@ -468,15 +548,26 @@ class SmDppHttpServer:
|
||||
self.rss[transactionId] = ss
|
||||
return {
|
||||
'transactionId': transactionId,
|
||||
'boundProfilePackage': b64encode2str(bpp.encode(ss, self.dp_pb)),
|
||||
'boundProfilePackage': bpp.encode(ss, self.dp_pb),
|
||||
}
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST'])
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_getBoundProfilePackage(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['prepareDownloadResponse'])
|
||||
asn1decode_member(content, 'prepareDownlaodResponse')
|
||||
|
||||
output = self.getBoundProfilePackage(request, content)
|
||||
|
||||
asn1encode_member(output, 'boundProfilePackage')
|
||||
b64encode_members(output, ['boundProfilePackage'])
|
||||
return output
|
||||
|
||||
def handleNotification(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ HandleNotification in SGP.22 Section 5.6.4"""
|
||||
pendingNotification_bin = b64decode(content['pendingNotification'])
|
||||
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
|
||||
pendingNotification = content['pendingNotification']
|
||||
print("Rx %s: %s" % pendingNotification)
|
||||
if pendingNotification[0] == 'profileInstallationResult':
|
||||
profileInstallRes = pendingNotification[1]
|
||||
@@ -500,17 +591,27 @@ class SmDppHttpServer:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(pendingNotification)
|
||||
return None
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_handleNotification(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['pendingNotification'])
|
||||
asn1decode_member(content, 'pendingNotification')
|
||||
|
||||
output = self.handleNotification(request, content)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
#@app.route('/gsma/rsp3/es9plus/handleDeviceChangeRequest, methods=['POST']')
|
||||
#@rsp_api_wrapper
|
||||
#"""See ES9+ ConfirmDeviceChange in SGP.22 Section 5.6.6"""
|
||||
# TODO: implement this
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def cancelSession(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ CancelSession in SGP.22 Section 5.6.5"""
|
||||
print("Rx JSON: %s" % content)
|
||||
transactionId = content['transactionId']
|
||||
|
||||
# Verify that the received transactionId is known and relates to an ongoing RSP session
|
||||
@@ -518,8 +619,7 @@ class SmDppHttpServer:
|
||||
if ss is None:
|
||||
raise ApiError('8.10.1', '3.9', 'The RSP session identified by the transactionId is unknown')
|
||||
|
||||
cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
|
||||
cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
|
||||
cancelSessionResponse = content['cancelSessionResponse']
|
||||
print("Rx %s: %s" % cancelSessionResponse)
|
||||
|
||||
if cancelSessionResponse[0] == 'cancelSessionResponseError':
|
||||
@@ -549,6 +649,54 @@ class SmDppHttpServer:
|
||||
del self.rss[transactionId]
|
||||
return { 'transactionId': transactionId }
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_cancelSessionn(self, request: IRequest, content: dict) -> dict:
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['cancelSessionResponse'])
|
||||
asn1decode_member(content, 'cancelSessionResponse')
|
||||
|
||||
output = self.cancelSession(request, content)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@app.route('/gsma/rsp2/asn1', methods=['POST'])
|
||||
def asn1(self, request: IRequest) -> dict:
|
||||
# TODO: evaluate User-Agent + X-Admin-Protocol header
|
||||
# TODO: reject any non-ASN.1 Content-type
|
||||
|
||||
content = rsp.asn1.decode('RemoteProfileProvisioningRequest', request.content.read())
|
||||
print("Rx ASN.1: %s" % content)
|
||||
request.setHeader('Content-Type', 'application/x-gsma-rsp-asn1')
|
||||
request.setHeader('X-Admin-Protocol', 'gsma/rsp/v2.1.0')
|
||||
|
||||
operation = content[0]
|
||||
if operation == 'initiateAuthenticationRequest':
|
||||
method = self.initiateAuthentication
|
||||
ochoice = 'initiateAuthenticationResponse'
|
||||
elif operation == 'authenticateClientRequest':
|
||||
method = self.authenticateClient
|
||||
ochoice = 'authenticateClientResponseEs9'
|
||||
elif operation == 'getBoundProfilePackageRequest':
|
||||
method = self.getBoundProfilePackage
|
||||
ochoice = 'getBoundProfilePackageResponse'
|
||||
elif operation == 'cancelSessionRequestEs9':
|
||||
method = self.cancelSession
|
||||
ochoice = 'cancelSessionResponseEs9'
|
||||
elif operation == 'handleNotification':
|
||||
method = self.handleNotification
|
||||
ochoice = None
|
||||
|
||||
output = method(self, request, content[1])
|
||||
|
||||
if ochoice:
|
||||
output = (ochoice, output)
|
||||
print("Tx ASN.1: %s" % output)
|
||||
return rsp.asn1.encode('RemoteProfileProvisioningResponse', output)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def main(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -205,11 +205,7 @@ 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)
|
||||
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)
|
||||
self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
|
||||
else:
|
||||
if self.card:
|
||||
self.prompt = 'pySIM-shell (no card profile)> '
|
||||
@@ -237,7 +233,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
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('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
|
||||
|
||||
@cmd2.with_argparser(apdu_cmd_parser)
|
||||
def do_apdu(self, opts):
|
||||
@@ -250,10 +245,7 @@ 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).
|
||||
if opts.raw or self.lchan is None:
|
||||
data, sw = self.card._scc.send_apdu(opts.APDU)
|
||||
else:
|
||||
data, sw = self.lchan.scc.send_apdu(opts.APDU)
|
||||
data, sw = self.card._scc._tp.send_apdu(opts.APDU)
|
||||
if data:
|
||||
self.poutput("SW: %s, RESP: %s" % (sw, data))
|
||||
else:
|
||||
@@ -262,15 +254,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
if not sw_match(sw, opts.expect_sw):
|
||||
raise SwMatchError(sw, opts.expect_sw)
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_reset(self, opts):
|
||||
"""Reset the Card."""
|
||||
atr = self.card.reset()
|
||||
if self.lchan and self.lchan.scc.scp:
|
||||
self.lchan.scc.scp = None
|
||||
self.poutput('Card ATR: %s' % i2h(atr))
|
||||
self.update_prompt()
|
||||
|
||||
class InterceptStderr(list):
|
||||
def __init__(self):
|
||||
self._stderr_backup = sys.stderr
|
||||
@@ -719,6 +702,12 @@ 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 do_desc(self, opts):
|
||||
"""Display human readable file description for the currently selected file"""
|
||||
desc = self._cmd.lchan.selected_file.desc
|
||||
@@ -893,15 +882,8 @@ 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 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!"""
|
||||
"""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."""
|
||||
(data, sw) = self._cmd.lchan.activate_file(opts.NAME)
|
||||
|
||||
def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# coding=utf-8
|
||||
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
|
||||
|
||||
The File (and its classes) represent the structure / hierarchy
|
||||
@@ -26,13 +27,12 @@ we already know in pySim about the filesystem structure, file encoding, etc.
|
||||
|
||||
|
||||
import abc
|
||||
from termcolor import colored
|
||||
import typing
|
||||
from typing import List, Dict, Optional
|
||||
from termcolor import colored
|
||||
|
||||
from construct import Byte, GreedyBytes
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
|
||||
from pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
|
||||
@@ -52,8 +52,8 @@ from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
||||
class ApduCommandMeta(abc.ABCMeta):
|
||||
"""A meta-class that we can use to set some class variables when declaring
|
||||
a derived class of ApduCommand."""
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
x = super().__new__(mcs, name, bases, namespace)
|
||||
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||
x = super().__new__(metacls, 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))
|
||||
@@ -187,39 +187,44 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
if apdu_case in [1, 2]:
|
||||
# data is part of response
|
||||
return cls(buffer[:5], buffer[5:])
|
||||
if apdu_case in [3, 4]:
|
||||
elif apdu_case in [3, 4]:
|
||||
# data is part of command
|
||||
lc = buffer[4]
|
||||
return cls(buffer[:5+lc], buffer[5+lc:])
|
||||
raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
|
||||
else:
|
||||
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()
|
||||
return []
|
||||
else:
|
||||
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()
|
||||
return ''
|
||||
else:
|
||||
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')
|
||||
return colored(b2h(self.sw), 'red')
|
||||
else:
|
||||
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
|
||||
return lchan_nr_from_cla(self.cla)
|
||||
else:
|
||||
return lchan_nr_from_cla(self.cla)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '%02u %s(%s): %s' % (self.lchan_nr, type(self).__name__, self.path_str, self.to_dict())
|
||||
@@ -231,7 +236,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 'p1' not in self.cmd_dict:
|
||||
if not 'p1' in self.cmd_dict:
|
||||
self.processed = self.to_dict()
|
||||
else:
|
||||
self.processed['p1'] = self.cmd_dict['p1']
|
||||
@@ -423,7 +428,8 @@ class TpduFilter(ApduHandler):
|
||||
apdu = Apdu(icmd, tpdu.rsp)
|
||||
if self.apdu_handler:
|
||||
return self.apdu_handler.input(apdu)
|
||||
return Apdu(icmd, tpdu.rsp)
|
||||
else:
|
||||
return Apdu(icmd, tpdu.rsp)
|
||||
|
||||
def input(self, cmd: bytes, rsp: bytes):
|
||||
if isinstance(cmd, str):
|
||||
@@ -446,6 +452,7 @@ class CardReset:
|
||||
self.atr = atr
|
||||
|
||||
def __str__(self):
|
||||
if self.atr:
|
||||
if (self.atr):
|
||||
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
|
||||
return '%s' % (type(self).__name__)
|
||||
else:
|
||||
return '%s' % (type(self).__name__)
|
||||
|
||||
@@ -17,16 +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 typing import Optional, Dict
|
||||
import logging
|
||||
|
||||
from construct import GreedyRange, Struct
|
||||
|
||||
from pySim.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.runtime import RuntimeLchan
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
from pySim.utils import i2h
|
||||
from typing import Optional, Dict, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -107,6 +103,7 @@ 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
|
||||
@@ -293,9 +290,12 @@ class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
|
||||
|
||||
@staticmethod
|
||||
def _pin_is_success(sw):
|
||||
return bool(sw[0] == 0x63)
|
||||
if sw[0] == 0x63:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
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 +307,7 @@ class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -319,7 +319,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -330,7 +330,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
|
||||
class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -342,7 +342,7 @@ class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X'])
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -395,12 +395,13 @@ 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 }
|
||||
if mode == 'close_channel':
|
||||
elif 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 }
|
||||
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
|
||||
else:
|
||||
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']):
|
||||
@@ -418,13 +419,13 @@ class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=
|
||||
p2 = hdr[3]
|
||||
if p1 & 0x7 == 0: # retrieve UICC Endpoints
|
||||
return 2
|
||||
if p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
|
||||
elif 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
|
||||
if p2_cmd in [1,3,5]: # response data
|
||||
elif p2_cmd in [1,3,5]: # response data
|
||||
return 2
|
||||
if p1 & 0xf == 4: # terminate secure channel SA
|
||||
elif p1 & 0xf == 4: # terminate secure channel SA
|
||||
return 3
|
||||
raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
|
||||
|
||||
@@ -435,7 +436,8 @@ class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6
|
||||
p1 = hdr[2]
|
||||
if p1 & 0x04:
|
||||
return 3
|
||||
return 2
|
||||
else:
|
||||
return 2
|
||||
|
||||
# TS 102 221 Section 11.1.22
|
||||
class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
|
||||
|
||||
@@ -9,12 +9,12 @@ APDU commands of 3GPP TS 31.102 V16.6.0
|
||||
"""
|
||||
from typing import Dict
|
||||
|
||||
from construct import BitStruct, Enum, BitsInteger, Int8ub, Bytes, this, Struct, If, Switch, Const
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
|
||||
from pySim.filesystem import *
|
||||
from pySim.construct import *
|
||||
from pySim.ts_31_102 import SUCI_TlvDataObject
|
||||
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
|
||||
# Copyright (C) 2022 Harald Welte <laforge@osmocom.org>
|
||||
@@ -35,6 +35,8 @@ 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
|
||||
|
||||
@@ -14,6 +14,7 @@ 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."""
|
||||
@@ -30,5 +31,5 @@ class ApduSource(abc.ABC):
|
||||
elif isinstance(r, CardReset):
|
||||
apdu = r
|
||||
else:
|
||||
raise ValueError('Unknown read_packet() return %s' % r)
|
||||
ValueError('Unknown read_packet() return %s' % r)
|
||||
return apdu
|
||||
|
||||
@@ -16,14 +16,12 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pySim.gsmtap import GsmtapSource
|
||||
from pySim.gsmtap import GsmtapMessage, GsmtapSource
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
class GsmtapApduSource(ApduSource):
|
||||
@@ -43,16 +41,16 @@ class GsmtapApduSource(ApduSource):
|
||||
self.gsmtap = GsmtapSource(bind_ip, bind_port)
|
||||
|
||||
def read_packet(self) -> PacketType:
|
||||
gsmtap_msg, _addr = self.gsmtap.read_packet()
|
||||
gsmtap_msg, addr = self.gsmtap.read_packet()
|
||||
if gsmtap_msg['type'] != 'sim':
|
||||
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
|
||||
sub_type = gsmtap_msg['sub_type']
|
||||
if sub_type == 'apdu':
|
||||
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
||||
if sub_type == 'atr':
|
||||
elif sub_type == 'atr':
|
||||
# card has been reset
|
||||
return CardReset(gsmtap_msg['body'])
|
||||
if sub_type in ['pps_req', 'pps_rsp']:
|
||||
elif sub_type in ['pps_req', 'pps_rsp']:
|
||||
# simply ignore for now
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -16,19 +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 pySim.utils import h2b
|
||||
from pySim.utils import h2b, b2h
|
||||
from pySim.apdu import Tpdu
|
||||
from pySim.gsmtap import GsmtapMessage
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -66,10 +67,10 @@ class _PysharkGsmtap(ApduSource):
|
||||
sub_type = gsmtap_msg['sub_type']
|
||||
if sub_type == 'apdu':
|
||||
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
||||
if sub_type == 'atr':
|
||||
elif sub_type == 'atr':
|
||||
# card has been reset
|
||||
return CardReset(gsmtap_msg['body'])
|
||||
if sub_type in ['pps_req', 'pps_rsp']:
|
||||
elif sub_type in ['pps_req', 'pps_rsp']:
|
||||
# simply ignore for now
|
||||
pass
|
||||
else:
|
||||
@@ -86,3 +87,4 @@ class PysharkGsmtapPcap(_PysharkGsmtap):
|
||||
"""
|
||||
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', use_json=True, keep_packets=False)
|
||||
super().__init__(pyshark_inst)
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
# 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
|
||||
from pySim.utils import h2b, b2h
|
||||
from pySim.apdu import Tpdu
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ 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.runtime import RuntimeState
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.cdma_ruim import CardProfileRUIM
|
||||
@@ -107,3 +108,6 @@ def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
|
||||
sl.set_sw_interpreter(rs)
|
||||
|
||||
return rs, card
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,13 +26,11 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
|
||||
#
|
||||
|
||||
|
||||
from construct import GreedyBytes, GreedyString, Struct, Enum, Int8ub, Int16ub
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
from pySim.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.tlv import *
|
||||
from pySim.utils import Hexstr
|
||||
import pySim.global_platform
|
||||
|
||||
# various BER-TLV encoded Data Objects (DOs)
|
||||
|
||||
@@ -69,10 +67,11 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
if do[0] == 0x00:
|
||||
self.decoded = {'generic_access_rule': 'never'}
|
||||
return self.decoded
|
||||
if do[0] == 0x01:
|
||||
elif do[0] == 0x01:
|
||||
self.decoded = {'generic_access_rule': 'always'}
|
||||
return self.decoded
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
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))
|
||||
@@ -88,9 +87,10 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
if 'generic_access_rule' in self.decoded:
|
||||
if self.decoded['generic_access_rule'] == 'never':
|
||||
return b'\x00'
|
||||
if self.decoded['generic_access_rule'] == 'always':
|
||||
elif self.decoded['generic_access_rule'] == 'always':
|
||||
return b'\x01'
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
if not 'apdu_filter' in self.decoded:
|
||||
return ValueError('Invalid APDU AR DO')
|
||||
@@ -115,7 +115,6 @@ 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)))
|
||||
|
||||
|
||||
@@ -260,9 +259,6 @@ class ADF_ARAM(CardADF):
|
||||
files = []
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
return pySim.global_platform.decode_select_response(data_hex)
|
||||
|
||||
@staticmethod
|
||||
def xceive_apdu_tlv(tp, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
|
||||
"""Transceive an APDU with the card, transparently encoding the command data from TLV
|
||||
@@ -276,13 +272,14 @@ class ADF_ARAM(CardADF):
|
||||
cmd_do_enc = b''
|
||||
cmd_do_len = 0
|
||||
c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
|
||||
(data, _sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
|
||||
(data, sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
|
||||
if data:
|
||||
if resp_cls:
|
||||
resp_do = resp_cls()
|
||||
resp_do.from_tlv(h2b(data))
|
||||
return resp_do
|
||||
return data
|
||||
else:
|
||||
return data
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -304,13 +301,16 @@ class ADF_ARAM(CardADF):
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def do_aram_get_all(self, _opts):
|
||||
def __init(self):
|
||||
super().__init__()
|
||||
|
||||
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)
|
||||
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)
|
||||
if res_do:
|
||||
@@ -348,7 +348,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 is not None:
|
||||
if opts.aid:
|
||||
ref_do_content += [{'aid_ref_do': opts.aid}]
|
||||
elif opts.aid_empty:
|
||||
ref_do_content += [{'aid_ref_empty_do': None}]
|
||||
@@ -377,7 +377,7 @@ class ADF_ARAM(CardADF):
|
||||
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)
|
||||
|
||||
@@ -24,11 +24,12 @@ 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."""
|
||||
@@ -96,7 +97,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') is True
|
||||
self.verbose = (self.cmds.get('verbose') == True)
|
||||
|
||||
def __print_outout(self, out):
|
||||
print("")
|
||||
|
||||
@@ -54,11 +54,11 @@ class CardKeyProvider(abc.ABC):
|
||||
dictionary of {field, value} strings for each requested field from 'fields'
|
||||
"""
|
||||
for f in fields:
|
||||
if f not in self.VALID_FIELD_NAMES:
|
||||
if (f not in self.VALID_FIELD_NAMES):
|
||||
raise ValueError("Requested field name '%s' is not a valid field name, valid field names are: %s" %
|
||||
(f, str(self.VALID_FIELD_NAMES)))
|
||||
|
||||
if key not in self.VALID_FIELD_NAMES:
|
||||
if (key not in self.VALID_FIELD_NAMES):
|
||||
raise ValueError("Key field name '%s' is not a valid field name, valid field names are: %s" %
|
||||
(key, str(self.VALID_FIELD_NAMES)))
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import Optional, Tuple
|
||||
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 pySim.utils import *
|
||||
from pySim.commands import Path, SimCardCommands
|
||||
@@ -39,7 +40,8 @@ class CardBase:
|
||||
rc = self._scc.reset_card()
|
||||
if rc == 1:
|
||||
return self._scc.get_atr()
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
|
||||
"""Set apdu parameters (class byte and selection control bytes)"""
|
||||
@@ -52,6 +54,7 @@ class CardBase:
|
||||
|
||||
def erase(self):
|
||||
print("warning: erasing is not supported for specified card type!")
|
||||
return
|
||||
|
||||
def file_exists(self, fid: Path) -> bool:
|
||||
res_arr = self._scc.try_select_path(fid)
|
||||
@@ -72,7 +75,7 @@ class SimCardBase(CardBase):
|
||||
name = 'SIM'
|
||||
|
||||
def __init__(self, scc: SimCardCommands):
|
||||
super().__init__(scc)
|
||||
super(SimCardBase, self).__init__(scc)
|
||||
self._scc.cla_byte = "A0"
|
||||
self._scc.sel_ctrl = "0000"
|
||||
|
||||
@@ -85,7 +88,7 @@ class UiccCardBase(SimCardBase):
|
||||
name = 'UICC'
|
||||
|
||||
def __init__(self, scc: SimCardCommands):
|
||||
super().__init__(scc)
|
||||
super(UiccCardBase, self).__init__(scc)
|
||||
self._scc.cla_byte = "00"
|
||||
self._scc.sel_ctrl = "0004" # request an FCP
|
||||
# See also: ETSI TS 102 221, Table 9.3
|
||||
@@ -155,8 +158,9 @@ class UiccCardBase(SimCardBase):
|
||||
aid_full = self._complete_aid(aid)
|
||||
if aid_full:
|
||||
return scc.select_adf(aid_full)
|
||||
# If we cannot get the full AID, try with short AID
|
||||
return scc.select_adf(aid)
|
||||
else:
|
||||
# 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]:
|
||||
|
||||
26
pySim/cat.py
26
pySim/cat.py
@@ -18,14 +18,14 @@ as described in 3GPP TS 31.111."""
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from typing import List
|
||||
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 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 pySim.utils import b2h, dec_xplmn_w_act
|
||||
from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger
|
||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil
|
||||
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
|
||||
|
||||
# Tag values as per TS 101 220 Table 7.23
|
||||
|
||||
@@ -583,11 +583,11 @@ class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
||||
|
||||
# TS 31.111 Section 8.90
|
||||
class PlmnWactList(COMPR_TLV_IE, tag=0xF2):
|
||||
def _from_bytes(self, do: bytes):
|
||||
def _from_bytes(self, x):
|
||||
r = []
|
||||
i = 0
|
||||
while i < len(do):
|
||||
r.append(dec_xplmn_w_act(b2h(do[i:i+5])))
|
||||
while i < len(x):
|
||||
r.append(dec_xplmn_w_act(b2h(x[i:i+5])))
|
||||
i += 5
|
||||
return r
|
||||
|
||||
@@ -978,8 +978,8 @@ class ProactiveCommandBase(BER_TLV_IE, tag=0xD0, nested=[CommandDetails]):
|
||||
for c in self.children:
|
||||
if type(c).__name__ == 'CommandDetails':
|
||||
return c
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
class ProactiveCommand(TLV_IE_Collection,
|
||||
nested=[Refresh, MoreTime, PollInterval, PollingOff, SetUpEventList, SetUpCall,
|
||||
@@ -997,7 +997,7 @@ class ProactiveCommand(TLV_IE_Collection,
|
||||
more difficult than any normal TLV IE Collection, because the content of one of the IEs defines the
|
||||
definitions of all the other IEs. So we first need to find the CommandDetails, and then parse according
|
||||
to the command type indicated in that IE data."""
|
||||
def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
|
||||
def from_bytes(self, binary: bytes) -> List[TLV_IE]:
|
||||
# do a first parse step to get the CommandDetails
|
||||
pcmd = ProactiveCommandBase()
|
||||
pcmd.from_tlv(binary)
|
||||
@@ -1007,7 +1007,7 @@ class ProactiveCommand(TLV_IE_Collection,
|
||||
if cmd_type in self.members_by_tag:
|
||||
cls = self.members_by_tag[cmd_type]
|
||||
inst = cls()
|
||||
_dec, remainder = inst.from_tlv(binary)
|
||||
dec, remainder = inst.from_tlv(binary)
|
||||
self.decoded = inst
|
||||
else:
|
||||
self.decoded = pcmd
|
||||
@@ -1019,7 +1019,7 @@ class ProactiveCommand(TLV_IE_Collection,
|
||||
def to_dict(self):
|
||||
return self.decoded.to_dict()
|
||||
|
||||
def to_bytes(self, context: dict = {}):
|
||||
def to_bytes(self):
|
||||
return self.decoded.to_tlv()
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import enum
|
||||
|
||||
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
|
||||
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import match_ruim
|
||||
@@ -29,6 +27,7 @@ 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 *
|
||||
|
||||
|
||||
# Mapping between CDMA Service Number and its description
|
||||
@@ -186,9 +185,9 @@ class CardProfileRUIM(CardProfile):
|
||||
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
def decode_select_response(resp_hex: str) -> object:
|
||||
# TODO: Response parameters/data in case of DF_CDMA (section 2.6)
|
||||
return CardProfileSIM.decode_select_response(data_hex)
|
||||
return CardProfileSIM.decode_select_response(resp_hex)
|
||||
|
||||
@staticmethod
|
||||
def match_with_card(scc: SimCardCommands) -> bool:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#
|
||||
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
||||
# Copyright (C) 2010-2024 Harald Welte <laforge@gnumonks.org>
|
||||
# Copyright (C) 2010-2023 Harald Welte <laforge@gnumonks.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -21,13 +21,12 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import List, Tuple
|
||||
from typing import List, Optional, Tuple
|
||||
import typing # construct also has a Union, so we do typing.Union below
|
||||
|
||||
from construct import Construct, Struct, Const, Select
|
||||
from construct import Optional as COptional
|
||||
from pySim.construct import LV, filter_dict
|
||||
from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, h2i, i2h, str_sanitize, expand_hex, SwMatchstr
|
||||
from 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.exceptions import SwMatchError
|
||||
from pySim.transport import LinkBase
|
||||
@@ -70,7 +69,6 @@ class SimCardCommands:
|
||||
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."""
|
||||
@@ -84,14 +82,6 @@ class SimCardCommands:
|
||||
"""Return the (cached) patched default CLA byte for this card."""
|
||||
return self._cla4lchan
|
||||
|
||||
@property
|
||||
def max_cmd_len(self) -> int:
|
||||
"""Maximum length of the command apdu data section. Depends on secure channel protocol used."""
|
||||
if self.scp:
|
||||
return 255 - self.scp.overhead
|
||||
else:
|
||||
return 255
|
||||
|
||||
@cla_byte.setter
|
||||
def cla_byte(self, new_val: Hexstr):
|
||||
"""Set the (raw, without lchan) default CLA value for this card."""
|
||||
@@ -110,87 +100,6 @@ class SimCardCommands:
|
||||
else:
|
||||
return cla_with_lchan(cla, self.lchan_nr)
|
||||
|
||||
def send_apdu(self, pdu: Hexstr) -> ResTuple:
|
||||
"""Sends an APDU and auto fetch response data
|
||||
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
if self.scp:
|
||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
|
||||
else:
|
||||
return self._tp.send_apdu(pdu)
|
||||
|
||||
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
|
||||
"""Sends an APDU and check returned SW
|
||||
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
|
||||
digits using a '?' to add some ambiguity if needed.
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
if self.scp:
|
||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
|
||||
else:
|
||||
return self._tp.send_apdu_checksw(pdu, sw)
|
||||
|
||||
def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,
|
||||
cmd_data: Hexstr, resp_constr: Construct) -> Tuple[dict, SwHexstr]:
|
||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
||||
|
||||
Args:
|
||||
cla : string (in hex) ISO 7816 class byte
|
||||
ins : string (in hex) ISO 7816 instruction byte
|
||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
||||
cmd_cosntr : defining how to generate binary APDU command data
|
||||
cmd_data : command data passed to cmd_constr
|
||||
resp_cosntr : defining how to decode binary APDU response data
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
|
||||
p3 = i2h([len(cmd)])
|
||||
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
|
||||
(data, sw) = self.send_apdu(pdu)
|
||||
if data:
|
||||
# filter the resulting dict to avoid '_io' members inside
|
||||
rsp = filter_dict(resp_constr.parse(h2b(data)))
|
||||
else:
|
||||
rsp = None
|
||||
return (rsp, sw)
|
||||
|
||||
def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
|
||||
cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
|
||||
sw_exp: SwMatchstr="9000") -> Tuple[dict, SwHexstr]:
|
||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
||||
|
||||
Args:
|
||||
cla : string (in hex) ISO 7816 class byte
|
||||
ins : string (in hex) ISO 7816 instruction byte
|
||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
||||
cmd_cosntr : defining how to generate binary APDU command data
|
||||
cmd_data : command data passed to cmd_constr
|
||||
resp_cosntr : defining how to decode binary APDU response data
|
||||
exp_sw : string (in hex) of status word (ex. "9000")
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
(rsp, sw) = self.send_apdu_constr(cla, ins,
|
||||
p1, p2, cmd_constr, cmd_data, resp_constr)
|
||||
if not sw_match(sw, sw_exp):
|
||||
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
|
||||
return (rsp, sw)
|
||||
|
||||
# Extract a single FCP item from TLV
|
||||
def __parse_fcp(self, fcp: Hexstr):
|
||||
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
|
||||
@@ -258,10 +167,11 @@ class SimCardCommands:
|
||||
"""
|
||||
|
||||
rv = []
|
||||
if not isinstance(dir_list, list):
|
||||
if type(dir_list) is not list:
|
||||
dir_list = [dir_list]
|
||||
for i in dir_list:
|
||||
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
|
||||
rv.append((data, sw))
|
||||
if sw != '9000':
|
||||
return rv
|
||||
@@ -277,10 +187,10 @@ class SimCardCommands:
|
||||
list of return values (FCP in hex encoding) for each element of the path
|
||||
"""
|
||||
rv = []
|
||||
if not isinstance(dir_list, list):
|
||||
if type(dir_list) is not 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
|
||||
|
||||
@@ -291,11 +201,11 @@ class SimCardCommands:
|
||||
fid : file identifier as hex string
|
||||
"""
|
||||
|
||||
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
|
||||
|
||||
def select_parent_df(self) -> ResTuple:
|
||||
"""Execute SELECT to switch to the parent DF """
|
||||
return self.send_apdu_checksw(self.cla_byte + "a4030400")
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
|
||||
|
||||
def select_adf(self, aid: Hexstr) -> ResTuple:
|
||||
"""Execute SELECT a given Applicaiton ADF.
|
||||
@@ -305,7 +215,7 @@ class SimCardCommands:
|
||||
"""
|
||||
|
||||
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
|
||||
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
|
||||
|
||||
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
|
||||
"""Execute READD BINARY.
|
||||
@@ -326,14 +236,14 @@ class SimCardCommands:
|
||||
total_data = ''
|
||||
chunk_offset = 0
|
||||
while chunk_offset < length:
|
||||
chunk_len = min(self.max_cmd_len, length-chunk_offset)
|
||||
chunk_len = min(255, length-chunk_offset)
|
||||
pdu = self.cla_byte + \
|
||||
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
|
||||
try:
|
||||
data, sw = self.send_apdu_checksw(pdu)
|
||||
data, sw = self._tp.send_apdu_checksw(pdu)
|
||||
except Exception as e:
|
||||
raise ValueError('%s, failed to read (offset %d)' %
|
||||
(str_sanitize(str(e)), offset)) from e
|
||||
(str_sanitize(str(e)), offset))
|
||||
total_data += data
|
||||
chunk_offset += chunk_len
|
||||
return total_data, sw
|
||||
@@ -384,16 +294,16 @@ class SimCardCommands:
|
||||
total_data = ''
|
||||
chunk_offset = 0
|
||||
while chunk_offset < data_length:
|
||||
chunk_len = min(self.max_cmd_len, data_length - chunk_offset)
|
||||
chunk_len = min(255, 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.send_apdu_checksw(pdu)
|
||||
chunk_data, chunk_sw = self._tp.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)) from e
|
||||
(str_sanitize(str(e)), chunk_offset, chunk_len))
|
||||
total_data += data
|
||||
chunk_offset += chunk_len
|
||||
if verify:
|
||||
@@ -410,7 +320,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.send_apdu_checksw(pdu)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def __verify_record(self, ef: Path, rec_no: int, data: str):
|
||||
"""Verify record against given data
|
||||
@@ -449,10 +359,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:
|
||||
@@ -473,7 +383,7 @@ class SimCardCommands:
|
||||
pass
|
||||
|
||||
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
|
||||
res = self.send_apdu_checksw(pdu)
|
||||
res = self._tp.send_apdu_checksw(pdu)
|
||||
if verify:
|
||||
self.__verify_record(ef, rec_no, data)
|
||||
return res
|
||||
@@ -511,7 +421,7 @@ class SimCardCommands:
|
||||
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
|
||||
else:
|
||||
pdu = self.cla4lchan('80') + 'cb000000'
|
||||
return self.send_apdu_checksw(pdu)
|
||||
return self._tp.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.
|
||||
@@ -527,7 +437,7 @@ class SimCardCommands:
|
||||
# retrieve first block
|
||||
data, sw = self._retrieve_data(tag, first=True)
|
||||
total_data += data
|
||||
while sw in ['62f1', '62f2']:
|
||||
while sw == '62f1' or sw == '62f2':
|
||||
data, sw = self._retrieve_data(tag, first=False)
|
||||
total_data += data
|
||||
return total_data, sw
|
||||
@@ -538,10 +448,10 @@ class SimCardCommands:
|
||||
p1 = 0x80
|
||||
else:
|
||||
p1 = 0x00
|
||||
if isinstance(data, (bytes, bytearray)):
|
||||
if isinstance(data, bytes) or isinstance(data, bytearray):
|
||||
data = b2h(data)
|
||||
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
|
||||
return self.send_apdu_checksw(pdu)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
|
||||
"""Execute SET DATA.
|
||||
@@ -568,10 +478,10 @@ class SimCardCommands:
|
||||
total_len = len(tlv_bin)
|
||||
remaining = tlv_bin
|
||||
while len(remaining) > 0:
|
||||
fragment = remaining[:self.max_cmd_len]
|
||||
fragment = remaining[:255]
|
||||
rdata, sw = self._set_data(fragment, first=first)
|
||||
first = False
|
||||
remaining = remaining[self.max_cmd_len:]
|
||||
remaining = remaining[255:]
|
||||
return rdata, sw
|
||||
|
||||
def run_gsm(self, rand: Hexstr) -> ResTuple:
|
||||
@@ -583,7 +493,7 @@ class SimCardCommands:
|
||||
if len(rand) != 32:
|
||||
raise ValueError('Invalid rand')
|
||||
self.select_path(['3f00', '7f20'])
|
||||
return self.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
|
||||
|
||||
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
|
||||
"""Execute AUTHENTICATE (USIM/ISIM).
|
||||
@@ -594,9 +504,10 @@ class SimCardCommands:
|
||||
context : 16 byte random data ('3g' or 'gsm')
|
||||
"""
|
||||
# 3GPP TS 31.102 Section 7.1.2.1
|
||||
AuthCmd3G = Struct('rand'/LV, 'autn'/COptional(LV))
|
||||
AuthCmd3G = Struct('rand'/LV, 'autn'/Optional(LV))
|
||||
AuthResp3GSyncFail = Struct(Const(b'\xDC'), 'auts'/LV)
|
||||
AuthResp3GSuccess = Struct(Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/COptional(LV))
|
||||
AuthResp3GSuccess = Struct(
|
||||
Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/Optional(LV))
|
||||
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
|
||||
# build parameters
|
||||
cmd_data = {'rand': rand, 'autn': autn}
|
||||
@@ -604,7 +515,7 @@ class SimCardCommands:
|
||||
p2 = '81'
|
||||
elif context == 'gsm':
|
||||
p2 = '80'
|
||||
(data, sw) = self.send_apdu_constr_checksw(
|
||||
(data, sw) = self._tp.send_apdu_constr_checksw(
|
||||
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
|
||||
if 'auts' in data:
|
||||
ret = {'synchronisation_failure': data}
|
||||
@@ -614,11 +525,11 @@ class SimCardCommands:
|
||||
|
||||
def status(self) -> ResTuple:
|
||||
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
|
||||
return self.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
|
||||
|
||||
def deactivate_file(self) -> ResTuple:
|
||||
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
|
||||
return self.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
|
||||
return self._tp.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.
|
||||
@@ -626,31 +537,31 @@ class SimCardCommands:
|
||||
Args:
|
||||
fid : file identifier as hex string
|
||||
"""
|
||||
return self.send_apdu_checksw(self.cla_byte + '44000002' + fid)
|
||||
return self._tp.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.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
|
||||
return self._tp.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.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
|
||||
|
||||
def delete_file(self, fid: Hexstr) -> ResTuple:
|
||||
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
|
||||
return self.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
|
||||
return self._tp.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.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
|
||||
return self._tp.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.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
|
||||
return self._tp.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.send_apdu_checksw(self.cla_byte + 'fe000000')
|
||||
return self._tp.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.
|
||||
@@ -664,7 +575,7 @@ class SimCardCommands:
|
||||
else:
|
||||
p1 = 0x00
|
||||
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
|
||||
return self.send_apdu_checksw(pdu)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def reset_card(self) -> Hexstr:
|
||||
"""Physically reset the card"""
|
||||
@@ -674,7 +585,7 @@ 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])))
|
||||
if sw != '9000':
|
||||
elif (sw != '9000'):
|
||||
raise SwMatchError(sw, '9000')
|
||||
|
||||
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
|
||||
@@ -685,7 +596,8 @@ class SimCardCommands:
|
||||
code : chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
|
||||
self._chv_process_sw('verify', chv_no, code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -698,7 +610,8 @@ class SimCardCommands:
|
||||
pin_code : new chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
|
||||
self._chv_process_sw('unblock', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -711,7 +624,8 @@ 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.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
|
||||
self._chv_process_sw('change', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -724,7 +638,8 @@ class SimCardCommands:
|
||||
new_pin_code : new chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
|
||||
self._chv_process_sw('disable', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -736,7 +651,8 @@ class SimCardCommands:
|
||||
pin_code : chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
|
||||
self._chv_process_sw('enable', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -746,7 +662,7 @@ class SimCardCommands:
|
||||
Args:
|
||||
payload : payload as hex string
|
||||
"""
|
||||
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
|
||||
return self._tp.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
|
||||
|
||||
def terminal_profile(self, payload: Hexstr) -> ResTuple:
|
||||
"""Send TERMINAL PROFILE to card
|
||||
@@ -755,7 +671,7 @@ class SimCardCommands:
|
||||
payload : payload as hex string
|
||||
"""
|
||||
data_length = len(payload) // 2
|
||||
data, sw = self.send_apdu(('80100000%02x' % data_length) + payload)
|
||||
data, sw = self._tp.send_apdu(('80100000%02x' % data_length) + payload)
|
||||
return (data, sw)
|
||||
|
||||
# ETSI TS 102 221 11.1.22
|
||||
@@ -769,31 +685,34 @@ class SimCardCommands:
|
||||
def encode_duration(secs: int) -> Hexstr:
|
||||
if secs >= 10*24*60*60:
|
||||
return '04%02x' % (secs // (10*24*60*60))
|
||||
if secs >= 24*60*60:
|
||||
elif secs >= 24*60*60:
|
||||
return '03%02x' % (secs // (24*60*60))
|
||||
if secs >= 60*60:
|
||||
elif secs >= 60*60:
|
||||
return '02%02x' % (secs // (60*60))
|
||||
if secs >= 60:
|
||||
elif secs >= 60:
|
||||
return '01%02x' % (secs // 60)
|
||||
return '00%02x' % secs
|
||||
else:
|
||||
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
|
||||
if time_unit == '03':
|
||||
elif time_unit == '03':
|
||||
return length * 24*60*60
|
||||
if time_unit == '02':
|
||||
elif time_unit == '02':
|
||||
return length * 60*60
|
||||
if time_unit == '01':
|
||||
elif time_unit == '01':
|
||||
return length * 60
|
||||
if time_unit == '00':
|
||||
elif time_unit == '00':
|
||||
return length
|
||||
raise ValueError('Time unit must be 0x00..0x04')
|
||||
else:
|
||||
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.send_apdu_checksw('8076000004' + min_dur_enc + max_dur_enc)
|
||||
data, sw = self._tp.send_apdu_checksw(
|
||||
'8076000004' + min_dur_enc + max_dur_enc)
|
||||
negotiated_duration_secs = decode_duration(data[:4])
|
||||
resume_token = data[4:]
|
||||
return (negotiated_duration_secs, resume_token, sw)
|
||||
@@ -803,14 +722,14 @@ 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.send_apdu_checksw('8076010008' + token)
|
||||
data, sw = self._tp.send_apdu_checksw('8076010008' + token)
|
||||
return (data, sw)
|
||||
|
||||
def get_data(self, tag: int, cla: int = 0x00):
|
||||
data, sw = self.send_apdu('%02xca%04x00' % (cla, tag))
|
||||
data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
|
||||
return (data, sw)
|
||||
|
||||
# TS 31.102 Section 7.5.2
|
||||
def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
|
||||
data, sw = self.send_apdu_checksw('807800%02x00' % (context))
|
||||
data, sw = self._tp.send_apdu_checksw('807800%02x00' % (context))
|
||||
return (data, sw)
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
"""Utility code related to the integration of the 'construct' declarative parser."""
|
||||
|
||||
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
|
||||
|
||||
import gsm0338
|
||||
|
||||
from construct.lib.containers import Container, ListContainer
|
||||
from construct.core import EnumIntegerString
|
||||
from construct import Adapter, Prefixed, Int8ub, GreedyBytes, Default, Flag, Byte, Construct, Enum
|
||||
from construct import BitsInteger, BitStruct, Bytes, StreamError, stream_read_entire, stream_write
|
||||
from construct import SizeofError, IntegerError, swapbytes
|
||||
from construct.core import evaluate
|
||||
from construct.lib import integertypes
|
||||
|
||||
from pySim.utils import b2h, h2b, swap_nibbles
|
||||
"""Utility code related to the integration of the 'construct' declarative parser."""
|
||||
|
||||
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
@@ -47,7 +42,7 @@ class Utf8Adapter(Adapter):
|
||||
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 ""
|
||||
return codecs.decode(obj, "utf-8")
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
@@ -59,7 +54,7 @@ class GsmOrUcs2Adapter(Adapter):
|
||||
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 ""
|
||||
# one of the magic bytes of TS 102 221 Annex A
|
||||
if obj[0] in [0x80, 0x81, 0x82]:
|
||||
ad = Ucs2Adapter(GreedyBytes)
|
||||
@@ -82,7 +77,7 @@ class Ucs2Adapter(Adapter):
|
||||
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 ""
|
||||
if obj[0] == 0x80:
|
||||
# TS 102 221 Annex A Variant 1
|
||||
return codecs.decode(obj[1:], 'utf_16_be')
|
||||
@@ -180,7 +175,7 @@ class Ucs2Adapter(Adapter):
|
||||
|
||||
def _encode_variant1(instr: str) -> bytes:
|
||||
"""Encode according to TS 102 221 Annex A Variant 1"""
|
||||
return b'\x80' + codecs.encode(instr, 'utf_16_be')
|
||||
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"""
|
||||
@@ -199,7 +194,7 @@ class Ucs2Adapter(Adapter):
|
||||
assert codepoint_prefix == c_prefix
|
||||
enc = (0x80 + (c_codepoint & 0x7f)).to_bytes(1, byteorder='big')
|
||||
chars += enc
|
||||
if codepoint_prefix is None:
|
||||
if codepoint_prefix == None:
|
||||
codepoint_prefix = 0
|
||||
return hdr + codepoint_prefix.to_bytes(1, byteorder='big') + chars
|
||||
|
||||
@@ -269,9 +264,9 @@ class InvertAdapter(Adapter):
|
||||
# skip all private entries
|
||||
if k.startswith('_'):
|
||||
continue
|
||||
if v is False:
|
||||
if v == False:
|
||||
obj[k] = True
|
||||
elif v is True:
|
||||
elif v == True:
|
||||
obj[k] = False
|
||||
return obj
|
||||
|
||||
@@ -368,37 +363,6 @@ class Ipv6Adapter(Adapter):
|
||||
ia = ipaddress.IPv6Address(obj)
|
||||
return ia.packed
|
||||
|
||||
class StripTrailerAdapter(Adapter):
|
||||
"""
|
||||
Encoder removes all trailing bytes matching the default_value
|
||||
Decoder pads input data up to total_length with default_value
|
||||
|
||||
This is used in constellations like "FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), ..."
|
||||
where you have a bit-mask that may have 1, 2 or 3 bytes, depending on whether or not any
|
||||
of the LSBs are actually set.
|
||||
"""
|
||||
def __init__(self, subcon, total_length:int, default_value=b'\x00', min_len=1):
|
||||
super().__init__(subcon)
|
||||
assert len(default_value) == 1
|
||||
self.total_length = total_length
|
||||
self.default_value = default_value
|
||||
self.min_len = min_len
|
||||
|
||||
def _decode(self, obj, context, path):
|
||||
assert isinstance(obj, bytes)
|
||||
# pad with suppressed/missing bytes
|
||||
if len(obj) < self.total_length:
|
||||
obj += self.default_value * (self.total_length - len(obj))
|
||||
return int.from_bytes(obj, 'big')
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
assert isinstance(obj, int)
|
||||
obj = obj.to_bytes(self.total_length, 'big')
|
||||
# remove trailing bytes if they are zero
|
||||
while len(obj) > self.min_len and obj[-1] == self.default_value[0]:
|
||||
obj = obj[:-1]
|
||||
return obj
|
||||
|
||||
|
||||
def filter_dict(d, exclude_prefix='_'):
|
||||
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
|
||||
@@ -408,20 +372,20 @@ def filter_dict(d, exclude_prefix='_'):
|
||||
for (key, value) in d.items():
|
||||
if key.startswith(exclude_prefix):
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
if type(value) is dict:
|
||||
res[key] = filter_dict(value)
|
||||
else:
|
||||
res[key] = value
|
||||
return res
|
||||
|
||||
|
||||
def normalize_construct(c, exclude_prefix: str = '_'):
|
||||
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, exclude_prefix)
|
||||
if isinstance(c, (Container, dict)):
|
||||
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]
|
||||
@@ -444,11 +408,11 @@ def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None,
|
||||
# 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):
|
||||
if all([v == 0xff for v in raw_bin_data]):
|
||||
return None
|
||||
else:
|
||||
raise e
|
||||
return normalize_construct(parsed, exclude_prefix)
|
||||
return normalize_construct(parsed)
|
||||
|
||||
def build_construct(c, decoded_data, context: dict = {}):
|
||||
"""Helper function to handle total_len."""
|
||||
@@ -561,7 +525,8 @@ class GreedyInteger(Construct):
|
||||
|
||||
# round up to the minimum number
|
||||
# of bytes we anticipate
|
||||
nbytes = max(nbytes, minlen)
|
||||
if nbytes < minlen:
|
||||
nbytes = minlen
|
||||
|
||||
return nbytes
|
||||
|
||||
@@ -572,7 +537,7 @@ class GreedyInteger(Construct):
|
||||
try:
|
||||
data = obj.to_bytes(length, byteorder='big', signed=self.signed)
|
||||
except ValueError as e:
|
||||
raise IntegerError(str(e), path=path) from e
|
||||
raise IntegerError(str(e), path=path)
|
||||
if evaluate(self.swapped, context):
|
||||
data = swapbytes(data)
|
||||
stream_write(stream, data, length, path)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import sys
|
||||
from typing import Optional
|
||||
from importlib import resources
|
||||
|
||||
import asn1tools
|
||||
|
||||
def compile_asn1_subdir(subdir_name:str):
|
||||
"""Helper function that compiles ASN.1 syntax from all files within given subdir"""
|
||||
import asn1tools
|
||||
asn_txt = ''
|
||||
__ver = sys.version_info
|
||||
if (__ver.major, __ver.minor) >= (3, 9):
|
||||
@@ -15,79 +14,3 @@ def compile_asn1_subdir(subdir_name:str):
|
||||
#else:
|
||||
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
|
||||
return asn1tools.compile_string(asn_txt, codec='der')
|
||||
|
||||
|
||||
# SGP.22 section 4.1 Activation Code
|
||||
class ActivationCode:
|
||||
def __init__(self, hostname:str, token:str, oid: Optional[str] = None, cc_required: Optional[bool] = False):
|
||||
if '$' in hostname:
|
||||
raise ValueError('$ sign not permitted in hostname')
|
||||
self.hostname = hostname
|
||||
if '$' in token:
|
||||
raise ValueError('$ sign not permitted in token')
|
||||
self.token = token
|
||||
# TODO: validate OID
|
||||
self.oid = oid
|
||||
self.cc_required = cc_required
|
||||
# only format 1 is specified and supported here
|
||||
self.format = 1
|
||||
|
||||
@staticmethod
|
||||
def decode_str(ac: str) -> dict:
|
||||
if ac[0] != '1':
|
||||
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
|
||||
ac_elements = ac.split('$')
|
||||
d = {
|
||||
'oid': None,
|
||||
'cc_required': False,
|
||||
}
|
||||
d['format'] = ac_elements.pop(0)
|
||||
d['hostname'] = ac_elements.pop(0)
|
||||
d['token'] = ac_elements.pop(0)
|
||||
if len(ac_elements):
|
||||
oid = ac_elements.pop(0)
|
||||
if oid != '':
|
||||
d['oid'] = oid
|
||||
if len(ac_elements):
|
||||
ccr = ac_elements.pop(0)
|
||||
if ccr == '1':
|
||||
d['cc_required'] = True
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, ac: str) -> 'ActivationCode':
|
||||
"""Create new instance from SGP.22 section 4.1 string representation."""
|
||||
d = cls.decode_str(ac)
|
||||
return cls(d['hostname'], d['token'], d['oid'], d['cc_required'])
|
||||
|
||||
def to_string(self, for_qrcode:bool = False) -> str:
|
||||
"""Convert from internal representation to SGP.22 section 4.1 string representation."""
|
||||
if for_qrcode:
|
||||
ret = 'LPA:'
|
||||
else:
|
||||
ret = ''
|
||||
ret += '%d$%s$%s' % (self.format, self.hostname, self.token)
|
||||
if self.oid:
|
||||
ret += '$%s' % (self.oid)
|
||||
elif self.cc_required:
|
||||
ret += '$'
|
||||
if self.cc_required:
|
||||
ret += '$1'
|
||||
return ret
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
def to_qrcode(self):
|
||||
"""Encode internal representation to QR code."""
|
||||
import qrcode
|
||||
qr = qrcode.QRCode()
|
||||
qr.add_data(self.to_string(for_qrcode=True))
|
||||
return qr.make_image()
|
||||
|
||||
def __repr__(self):
|
||||
return "ActivationCode(format=%u, hostname='%s', token='%s', oid=%s, cc_required=%s)" % (self.format,
|
||||
self.hostname,
|
||||
self.token,
|
||||
self.oid,
|
||||
self.cc_required)
|
||||
|
||||
@@ -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,
|
||||
keyComponents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||
keyCompontents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||
keyType [0] OCTET STRING,
|
||||
keyData [6] OCTET STRING,
|
||||
macLength[7] UInt8 DEFAULT 8
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
# 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
|
||||
@@ -41,8 +42,6 @@ from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
MAX_SEGMENT_SIZE = 1020
|
||||
|
||||
class BspAlgo(abc.ABC):
|
||||
blocksize: int
|
||||
|
||||
@@ -51,11 +50,11 @@ class BspAlgo(abc.ABC):
|
||||
if in_len % multiple == 0:
|
||||
return b''
|
||||
pad_cnt = multiple - (in_len % multiple)
|
||||
return bytes([padding]) * pad_cnt
|
||||
return b'\x00' * pad_cnt
|
||||
|
||||
def _pad_to_multiple(self, indat: bytes, multiple: int, padding: int = 0) -> bytes:
|
||||
"""Pad the input data to multiples of 'multiple'."""
|
||||
return indat + self._get_padding(len(indat), multiple, padding)
|
||||
return indat + self._get_padding(len(indat), self.blocksize, padding)
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
@@ -82,14 +81,17 @@ 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'
|
||||
@@ -164,6 +166,7 @@ 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'
|
||||
@@ -286,7 +289,7 @@ 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)
|
||||
return val
|
||||
|
||||
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
|
||||
|
||||
@@ -1,458 +0,0 @@
|
||||
"""GSMA eSIM RSP ES2+ interface according to SGP.22 v2.5"""
|
||||
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
import time
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class ApiParam(abc.ABC):
|
||||
"""A class reprsenting a single parameter in the ES2+ API."""
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
"""Verify the decoded reprsentation of a value. Should raise an exception if somthing is odd."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
"""Verify the encoded reprsentation of a value. Should raise an exception if somthing is odd."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def encode(cls, data):
|
||||
"""[Validate and] Encode the given value."""
|
||||
cls.verify_decoded(data)
|
||||
encoded = cls._encode(data)
|
||||
cls.verify_decoded(encoded)
|
||||
return encoded
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
"""encoder function, typically [but not always] overridden by derived class."""
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def decode(cls, data):
|
||||
"""[Validate and] Decode the given value."""
|
||||
cls.verify_encoded(data)
|
||||
decoded = cls._decode(data)
|
||||
cls.verify_decoded(decoded)
|
||||
return decoded
|
||||
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
"""decoder function, typically [but not always] overridden by derived class."""
|
||||
return data
|
||||
|
||||
class ApiParamString(ApiParam):
|
||||
"""Base class representing an API parameter of 'string' type."""
|
||||
pass
|
||||
|
||||
|
||||
class ApiParamInteger(ApiParam):
|
||||
"""Base class representing an API parameter of 'integer' type."""
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
return int(data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
return str(data)
|
||||
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
if not isinstance(data, int):
|
||||
raise TypeError('Expected an integer input data type')
|
||||
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
if not data.isdecimal():
|
||||
raise ValueError('integer (%s) contains non-decimal characters' % data)
|
||||
assert str(int(data)) == data
|
||||
|
||||
class ApiParamBoolean(ApiParam):
|
||||
"""Base class representing an API parameter of 'boolean' type."""
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
return bool(data)
|
||||
|
||||
class ApiParamFqdn(ApiParam):
|
||||
"""String, as a list of domain labels concatenated using the full stop (dot, period) character as
|
||||
separator between labels. Labels are restricted to the Alphanumeric mode character set defined in table 5
|
||||
of ISO/IEC 18004"""
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
# FIXME
|
||||
pass
|
||||
|
||||
class param:
|
||||
class Iccid(ApiParamString):
|
||||
"""String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
|
||||
character F."""
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
data = str(data)
|
||||
# SGP.22 version prior to 2.2 do not require support for 19-digit ICCIDs, so let's always
|
||||
# encode it with padding F at the end.
|
||||
if len(data) == 19:
|
||||
data += 'F'
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
if len(data) not in [19, 20]:
|
||||
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
|
||||
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
# strip trailing padding (if it's 20 digits)
|
||||
if len(data) == 20 and data[-1] in ['F', 'f']:
|
||||
data = data[:-1]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
data = str(data)
|
||||
if len(data) not in [19, 20]:
|
||||
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
|
||||
if len(data) == 19:
|
||||
decimal_part = data
|
||||
else:
|
||||
decimal_part = data[:-1]
|
||||
final_part = data[-1:]
|
||||
if final_part not in ['F', 'f'] and not final_part.isdecimal():
|
||||
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
|
||||
if not decimal_part.isdecimal():
|
||||
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
|
||||
|
||||
|
||||
class Eid(ApiParamString):
|
||||
"""String of 32 decimal characters"""
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
if len(data) != 32:
|
||||
raise ValueError('EID length invalid: "%s" (%u)' % (data, len(data)))
|
||||
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
if not data.isdecimal():
|
||||
raise ValueError('EID (%s) contains non-decimal characters' % data)
|
||||
|
||||
class ProfileType(ApiParamString):
|
||||
pass
|
||||
|
||||
class MatchingId(ApiParamString):
|
||||
pass
|
||||
|
||||
class ConfirmationCode(ApiParamString):
|
||||
pass
|
||||
|
||||
class SmdsAddress(ApiParamFqdn):
|
||||
pass
|
||||
|
||||
class SmdpAddress(ApiParamFqdn):
|
||||
pass
|
||||
|
||||
class ReleaseFlag(ApiParamBoolean):
|
||||
pass
|
||||
|
||||
class FinalProfileStatusIndicator(ApiParamString):
|
||||
pass
|
||||
|
||||
class Timestamp(ApiParamString):
|
||||
"""String format as specified by W3C: YYYY-MM-DDThh:mm:ssTZD"""
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
return datetime.fromisoformat(data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
return datetime.toisoformat(data)
|
||||
|
||||
class NotificationPointId(ApiParamInteger):
|
||||
pass
|
||||
|
||||
class NotificationPointStatus(ApiParam):
|
||||
pass
|
||||
|
||||
class ResultData(ApiParam):
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
return base64.b64decode(data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
return base64.b64encode(data)
|
||||
|
||||
class JsonResponseHeader(ApiParam):
|
||||
"""SGP.22 section 6.5.1.4."""
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
fe_status = data.get('functionExecutionStatus')
|
||||
if not fe_status:
|
||||
raise ValueError('Missing mandatory functionExecutionStatus in header')
|
||||
status = fe_status.get('status')
|
||||
if not status:
|
||||
raise ValueError('Missing mandatory status in header functionExecutionStatus')
|
||||
if status not in ['Executed-Success', 'Executed-WithWarning', 'Failed', 'Expired']:
|
||||
raise ValueError('Unknown/unspecified status "%s"' % status)
|
||||
|
||||
|
||||
class HttpStatusError(Exception):
|
||||
pass
|
||||
|
||||
class HttpHeaderError(Exception):
|
||||
pass
|
||||
|
||||
class Es2PlusApiError(Exception):
|
||||
"""Exception representing an error at the ES2+ API level (status != Executed)."""
|
||||
def __init__(self, func_ex_status: dict):
|
||||
self.status = func_ex_status['status']
|
||||
sec = {
|
||||
'subjectCode': None,
|
||||
'reasonCode': None,
|
||||
'subjectIdentifier': None,
|
||||
'message': None,
|
||||
}
|
||||
actual_sec = func_ex_status.get('statusCodeData', None)
|
||||
sec.update(actual_sec)
|
||||
self.subject_code = sec['subjectCode']
|
||||
self.reason_code = sec['reasonCode']
|
||||
self.subject_id = sec['subjectIdentifier']
|
||||
self.message = sec['message']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.status}("{self.subject_code}","{self.reason_code}","{self.subject_id}","{self.message}")'
|
||||
|
||||
class Es2PlusApiFunction(abc.ABC):
|
||||
"""Base classs for representing an ES2+ API Function."""
|
||||
# the below class variables are expected to be overridden in derived classes
|
||||
|
||||
path = None
|
||||
# dictionary of input parameters. key is parameter name, value is ApiParam class
|
||||
input_params = {}
|
||||
# list of mandatory input parameters
|
||||
input_mandatory = []
|
||||
# dictionary of output parameters. key is parameter name, value is ApiParam class
|
||||
output_params = {}
|
||||
# list of mandatory output parameters (for successful response)
|
||||
output_mandatory = []
|
||||
# expected HTTP status code of the response
|
||||
expected_http_status = 200
|
||||
|
||||
def __init__(self, url_prefix: str, func_req_id: str, session):
|
||||
self.url_prefix = url_prefix
|
||||
self.func_req_id = func_req_id
|
||||
self.session = session
|
||||
|
||||
def encode(self, data: dict, func_call_id: str) -> dict:
|
||||
"""Validate an encode input dict into JSON-serializable dict for request body."""
|
||||
output = {
|
||||
'header': {
|
||||
'functionRequesterIdentifier': self.func_req_id,
|
||||
'functionCallIdentifier': func_call_id
|
||||
}
|
||||
}
|
||||
for p in self.input_mandatory:
|
||||
if not p in data:
|
||||
raise ValueError('Mandatory input parameter %s missing' % p)
|
||||
for p, v in data.items():
|
||||
p_class = self.input_params.get(p)
|
||||
if not p_class:
|
||||
logger.warning('Unexpected/unsupported input parameter %s=%s', p, v)
|
||||
output[p] = v
|
||||
else:
|
||||
output[p] = p_class.encode(v)
|
||||
return output
|
||||
|
||||
|
||||
def decode(self, data: dict) -> dict:
|
||||
"""[further] Decode and validate the JSON-Dict of the respnse body."""
|
||||
output = {}
|
||||
# let's first do the header, it's special
|
||||
if not 'header' in data:
|
||||
raise ValueError('Mandatory output parameter "header" missing')
|
||||
hdr_class = self.output_params.get('header')
|
||||
output['header'] = hdr_class.decode(data['header'])
|
||||
|
||||
if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
|
||||
raise Es2PlusApiError(output['header']['functionExecutionStatus'])
|
||||
# we can only expect mandatory parameters to be present in case of successful execution
|
||||
for p in self.output_mandatory:
|
||||
if p == 'header':
|
||||
continue
|
||||
if not p in data:
|
||||
raise ValueError('Mandatory output parameter "%s" missing' % p)
|
||||
for p, v in data.items():
|
||||
p_class = self.output_params.get(p)
|
||||
if not p_class:
|
||||
logger.warning('Unexpected/unsupported output parameter "%s"="%s"', p, v)
|
||||
output[p] = v
|
||||
else:
|
||||
output[p] = p_class.decode(v)
|
||||
return output
|
||||
|
||||
def call(self, data: dict, func_call_id:str, timeout=10) -> dict:
|
||||
"""Make an API call to the ES2+ API endpoint represented by this object.
|
||||
Input data is passed in `data` as json-serializable dict. Output data
|
||||
is returned as json-deserialized dict."""
|
||||
url = self.url_prefix + self.path
|
||||
encoded = json.dumps(self.encode(data, func_call_id))
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
|
||||
}
|
||||
|
||||
logger.debug("HTTP REQ %s - '%s'" % (url, encoded))
|
||||
response = self.session.post(url, data=encoded, headers=headers, timeout=timeout)
|
||||
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
|
||||
logger.debug("HTTP RSP: %s" % (response.content))
|
||||
|
||||
if response.status_code != self.expected_http_status:
|
||||
raise HttpStatusError(response)
|
||||
if not response.headers.get('Content-Type').startswith(headers['Content-Type']):
|
||||
raise HttpHeaderError(response)
|
||||
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
|
||||
raise HttpHeaderError(response)
|
||||
|
||||
return self.decode(response.json())
|
||||
|
||||
|
||||
# ES2+ DownloadOrder function (SGP.22 section 5.3.1)
|
||||
class DownloadOrder(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/downloadOrder'
|
||||
input_params = {
|
||||
'eid': param.Eid,
|
||||
'iccid': param.Iccid,
|
||||
'profileType': param.ProfileType
|
||||
}
|
||||
output_params = {
|
||||
'header': param.JsonResponseHeader,
|
||||
'iccid': param.Iccid,
|
||||
}
|
||||
output_mandatory = ['header', 'iccid']
|
||||
|
||||
# ES2+ ConfirmOrder function (SGP.22 section 5.3.2)
|
||||
class ConfirmOrder(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/confirmOrder'
|
||||
input_params = {
|
||||
'iccid': param.Iccid,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'confirmationCode': param.ConfirmationCode,
|
||||
'smdsAddress': param.SmdsAddress,
|
||||
'releaseFlag': param.ReleaseFlag,
|
||||
}
|
||||
input_mandatory = ['iccid', 'releaseFlag']
|
||||
output_params = {
|
||||
'header': param.JsonResponseHeader,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'smdpAddress': param.SmdpAddress,
|
||||
}
|
||||
output_mandatory = ['header', 'matchingId']
|
||||
|
||||
# ES2+ CancelOrder function (SGP.22 section 5.3.3)
|
||||
class CancelOrder(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/cancelOrder'
|
||||
input_params = {
|
||||
'iccid': param.Iccid,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'finalProfileStatusIndicator': param.FinalProfileStatusIndicator,
|
||||
}
|
||||
input_mandatory = ['finalProfileStatusIndicator', 'iccid']
|
||||
output_params = {
|
||||
'header': param.JsonResponseHeader,
|
||||
}
|
||||
output_mandatory = ['header']
|
||||
|
||||
# ES2+ ReleaseProfile function (SGP.22 section 5.3.4)
|
||||
class ReleaseProfile(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/releaseProfile'
|
||||
input_params = {
|
||||
'iccid': param.Iccid,
|
||||
}
|
||||
input_mandatory = ['iccid']
|
||||
output_params = {
|
||||
'header': param.JsonResponseHeader,
|
||||
}
|
||||
output_mandatory = ['header']
|
||||
|
||||
# ES2+ HandleDownloadProgress function (SGP.22 section 5.3.5)
|
||||
class HandleDownloadProgressInfo(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/handleDownloadProgressInfo'
|
||||
input_params = {
|
||||
'eid': param.Eid,
|
||||
'iccid': param.Iccid,
|
||||
'profileType': param.ProfileType,
|
||||
'timestamp': param.Timestamp,
|
||||
'notificationPointId': param.NotificationPointId,
|
||||
'notificationPointStatus': param.NotificationPointStatus,
|
||||
'resultData': param.ResultData,
|
||||
}
|
||||
input_mandatory = ['iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
|
||||
expected_http_status = 204
|
||||
|
||||
|
||||
class Es2pApiClient:
|
||||
"""Main class representing a full ES2+ API client. Has one method for each API function."""
|
||||
def __init__(self, url_prefix:str, func_req_id:str, server_cert_verify: str = None, client_cert: str = None):
|
||||
self.func_id = 0
|
||||
self.session = requests.Session()
|
||||
if server_cert_verify:
|
||||
self.session.verify = server_cert_verify
|
||||
if client_cert:
|
||||
self.session.cert = client_cert
|
||||
|
||||
self.downloadOrder = DownloadOrder(url_prefix, func_req_id, self.session)
|
||||
self.confirmOrder = ConfirmOrder(url_prefix, func_req_id, self.session)
|
||||
self.cancelOrder = CancelOrder(url_prefix, func_req_id, self.session)
|
||||
self.releaseProfile = ReleaseProfile(url_prefix, func_req_id, self.session)
|
||||
self.handleDownloadProgressInfo = HandleDownloadProgressInfo(url_prefix, func_req_id, self.session)
|
||||
|
||||
def _gen_func_id(self) -> str:
|
||||
"""Generate the next function call id."""
|
||||
self.func_id += 1
|
||||
return 'FCI-%u-%u' % (time.time(), self.func_id)
|
||||
|
||||
|
||||
def call_downloadOrder(self, data: dict) -> dict:
|
||||
"""Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
|
||||
return self.downloadOrder.call(data, self._gen_func_id())
|
||||
|
||||
def call_confirmOrder(self, data: dict) -> dict:
|
||||
"""Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
|
||||
return self.confirmOrder.call(data, self._gen_func_id())
|
||||
|
||||
def call_cancelOrder(self, data: dict) -> dict:
|
||||
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
|
||||
return self.cancelOrder.call(data, self._gen_func_id())
|
||||
|
||||
def call_releaseProfile(self, data: dict) -> dict:
|
||||
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
|
||||
return self.releaseProfile.call(data, self._gen_func_id())
|
||||
|
||||
def call_handleDownloadProgressInfo(self, data: dict) -> dict:
|
||||
"""Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
|
||||
return self.handleDownloadProgressInfo.call(data, self._gen_func_id())
|
||||
@@ -19,10 +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 pySim.esim import compile_asn1_subdir
|
||||
|
||||
@@ -33,11 +35,10 @@ 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, ci_cert_id: bytes):
|
||||
def __init__(self, transactionId: str, serverChallenge: bytes):
|
||||
self.transactionId = transactionId
|
||||
self.serverChallenge = serverChallenge
|
||||
# used at a later point between API calsl
|
||||
self.ci_cert_id = ci_cert_id
|
||||
self.euicc_cert: Optional[x509.Certificate] = None
|
||||
self.eum_cert: Optional[x509.Certificate] = None
|
||||
self.eid: Optional[bytes] = None
|
||||
@@ -96,3 +97,4 @@ 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
|
||||
|
||||
@@ -16,125 +16,74 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
import io
|
||||
from typing import Tuple, List, Optional, Dict, Union
|
||||
from typing import Tuple, List, Optional, Dict
|
||||
|
||||
import asn1tools
|
||||
|
||||
from pySim.utils import bertlv_parse_tag, bertlv_parse_len
|
||||
from pySim.ts_102_221 import FileDescriptor
|
||||
from pySim.construct import build_construct
|
||||
from pySim.esim import compile_asn1_subdir
|
||||
from pySim.esim.saip import templates
|
||||
|
||||
asn1 = compile_asn1_subdir('saip')
|
||||
|
||||
class File:
|
||||
"""Internal representation of a file in a profile filesystem.
|
||||
class oid:
|
||||
class OID:
|
||||
@staticmethod
|
||||
def intlist_from_str(instr: str) -> List[int]:
|
||||
return [int(x) for x in instr.split('.')]
|
||||
|
||||
Parameters:
|
||||
pename: Name string of the profile element
|
||||
l: List of tuples [fileDescriptor, fillFileContent, fillFileOffset profile elements]
|
||||
template: Applicable FileTemplate describing defaults as per SAIP spec
|
||||
"""
|
||||
def __init__(self, pename: str, l: Optional[List[Tuple]] = None, template: Optional[templates.FileTemplate] = None):
|
||||
self.pe_name = pename
|
||||
self.template = template
|
||||
self.fileDescriptor = {}
|
||||
self.stream = None
|
||||
# apply some defaults from profile
|
||||
if self.template:
|
||||
self.from_template(self.template)
|
||||
print("after template: %s" % repr(self))
|
||||
if l:
|
||||
self.from_tuples(l)
|
||||
|
||||
def from_template(self, template: templates.FileTemplate):
|
||||
"""Determine defaults for file based on given FileTemplate."""
|
||||
fdb_dec = {}
|
||||
self.rec_len = None
|
||||
if template.fid:
|
||||
self.fileDescriptor['fileID'] = template.fid.to_bytes(2, 'big')
|
||||
if template.sfi:
|
||||
self.fileDescriptor['shortEFID'] = bytes([template.sfi])
|
||||
if template.arr:
|
||||
self.fileDescriptor['securityAttributesReferenced'] = bytes([template.arr])
|
||||
# All the files defined in the templates shall have, by default, shareable/not-shareable bit in the file descriptor set to "shareable".
|
||||
fdb_dec['shareable'] = True
|
||||
if template.file_type in ['LF', 'CY']:
|
||||
fdb_dec['file_type'] = 'working_ef'
|
||||
if template.rec_len:
|
||||
self.record_len = template.rec_len
|
||||
if template.nb_rec and template.rec_len:
|
||||
self.fileDescriptor['efFileSize'] = (template.nb_rec * template.rec_len).to_bytes(2, 'big') # FIXME
|
||||
if template.file_type == 'LF':
|
||||
fdb_dec['structure'] = 'linear_fixed'
|
||||
elif template.file_type == 'CY':
|
||||
fdb_dec['structure'] = 'cyclic'
|
||||
elif template.file_type in ['TR', 'BT']:
|
||||
fdb_dec['file_type'] = 'working_ef'
|
||||
if template.file_size:
|
||||
self.fileDescriptor['efFileSize'] = template.file_size.to_bytes(2, 'big') # FIXME
|
||||
if template.file_type == 'BT':
|
||||
fdb_dec['structure'] = 'ber_tlv'
|
||||
elif template.file_type == 'TR':
|
||||
fdb_dec['structure'] = 'transparent'
|
||||
elif template.file_type in ['MF', 'DF', 'ADF']:
|
||||
fdb_dec['file_type'] = 'df'
|
||||
fdb_dec['structure'] = 'no_info_given'
|
||||
# build file descriptor based on above input data
|
||||
fd_dict = {'file_descriptor_byte': fdb_dec}
|
||||
if self.rec_len:
|
||||
fd_dict['record_len'] = self.rec_len
|
||||
self.fileDescriptor['fileDescriptor'] = build_construct(FileDescriptor._construct, fd_dict)
|
||||
# FIXME: default_val
|
||||
# FIXME: high_update
|
||||
# FIXME: params?
|
||||
|
||||
def from_tuples(self, l:List[Tuple]):
|
||||
"""Parse a list of fileDescriptor, fillFileContent, fillFileOffset tuples into this instance."""
|
||||
def get_fileDescriptor(l:List[Tuple]):
|
||||
for k, v in l:
|
||||
if k == 'fileDescriptor':
|
||||
return v
|
||||
fd = get_fileDescriptor(l)
|
||||
if not fd:
|
||||
raise ValueError("No fileDescriptor found")
|
||||
self.fileDescriptor.update(dict(fd))
|
||||
self.stream = self.linearize_file_content(l)
|
||||
|
||||
def to_tuples(self) -> List[Tuple]:
|
||||
"""Generate a list of fileDescriptor, fillFileContent, fillFileOffset tuples into this instance."""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def linearize_file_content(l: List[Tuple]) -> Optional[io.BytesIO]:
|
||||
"""linearize a list of fillFileContent / fillFileOffset tuples into a stream of bytes."""
|
||||
stream = io.BytesIO()
|
||||
for k, v in l:
|
||||
if k == 'doNotCreate':
|
||||
return None
|
||||
if k == 'fileDescriptor':
|
||||
pass
|
||||
elif k == 'fillFileOffset':
|
||||
stream.write(b'\xff' * v)
|
||||
elif k == 'fillFileContent':
|
||||
stream.write(v)
|
||||
def __init__(self, initializer):
|
||||
if type(initializer) == str:
|
||||
self.intlist = self.intlist_from_str(initializer)
|
||||
else:
|
||||
return ValueError("Unknown key '%s' in tuple list" % k)
|
||||
return stream
|
||||
self.intlist = initializer
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "File(%s)" % self.pe_name
|
||||
def __str__(self):
|
||||
return '.'.join([str(x) for x in self.intlist])
|
||||
|
||||
def __repr__(self):
|
||||
return 'OID(%s)' % (str(self))
|
||||
|
||||
|
||||
class eOID(OID):
|
||||
"""OID helper for TCA eUICC prefix"""
|
||||
__prefix = [2,23,143,1]
|
||||
def __init__(self, initializer):
|
||||
if type(initializer) == str:
|
||||
initializer = self.intlist_from_str(initializer)
|
||||
super().__init__(self.__prefix + initializer)
|
||||
|
||||
MF = eOID("2.1")
|
||||
DF_CD = eOID("2.2")
|
||||
DF_TELECOM = eOID("2.3")
|
||||
DF_TELECOM_v2 = eOID("2.3.2")
|
||||
ADF_USIM_by_default = eOID("2.4")
|
||||
ADF_USIM_by_default_v2 = eOID("2.4.2")
|
||||
ADF_USIM_not_by_default = eOID("2.5")
|
||||
ADF_USIM_not_by_default_v2 = eOID("2.5.2")
|
||||
ADF_USIM_not_by_default_v3 = eOID("2.5.3")
|
||||
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
|
||||
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
|
||||
ADF_ISIM_by_default = eOID("2.8")
|
||||
ADF_ISIM_not_by_default = eOID("2.9")
|
||||
ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
|
||||
ADF_CSIM_by_default = eOID("2.10")
|
||||
ADF_CSIM_by_default_v2 = eOID("2.10.2")
|
||||
ADF_CSIM_not_by_default = eOID("2.11")
|
||||
ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
|
||||
DF_EAP = eOID("2.12")
|
||||
DF_5GS = eOID("2.13")
|
||||
DF_5GS_v2 = eOID("2.13.2")
|
||||
DF_5GS_v3 = eOID("2.13.3")
|
||||
DF_5GS_v4 = eOID("2.13.4")
|
||||
DF_SAIP = eOID("2.14")
|
||||
DF_SNPN = eOID("2.15")
|
||||
DF_5GProSe = eOID("2.16")
|
||||
IoT_default = eOID("2.17")
|
||||
IoT_default = eOID("2.18")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "File(%s): %s" % (self.pe_name, self.fileDescriptor)
|
||||
|
||||
class ProfileElement:
|
||||
"""Class representing a Profile Element (PE) within a SAIP Profile."""
|
||||
FILE_BEARING = ['mf', 'cd', 'telecom', 'usim', 'opt-usim', 'isim', 'opt-isim', 'phonebook', 'gsm-access',
|
||||
'csim', 'opt-csim', 'eap', 'df-5gs', 'df-saip', 'df-snpn', 'df-5gprose', 'iot', 'opt-iot']
|
||||
def _fixup_sqnInit_dec(self) -> None:
|
||||
def _fixup_sqnInit_dec(self):
|
||||
"""asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
|
||||
this."""
|
||||
if self.type != 'akaParameter':
|
||||
@@ -147,7 +96,7 @@ class ProfileElement:
|
||||
# SEQUENCE (SIZE (32)) OF OCTET STRING (SIZE (6))
|
||||
self.decoded['sqnInit'] = [b'\x00'*6] * 32
|
||||
|
||||
def _fixup_sqnInit_enc(self) -> None:
|
||||
def _fixup_sqnInit_enc(self):
|
||||
"""asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
|
||||
this."""
|
||||
if self.type != 'akaParameter':
|
||||
@@ -161,37 +110,12 @@ class ProfileElement:
|
||||
# none of the fields were initialized with a non-default (non-zero) value, so we can skip it
|
||||
del self.decoded['sqnInit']
|
||||
|
||||
def parse_der(self, der: bytes) -> None:
|
||||
def parse_der(self, der: bytes):
|
||||
"""Parse a sequence of PE and store the result in instance attributes."""
|
||||
self.type, self.decoded = asn1.decode('ProfileElement', der)
|
||||
# work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
|
||||
self._fixup_sqnInit_dec()
|
||||
|
||||
@property
|
||||
def header_name(self) -> str:
|
||||
"""Return the name of the header field within the profile element."""
|
||||
# unneccessarry compliaction by inconsistent naming :(
|
||||
if self.type.startswith('opt-'):
|
||||
return self.type.replace('-','') + '-header'
|
||||
return self.type + '-header'
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
"""Return the decoded ProfileHeader."""
|
||||
return self.decoded.get(self.header_name, None)
|
||||
|
||||
@property
|
||||
def templateID(self):
|
||||
"""Return the decoded templateID used by this profile element (if any)."""
|
||||
return self.decoded.get('templateID', None)
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
"""Return dict of decoded 'File' ASN.1 items."""
|
||||
if not self.type in self.FILE_BEARING:
|
||||
return {}
|
||||
return {k:v for (k,v) in self.decoded.items() if k not in ['templateID', self.header_name]}
|
||||
|
||||
@classmethod
|
||||
def from_der(cls, der: bytes) -> 'ProfileElement':
|
||||
"""Construct an instance from given raw, DER encoded bytes."""
|
||||
@@ -205,14 +129,14 @@ class ProfileElement:
|
||||
self._fixup_sqnInit_enc()
|
||||
return asn1.encode('ProfileElement', (self.type, self.decoded))
|
||||
|
||||
def __str__(self) -> str:
|
||||
def __str__(self):
|
||||
return self.type
|
||||
|
||||
|
||||
def bertlv_first_segment(binary: bytes) -> Tuple[bytes, bytes]:
|
||||
"""obtain the first segment of a binary concatenation of BER-TLV objects.
|
||||
Returns: tuple of first TLV and remainder."""
|
||||
_tagdict, remainder = bertlv_parse_tag(binary)
|
||||
tagdict, remainder = bertlv_parse_tag(binary)
|
||||
length, remainder = bertlv_parse_len(remainder)
|
||||
tl_length = len(binary) - len(remainder)
|
||||
tlv_length = tl_length + length
|
||||
@@ -226,19 +150,16 @@ class ProfileElementSequence:
|
||||
self.pes_by_naa: Dict = {}
|
||||
|
||||
def get_pes_for_type(self, tname: str) -> List[ProfileElement]:
|
||||
"""Return list of profile elements present for given profile element type."""
|
||||
return self.pe_by_type.get(tname, [])
|
||||
|
||||
def get_pe_for_type(self, tname: str) -> Optional[ProfileElement]:
|
||||
"""Return a single profile element for given profile element type. Works only for
|
||||
types of which there is only a signle instance in the PE Sequence!"""
|
||||
l = self.get_pes_for_type(tname)
|
||||
if len(l) == 0:
|
||||
return None
|
||||
assert len(l) == 1
|
||||
return l[0]
|
||||
|
||||
def parse_der(self, der: bytes) -> None:
|
||||
def parse_der(self, der: bytes):
|
||||
"""Parse a sequence of PE and store the result in self.pe_list."""
|
||||
self.pe_list = []
|
||||
remainder = der
|
||||
@@ -247,11 +168,11 @@ class ProfileElementSequence:
|
||||
self.pe_list.append(ProfileElement.from_der(first_tlv))
|
||||
self._process_pelist()
|
||||
|
||||
def _process_pelist(self) -> None:
|
||||
def _process_pelist(self):
|
||||
self._rebuild_pe_by_type()
|
||||
self._rebuild_pes_by_naa()
|
||||
|
||||
def _rebuild_pe_by_type(self) -> None:
|
||||
def _rebuild_pe_by_type(self):
|
||||
self.pe_by_type = {}
|
||||
# build a dict {pe_type: [pe, pe, pe]}
|
||||
for pe in self.pe_list:
|
||||
@@ -260,7 +181,7 @@ class ProfileElementSequence:
|
||||
else:
|
||||
self.pe_by_type[pe.type] = [pe]
|
||||
|
||||
def _rebuild_pes_by_naa(self) -> None:
|
||||
def _rebuild_pes_by_naa(self):
|
||||
"""rebuild the self.pes_by_naa dict {naa: [ [pe, pe, pe], [pe, pe] ]} form,
|
||||
which basically means for every NAA there's a lsit of instances, and each consists
|
||||
of a list of a list of PEs."""
|
||||
@@ -282,7 +203,7 @@ class ProfileElementSequence:
|
||||
cur_naa_list = []
|
||||
cur_naa_list.append(pe)
|
||||
# append the final one
|
||||
if cur_naa and len(cur_naa_list) > 0:
|
||||
if cur_naa and len(cur_naa_list):
|
||||
if not cur_naa in self.pes_by_naa:
|
||||
self.pes_by_naa[cur_naa] = []
|
||||
self.pes_by_naa[cur_naa].append(cur_naa_list)
|
||||
@@ -301,8 +222,8 @@ class ProfileElementSequence:
|
||||
out += pe.to_der()
|
||||
return out
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def __repr__(self):
|
||||
return "PESequence(%s)" % ', '.join([str(x) for x in self.pe_list])
|
||||
|
||||
def __iter__(self) -> str:
|
||||
def __iter__(self):
|
||||
yield from self.pe_list
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Data sources: Provding data for profile personalization
|
||||
#
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
import secrets
|
||||
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
|
||||
class DataSource(abc.ABC):
|
||||
"""Base class for something that can provide data during a personalization process."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_one(self):
|
||||
pass
|
||||
|
||||
|
||||
class DataSourceFixed(DataSource):
|
||||
"""A data source that provides a fixed value (of any type).
|
||||
|
||||
Parameters:
|
||||
fixed_value: The fixed value that shall be used during each data generation
|
||||
"""
|
||||
def __init__(self, fixed_value, **kwargs):
|
||||
self.fixed_value = fixed_value
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def generate_one(self):
|
||||
return self.fixed_value
|
||||
|
||||
|
||||
class DataSourceIncrementing(DataSource):
|
||||
"""A data source that provides incrementing integer numbers.
|
||||
|
||||
Parameters:
|
||||
base_value: The start value (value returned during first data generation)
|
||||
step_size: Increment step size (Default: 1)
|
||||
"""
|
||||
def __init__(self, base_value: int, **kwargs):
|
||||
self.base_value = int(base_value)
|
||||
self.step_size = kwargs.pop('step_size', 1)
|
||||
self.i = 0
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def generate_one(self):
|
||||
val = self.base_value + self.i
|
||||
self.i += self.step_size
|
||||
return val
|
||||
|
||||
|
||||
class DataSourceRandomBytes(DataSource):
|
||||
"""A data source that provides a configurable number of random bytes.
|
||||
|
||||
Parameters:
|
||||
size: Number of bytes to generate each turn
|
||||
"""
|
||||
def __init__(self, size: int, **kwargs):
|
||||
self.size = size
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def generate_one(self):
|
||||
return get_random_bytes(self.size)
|
||||
|
||||
|
||||
class DataSourceRandomUInt(DataSource):
|
||||
"""A data source that provides a configurable unsigned integer value.
|
||||
|
||||
Parameters:
|
||||
below: Number one greater than the maximum permitted random unsigned integer
|
||||
"""
|
||||
def __init__(self, below: int, **kwargs):
|
||||
self.below = below
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def generate_one(self):
|
||||
return secrets.randbelow(self.below)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile OIDs
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
class OID:
|
||||
@staticmethod
|
||||
def intlist_from_str(instr: str) -> List[int]:
|
||||
return [int(x) for x in instr.split('.')]
|
||||
|
||||
@staticmethod
|
||||
def str_from_intlist(intlist: List[int]) -> str:
|
||||
return '.'.join([str(x) for x in intlist])
|
||||
|
||||
def __init__(self, initializer: Union[List[int], str]):
|
||||
if isinstance(initializer, str):
|
||||
self.intlist = self.intlist_from_str(initializer)
|
||||
else:
|
||||
self.intlist = initializer
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.str_from_intlist(self.intlist)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'OID(%s)' % (str(self))
|
||||
|
||||
|
||||
class eOID(OID):
|
||||
"""OID helper for TCA eUICC prefix"""
|
||||
__prefix = [2,23,143,1]
|
||||
def __init__(self, initializer):
|
||||
if isinstance(initializer, str):
|
||||
initializer = self.intlist_from_str(initializer)
|
||||
super().__init__(self.__prefix + initializer)
|
||||
|
||||
MF = eOID("2.1")
|
||||
DF_CD = eOID("2.2")
|
||||
DF_TELECOM = eOID("2.3")
|
||||
DF_TELECOM_v2 = eOID("2.3.2")
|
||||
ADF_USIM_by_default = eOID("2.4")
|
||||
ADF_USIM_by_default_v2 = eOID("2.4.2")
|
||||
ADF_USIM_not_by_default = eOID("2.5")
|
||||
ADF_USIM_not_by_default_v2 = eOID("2.5.2")
|
||||
ADF_USIM_not_by_default_v3 = eOID("2.5.3")
|
||||
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
|
||||
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
|
||||
ADF_ISIM_by_default = eOID("2.8")
|
||||
ADF_ISIM_not_by_default = eOID("2.9")
|
||||
ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
|
||||
ADF_CSIM_by_default = eOID("2.10")
|
||||
ADF_CSIM_by_default_v2 = eOID("2.10.2")
|
||||
ADF_CSIM_not_by_default = eOID("2.11")
|
||||
ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
|
||||
DF_EAP = eOID("2.12")
|
||||
DF_5GS = eOID("2.13")
|
||||
DF_5GS_v2 = eOID("2.13.2")
|
||||
DF_5GS_v3 = eOID("2.13.3")
|
||||
DF_5GS_v4 = eOID("2.13.4")
|
||||
DF_SAIP = eOID("2.14")
|
||||
DF_SNPN = eOID("2.15")
|
||||
DF_5GProSe = eOID("2.16")
|
||||
IoT_default = eOID("2.17")
|
||||
IoT_default = eOID("2.18")
|
||||
@@ -16,189 +16,57 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
import io
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from pySim.tlv import camel_to_snake
|
||||
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
|
||||
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
||||
|
||||
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
|
||||
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_key:str) -> List[Tuple]:
|
||||
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
|
||||
return list(filter(lambda x: x[0] not in unwanted_keys, l))
|
||||
return list(filter(lambda x: x[0] != unwanted_key, l))
|
||||
|
||||
def file_replace_content(file: List[Tuple], new_content: bytes):
|
||||
"""Completely replace all fillFileContent of a decoded 'File' with the new_content."""
|
||||
# use [:] to avoid making a copy, as we're doing in-place modification of the list here
|
||||
file[:] = remove_unwanted_tuples_from_list(file, ['fillFileContent', 'fillFileOffset'])
|
||||
file = remove_unwanted_tuples_from_list(file, 'fillFileContent')
|
||||
file.append(('fillFileContent', new_content))
|
||||
return file
|
||||
|
||||
class ClassVarMeta(abc.ABCMeta):
|
||||
"""Metaclass that puts all additional keyword-args into the class. We use this to have one
|
||||
class definition for something like a PIN, and then have derived classes for PIN1, PIN2, ..."""
|
||||
"""Metaclass that puts all additional keyword-args into the class."""
|
||||
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||
#print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
|
||||
x = super().__new__(metacls, name, bases, namespace)
|
||||
for k, v in kwargs.items():
|
||||
setattr(x, k, v)
|
||||
setattr(x, 'name', camel_to_snake(name))
|
||||
return x
|
||||
|
||||
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
||||
"""Base class representing a part of the eSIM profile that is configurable during the
|
||||
personalization process (with dynamic data from elsewhere)."""
|
||||
def __init__(self, input_value):
|
||||
self.input_value = input_value # the raw input value as given by caller
|
||||
self.value = None # the processed input value (e.g. with check digit) as produced by validate()
|
||||
|
||||
def validate(self):
|
||||
"""Optional validation method. Can be used by derived classes to perform validation
|
||||
of the input value (self.value). Will raise an exception if validation fails."""
|
||||
# default implementation: simply copy input_value over to value
|
||||
self.value = self.input_value
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
def apply(self, pe_seq: ProfileElementSequence):
|
||||
pass
|
||||
|
||||
class Iccid(ConfigurableParameter):
|
||||
"""Configurable ICCID. Expects the value to be a string of decimal digits.
|
||||
If the string of digits is only 18 digits long, a Luhn check digit will be added."""
|
||||
|
||||
def validate(self):
|
||||
# convert to string as it migt be an integer
|
||||
iccid_str = str(self.input_value)
|
||||
if len(iccid_str) < 18 or len(iccid_str) > 20:
|
||||
raise ValueError('ICCID must be 18, 19 or 20 digits long')
|
||||
if not iccid_str.isdecimal():
|
||||
raise ValueError('ICCID must only contain decimal digits')
|
||||
self.value = sanitize_iccid(iccid_str)
|
||||
|
||||
"""Configurable ICCID. Expects the value to be in EF.ICCID format."""
|
||||
name = 'iccid'
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
# patch the header
|
||||
pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(self.value, 20))
|
||||
# patch the header; FIXME: swap nibbles!
|
||||
pes.get_pe_by_type('header').decoded['iccid'] = self.value
|
||||
# patch MF/EF.ICCID
|
||||
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(self.value)))
|
||||
file_replace_content(pes.get_pe_by_type('mf').decoded['ef-iccid'], self.value)
|
||||
|
||||
class Imsi(ConfigurableParameter):
|
||||
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
||||
the last digit of the IMSI."""
|
||||
|
||||
def validate(self):
|
||||
# convert to string as it migt be an integer
|
||||
imsi_str = str(self.input_value)
|
||||
if len(imsi_str) < 6 or len(imsi_str) > 15:
|
||||
raise ValueError('IMSI must be 6..15 digits long')
|
||||
if not imsi_str.isdecimal():
|
||||
raise ValueError('IMSI must only contain decimal digits')
|
||||
self.value = imsi_str
|
||||
|
||||
"""Configurable IMSI. Expects value to be n EF.IMSI format."""
|
||||
name = 'imsi'
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
imsi_str = self.value
|
||||
# we always use the least significant byte of the IMSI as ACC
|
||||
acc = (1 << int(imsi_str[-1]))
|
||||
# patch ADF.USIM/EF.IMSI
|
||||
for pe in pes.get_pes_for_type('usim'):
|
||||
file_replace_content(pe.decoded['ef-imsi'], h2b(enc_imsi(imsi_str)))
|
||||
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
|
||||
for pe in pes.get_pes_by_type('usim'):
|
||||
file_replace_content(pe.decoded['ef-imsi'], self.value)
|
||||
# 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
|
||||
@@ -209,25 +77,13 @@ 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'] = h2b(padded_puk)
|
||||
pukCode['pukValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pukCode')
|
||||
class Puk1(Puk, keyReference=0x01):
|
||||
@@ -236,52 +92,29 @@ class Puk2(Puk, keyReference=0x81):
|
||||
pass
|
||||
|
||||
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
||||
keyReference = None
|
||||
def validate(self):
|
||||
if isinstance(self.input_value, int):
|
||||
self.value = '%04d' % self.input_value
|
||||
else:
|
||||
self.value = self.input_value
|
||||
if len(self.value) < 4 or len(self.value) > 8:
|
||||
raise ValueError('PIN mus be 4..8 digits long')
|
||||
if not self.value.isdecimal():
|
||||
raise ValueError('PIN must only contain decimal digits')
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
|
||||
padded_pin = rpad(pin, 16)
|
||||
mf_pes = pes.pes_by_naa['mf'][0]
|
||||
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||
return
|
||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||
if pinCode['keyReference'] == self.keyReference:
|
||||
pinCode['pinValue'] = h2b(padded_pin)
|
||||
pinCode['pinValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pinCode')
|
||||
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
||||
keyReference = None
|
||||
def validate(self):
|
||||
if isinstance(self.input_value, int):
|
||||
self.value = '%04d' % self.input_value
|
||||
else:
|
||||
self.value = self.input_value
|
||||
if len(self.value) < 4 or len(self.value) > 8:
|
||||
raise ValueError('PIN mus be 4..8 digits long')
|
||||
if not self.value.isdecimal():
|
||||
raise ValueError('PIN must only contain decimal digits')
|
||||
def _apply_one(self, pe: ProfileElement):
|
||||
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
|
||||
padded_pin = rpad(pin, 16)
|
||||
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
|
||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||
return
|
||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||
if pinCode['keyReference'] == self.keyReference:
|
||||
pinCode['pinValue'] = h2b(padded_pin)
|
||||
pinCode['pinValue'] = self.value
|
||||
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']:
|
||||
@@ -300,12 +133,7 @@ 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']
|
||||
@@ -318,7 +146,5 @@ class K(AlgoConfig, key='key'):
|
||||
class Opc(AlgoConfig, key='opc'):
|
||||
pass
|
||||
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
||||
def validate(self):
|
||||
if self.input_value not in [1, 2, 3]:
|
||||
raise ValueError('Invalid algorithmID %s' % (self.input_value))
|
||||
self.value = self.input_value
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,675 +0,0 @@
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile Template handling
|
||||
#
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import *
|
||||
from copy import deepcopy
|
||||
import pySim.esim.saip.oid as OID
|
||||
|
||||
class FileTemplate:
|
||||
"""Representation of a single file in a SimAlliance/TCA Profile Template."""
|
||||
def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
|
||||
sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
|
||||
params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
|
||||
pe_name:Optional[str] = None):
|
||||
# initialize from arguments
|
||||
self.fid = fid
|
||||
self.name = name
|
||||
if pe_name:
|
||||
self.pe_name = pe_name
|
||||
else:
|
||||
self.pe_name = self.name.replace('.','-').replace('_','-').lower()
|
||||
self.file_type = ftype
|
||||
if ftype in ['LF', 'CY']:
|
||||
self.nb_rec = nb_rec
|
||||
self.rec_len = size
|
||||
elif ftype in ['TR']:
|
||||
self.file_size = size
|
||||
self.arr = arr
|
||||
self.sfi = sfi
|
||||
self.default_val = default_val
|
||||
self.content_rqd = content_rqd
|
||||
self.params = params
|
||||
self.ass_serv = ass_serv
|
||||
self.high_update = high_update
|
||||
# initialize empty
|
||||
self.parent = None
|
||||
self.children = []
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "FileTemplate(%s)" % (self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s_fid = "%04x" % self.fid if self.fid is not None else 'None'
|
||||
s_arr = self.arr if self.arr is not None else 'None'
|
||||
s_sfi = "%02x" % self.sfi if self.sfi is not None else 'None'
|
||||
return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s)" % (self.name, self.pe_name, s_fid,
|
||||
self.file_type, s_arr, s_sfi)
|
||||
|
||||
class ProfileTemplate:
|
||||
"""Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
|
||||
consists of a number of file definitions. We implement each profile template as a class derived from this
|
||||
base class. Each such derived class is a singleton and has no instances."""
|
||||
created_by_default: bool = False
|
||||
oid: Optional[OID.eOID] = None
|
||||
files: List[FileTemplate] = []
|
||||
files_by_pename: dict[str,FileTemplate] = {}
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
"""This classmethod is called automatically after executing the subclass body. We use it to
|
||||
initialize the cls.files_by_pename from the cls.files"""
|
||||
super().__init_subclass__(**kwargs)
|
||||
for f in cls.files:
|
||||
cls.files_by_pename[f.pe_name] = f
|
||||
ProfileTemplateRegistry.add(cls)
|
||||
|
||||
class ProfileTemplateRegistry:
|
||||
"""A registry of profile templates. Exists as a singleton class with no instances and only
|
||||
classmethods."""
|
||||
by_oid = {}
|
||||
|
||||
@classmethod
|
||||
def add(cls, tpl: ProfileTemplate):
|
||||
"""Add a ProfileTemplate to the registry. There can only be one Template per OID."""
|
||||
oid_str = str(tpl.oid)
|
||||
if oid_str in cls.by_oid:
|
||||
raise ValueError("We already have a template for OID %s" % oid_str)
|
||||
cls.by_oid[oid_str] = tpl
|
||||
|
||||
@classmethod
|
||||
def get_by_oid(cls, oid: Union[List[int], str]) -> Optional[ProfileTemplate]:
|
||||
"""Look-up the ProfileTemplate based on its OID. The OID can be given either in dotted-string format,
|
||||
or as a list of integers."""
|
||||
if not isinstance(oid, str):
|
||||
oid = OID.OID.str_from_intlist(oid)
|
||||
return cls.by_oid.get(oid, None)
|
||||
|
||||
# below are transcribed template definitions from "ANNEX A (Normative): File Structure Templates Definition"
|
||||
# of "Profile interoperability specification V3.1 Final" (unless other version explicitly specified).
|
||||
|
||||
# Section 9.2
|
||||
class FilesAtMF(ProfileTemplate):
|
||||
created_by_default = True
|
||||
oid = OID.MF
|
||||
files = [
|
||||
FileTemplate(0x3f00, 'MF', 'MF', None, None, 14, None, None, None, params=['pinStatusTemplateDO']),
|
||||
FileTemplate(0x2f05, 'EF.PL', 'TR', None, 2, 1, 0x05, 'FF...FF', None),
|
||||
FileTemplate(0x2f02, 'EF.ICCID', 'TR', None, 10, 11, None, None, True),
|
||||
FileTemplate(0x2f00, 'EF.DIR', 'LF', None, None, 10, 0x1e, None, True, params=['nb_rec', 'size']),
|
||||
FileTemplate(0x2f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, params=['nb_rec', 'size']),
|
||||
FileTemplate(0x2f08, 'EF.UMPC', 'TR', None, 5, 10, 0x08, None, False),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.3
|
||||
class FilesCD(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_CD
|
||||
files = [
|
||||
FileTemplate(0x7f11, 'DF.CD', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
|
||||
FileTemplate(0x6f01, 'EF.LAUNCHPAD', 'TR', None, None, 2, None, None, True, params=['size']),
|
||||
]
|
||||
for i in range(0x40, 0x7f):
|
||||
files.append(FileTemplate(0x6f00+i, 'EF.ICON', 'TR', None, None, 2, None, None, True, params=['size']))
|
||||
|
||||
|
||||
# Section 9.4: Do this separately, so we can use them also from 9.5.3
|
||||
df_pb_files = [
|
||||
FileTemplate(0x5f3a, 'DF.PHONEBOOK', 'DF', None, None, 14, None, None, True, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size']),
|
||||
]
|
||||
for i in range(0x38, 0x40):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi']))
|
||||
for i in range(0x40, 0x48):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
|
||||
for i in range(0x48, 0x50):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
|
||||
df_pb_files += [
|
||||
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi']),
|
||||
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
|
||||
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
|
||||
]
|
||||
for i in range(0x50, 0x58):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
|
||||
for i in range(0x58, 0x60):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
|
||||
for i in range(0x60, 0x68):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi']))
|
||||
for i in range(0x68, 0x70):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
|
||||
for i in range(0x70, 0x78):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi']))
|
||||
for i in range(0x78, 0x80):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
|
||||
for i in range(0x80, 0x88):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
|
||||
for i in range(0x88, 0x90):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi']))
|
||||
for i in range(0x90, 0x98):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi']))
|
||||
for i in range(0x98, 0xa0):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
|
||||
|
||||
# Section 9.4 v2.3.1
|
||||
class FilesTelecom(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_TELECOM
|
||||
files = [
|
||||
FileTemplate(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
|
||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
|
||||
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
|
||||
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
|
||||
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
|
||||
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
|
||||
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
|
||||
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
|
||||
# EF.IIDF below
|
||||
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size']),
|
||||
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
|
||||
# EF.ICON below
|
||||
]
|
||||
for i in range(0x40, 0x80):
|
||||
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
|
||||
for i in range(0x80, 0xC0):
|
||||
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
|
||||
|
||||
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
|
||||
files += [deepcopy(x) for x in df_pb_files]
|
||||
|
||||
files += [
|
||||
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
|
||||
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
|
||||
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
|
||||
|
||||
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
|
||||
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
|
||||
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.4
|
||||
class FilesTelecomV2(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_TELECOM_v2
|
||||
files = [
|
||||
FileTemplate(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
|
||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
|
||||
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
|
||||
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
|
||||
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
|
||||
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
|
||||
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
|
||||
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
|
||||
# EF.IIDF below
|
||||
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size']),
|
||||
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
|
||||
# EF.ICON below
|
||||
]
|
||||
for i in range(0x40, 0x80):
|
||||
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
|
||||
for i in range(0x80, 0xC0):
|
||||
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
|
||||
|
||||
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
|
||||
files += [deepcopy(x) for x in df_pb_files]
|
||||
|
||||
files += [
|
||||
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
|
||||
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
|
||||
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
|
||||
|
||||
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
||||
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
|
||||
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
|
||||
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
|
||||
|
||||
|
||||
FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
|
||||
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
|
||||
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
|
||||
|
||||
FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
|
||||
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119]),
|
||||
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119]),
|
||||
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 2
|
||||
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 3
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.1 v2.3.1
|
||||
class FilesUsimMandatory(ProfileTemplate):
|
||||
created_by_default = True
|
||||
oid = OID.ADF_USIM_by_default
|
||||
files = [
|
||||
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
|
||||
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
|
||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
|
||||
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
|
||||
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name = 'ef-keysPS'),
|
||||
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
|
||||
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 14, 2, 0x04, None, True),
|
||||
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
|
||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
|
||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
|
||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
|
||||
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
|
||||
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
|
||||
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
|
||||
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
|
||||
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
||||
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
|
||||
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
|
||||
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
||||
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
|
||||
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
|
||||
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
|
||||
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
|
||||
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
|
||||
]
|
||||
|
||||
# Section 9.5.1
|
||||
class FilesUsimMandatoryV2(ProfileTemplate):
|
||||
created_by_default = True
|
||||
oid = OID.ADF_USIM_by_default_v2
|
||||
files = [
|
||||
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
|
||||
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
|
||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
|
||||
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
|
||||
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name='ef-keysPS'),
|
||||
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
|
||||
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 17, 2, 0x04, None, True),
|
||||
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
|
||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
|
||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
|
||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
|
||||
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
|
||||
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
|
||||
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
|
||||
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
|
||||
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
||||
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
|
||||
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
|
||||
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
||||
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
|
||||
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
|
||||
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
|
||||
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
|
||||
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.2 v2.3.1
|
||||
class FilesUsimOptional(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.ADF_USIM_not_by_default
|
||||
files = [
|
||||
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
|
||||
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
|
||||
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
|
||||
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
|
||||
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
|
||||
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
|
||||
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
|
||||
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
|
||||
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
|
||||
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
|
||||
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
|
||||
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
|
||||
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
|
||||
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
|
||||
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
|
||||
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
|
||||
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
|
||||
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
|
||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
|
||||
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
|
||||
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
|
||||
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
|
||||
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
|
||||
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
|
||||
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
|
||||
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
|
||||
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
|
||||
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
|
||||
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
|
||||
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
|
||||
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
|
||||
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
|
||||
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
|
||||
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
|
||||
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
|
||||
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
|
||||
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
|
||||
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
|
||||
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
|
||||
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
|
||||
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
|
||||
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
|
||||
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
|
||||
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
|
||||
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
|
||||
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
|
||||
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
|
||||
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
|
||||
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
|
||||
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
|
||||
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
|
||||
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
|
||||
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
|
||||
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
|
||||
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
|
||||
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
|
||||
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
|
||||
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
|
||||
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
|
||||
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
|
||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
|
||||
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
|
||||
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
|
||||
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
|
||||
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
|
||||
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
|
||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
|
||||
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
|
||||
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
|
||||
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
|
||||
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
|
||||
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
|
||||
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
|
||||
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.2
|
||||
class FilesUsimOptionalV2(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.ADF_USIM_not_by_default_v2
|
||||
files = [
|
||||
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
|
||||
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
|
||||
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
|
||||
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
|
||||
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
|
||||
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
|
||||
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
|
||||
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
|
||||
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
|
||||
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
|
||||
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
|
||||
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
|
||||
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
|
||||
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
|
||||
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
|
||||
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
|
||||
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
|
||||
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
|
||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
|
||||
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
|
||||
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
|
||||
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
|
||||
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
|
||||
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
|
||||
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
|
||||
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
|
||||
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
|
||||
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
|
||||
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
|
||||
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
|
||||
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
|
||||
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
|
||||
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
|
||||
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
|
||||
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
|
||||
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
|
||||
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
|
||||
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
|
||||
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
|
||||
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
|
||||
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
|
||||
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
|
||||
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
|
||||
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
|
||||
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
|
||||
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
|
||||
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
|
||||
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
|
||||
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
|
||||
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
|
||||
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
|
||||
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
|
||||
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
|
||||
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
|
||||
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
|
||||
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
|
||||
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
|
||||
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
|
||||
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
|
||||
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
|
||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
|
||||
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
|
||||
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
|
||||
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
|
||||
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
|
||||
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
|
||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
|
||||
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
|
||||
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
|
||||
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
|
||||
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
|
||||
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
|
||||
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
|
||||
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
|
||||
FileTemplate(0x6ff3, 'EF.EPDGID', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
|
||||
FileTemplate(0x6ff4, 'EF.EPDGSELECTION','TR',None,None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
|
||||
FileTemplate(0x6ff5, 'EF.EPDGIDEM', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
|
||||
FileTemplate(0x6ff6, 'EF.EPDGIDEMSEL','TR',None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
|
||||
FileTemplate(0x6ff7, 'EF.FromPreferred','TR',None, 1, 2, None, '00', False, ass_serv=[114]),
|
||||
FileTemplate(0x6ff8, 'EF.IMSConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[115]),
|
||||
FileTemplate(0x6ff9, 'EF.3GPPPSDataOff','TR',None, 4, 2, None, None, True, ass_serv=[117]),
|
||||
FileTemplate(0x6ffa, 'EF.3GPPPSDOSLIST','LF',None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[118]),
|
||||
FileTemplate(0x6ffc, 'EF.XCAPConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[120]),
|
||||
FileTemplate(0x6ffd, 'EF.EARFCNLIST','TR', None, None, 10, None, None, True, ['size'], ass_serv=[121]),
|
||||
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.3
|
||||
class FilesUsimDfPhonebook(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_PHONEBOOK_ADF_USIM
|
||||
files = df_pb_files
|
||||
|
||||
|
||||
# Section 9.5.4
|
||||
class FilesUsimDfGsmAccess(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_GSM_ACCESS_ADF_USIM
|
||||
files = [
|
||||
FileTemplate(0x5f3b, 'DF.GSM-ACCESS','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[27]),
|
||||
FileTemplate(0x4f20, 'EF.Kc', 'TR', None, 9, 5, 0x01, 'FF...FF07', False, ass_serv=[27], high_update=True),
|
||||
FileTemplate(0x4f52, 'EF.KcGPRS', 'TR', None, 9, 5, 0x02, 'FF...FF07', False, ass_serv=[27], high_update=True),
|
||||
FileTemplate(0x4f63, 'EF.CPBCCH', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[39], high_update=True),
|
||||
FileTemplate(0x4f64, 'EF.InvScan', 'TR', None, 1, 2, None, '00', False, ass_serv=[40]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.11 v2.3.1
|
||||
class FilesUsimDf5GS(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS
|
||||
files = [
|
||||
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
|
||||
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
|
||||
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
|
||||
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
|
||||
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
|
||||
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
|
||||
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.11.2
|
||||
class FilesUsimDf5GSv2(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS_v2
|
||||
files = [
|
||||
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
|
||||
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
|
||||
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
|
||||
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
|
||||
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
|
||||
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
|
||||
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
|
||||
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
|
||||
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.11.3
|
||||
class FilesUsimDf5GSv3(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS_v3
|
||||
files = [
|
||||
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
|
||||
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
||||
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 2, 62, 5, 0x03, 'FF...FF', False, ass_serv=[122,136], high_update=True),
|
||||
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
|
||||
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 2, 62, 5, 0x04, 'FF...FF', False, ass_serv=[122,136], high_update=True),
|
||||
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
|
||||
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
|
||||
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
|
||||
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
|
||||
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
|
||||
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130], pe_name='ef-supinai'),
|
||||
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
|
||||
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
|
||||
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.12
|
||||
class FilesUsimDfSaip(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_SAIP
|
||||
files = [
|
||||
FileTemplate(0x6fd0, 'DF.SAIP', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[(124, 125)], pe_name='df-df-saip'),
|
||||
FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF..FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.6.1
|
||||
class FilesIsimMandatory(ProfileTemplate):
|
||||
created_by_default = True
|
||||
oid = OID.ADF_ISIM_by_default
|
||||
files = [
|
||||
FileTemplate( None, 'ADF.ISIM', 'ADF', None, None, 14, None, None, False, ['aid','temporary_fid','pinStatusTemplateDO']),
|
||||
FileTemplate(0x6f02, 'EF.IMPI', 'TR', None, None, 2, 0x02, None, True, ['size']),
|
||||
FileTemplate(0x6f04, 'EF.IMPU', 'LF', 1, None, 2, 0x04, None, True, ['size']),
|
||||
FileTemplate(0x6f03, 'EF.Domain', 'TR', None, None, 2, 0x05, None, True, ['size']),
|
||||
FileTemplate(0x6f07, 'EF.IST', 'TR', None, 14, 2, 0x07, None, True),
|
||||
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 3, 10, 0x03, '000000', False),
|
||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x06, None, True, ['nb_rec','size']),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.6.2 v2.3.1
|
||||
class FilesIsimOptional(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.ADF_ISIM_not_by_default
|
||||
files = [
|
||||
FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
|
||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
|
||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
|
||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
|
||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
|
||||
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
|
||||
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
|
||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
|
||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.6.2
|
||||
class FilesIsimOptionalv2(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.ADF_ISIM_not_by_default_v2
|
||||
files = [
|
||||
FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
|
||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
|
||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
|
||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
|
||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
|
||||
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
|
||||
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
|
||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
|
||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
|
||||
FileTemplate(0x6ff7, 'EF.FromPreferred','TR', None, 1, 2, None, '00', False, ass_serv=[17]),
|
||||
FileTemplate(0x6ff8, 'EF.ImsConfigData','BT', None,None, 2, None, None, True, ['size'], ass_serv=[18]),
|
||||
FileTemplate(0x6ffc, 'EF.XcapconfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[19]),
|
||||
FileTemplate(0x6ffa, 'EF.WebRTCURI', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ass_serv=[20]),
|
||||
FileTemplate(0x6ffa, 'EF.MudMidCfgData','BT',None, None, 2, None, None, True, ['size'], ass_serv=[21]),
|
||||
]
|
||||
|
||||
|
||||
# TODO: CSIM
|
||||
|
||||
|
||||
# Section 9.8
|
||||
class FilesEap(ProfileTemplate):
|
||||
created_by_default = False
|
||||
oid = OID.DF_EAP
|
||||
files = [
|
||||
FileTemplate( None, 'DF.EAP', 'DF', None, None, 14, None, None, False, ['fid','pinStatusTemplateDO'], ass_serv=[(124, 125)]),
|
||||
FileTemplate(0x4f01, 'EF.EAPKEYS', 'TR', None, None, 2, None, None, True, ['size'], high_update=True),
|
||||
FileTemplate(0x4f02, 'EF.EAPSTATUS', 'TR', None, 1, 2, None, '00', False, high_update=True),
|
||||
FileTemplate(0x4f03, 'EF.PUId', 'TR', None, None, 2, None, None, True, ['size']),
|
||||
FileTemplate(0x4f04, 'EF.Ps', 'TR', None, None, 5, None, 'FF..FF', False, ['size'], high_update=True),
|
||||
FileTemplate(0x4f20, 'EF.CurID', 'TR', None, None, 5, None, 'FF..FF', False, ['size'], high_update=True),
|
||||
FileTemplate(0x4f21, 'EF.RelID', 'TR', None, None, 5, None, None, True, ['size']),
|
||||
FileTemplate(0x4f22, 'EF.Realm', 'TR', None, None, 5, None, None, True, ['size']),
|
||||
]
|
||||
|
||||
|
||||
# Section 9.9 Access Rules Definition
|
||||
ARR_DEFINITION = {
|
||||
1: ['8001019000', '800102A406830101950108', '800158A40683010A950108'],
|
||||
2: ['800101A406830101950108', '80015AA40683010A950108'],
|
||||
3: ['80015BA40683010A950108'],
|
||||
4: ['8001019000', '80011A9700', '800140A40683010A950108'],
|
||||
5: ['800103A406830101950108', '800158A40683010A950108'],
|
||||
6: ['800111A406830101950108', '80014AA40683010A950108'],
|
||||
7: ['800103A406830101950108', '800158A40683010A950108', '840132A406830101950108'],
|
||||
8: ['800101A406830101950108', '800102A406830181950108', '800158A40683010A950108'],
|
||||
9: ['8001019000', '80011AA406830101950108', '800140A40683010A950108'],
|
||||
10: ['8001019000', '80015AA40683010A950108'],
|
||||
11: ['8001019000', '800118A40683010A950108', '8001429700'],
|
||||
12: ['800101A406830101950108', '80015A9700'],
|
||||
13: ['800113A406830101950108', '800148A40683010A950108'],
|
||||
14: ['80015EA40683010A950108'],
|
||||
}
|
||||
@@ -92,37 +92,5 @@ 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 ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
if 'profile-a-p256' in m_svcs and not not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
|
||||
|
||||
FileChoiceList = List[Tuple]
|
||||
|
||||
class FileError(ProfileError):
|
||||
pass
|
||||
|
||||
class FileConstraintChecker:
|
||||
def check(self, l: FileChoiceList):
|
||||
for name in dir(self):
|
||||
if name.startswith('check_'):
|
||||
method = getattr(self, name)
|
||||
method(l)
|
||||
|
||||
class FileCheckBasicStructure(FileConstraintChecker):
|
||||
def check_seqence(self, l: FileChoiceList):
|
||||
by_type = {}
|
||||
for k, v in l:
|
||||
if k in by_type:
|
||||
by_type[k].append(v)
|
||||
else:
|
||||
by_type[k] = [v]
|
||||
if 'doNotCreate' in by_type:
|
||||
if len(l) != 1:
|
||||
raise FileError("doNotCreate must be the only element")
|
||||
if 'fileDescriptor' in by_type:
|
||||
if len(by_type['fileDescriptor']) != 1:
|
||||
raise FileError("fileDescriptor must be the only element")
|
||||
if l[0][0] != 'fileDescriptor':
|
||||
raise FileError("fileDescriptor must be the first element")
|
||||
|
||||
def check_forbidden(self, l: FileChoiceList):
|
||||
"""Perform checks for forbidden parameters as described in Section 8.3.3."""
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
# 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
|
||||
148
pySim/euicc.py
148
pySim/euicc.py
@@ -21,47 +21,17 @@ 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/>.
|
||||
|
||||
import argparse
|
||||
|
||||
from construct import Array, Struct, FlagsEnum, GreedyRange
|
||||
from cmd2 import cmd2, CommandSet, with_default_category
|
||||
|
||||
from pySim.tlv import *
|
||||
from pySim.construct import *
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
import argparse
|
||||
from cmd2 import cmd2, CommandSet, with_default_category
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
|
||||
from pySim.filesystem import CardADF, CardApplication
|
||||
from pySim.utils import Hexstr, SwHexstr
|
||||
import pySim.global_platform
|
||||
|
||||
def compute_eid_checksum(eid) -> str:
|
||||
"""Compute and add/replace check digits of an EID value according to GSMA SGP.29 Section 10."""
|
||||
if isinstance(eid, str):
|
||||
if len(eid) == 30:
|
||||
# first pad by 2 digits
|
||||
eid += "00"
|
||||
elif len(eid) == 32:
|
||||
# zero the last two digits
|
||||
eid = eid[:-2] + "00"
|
||||
else:
|
||||
raise ValueError("and EID must be 30 or 32 digits")
|
||||
eid_int = int(eid)
|
||||
elif isinstance(eid, int):
|
||||
eid_int = eid
|
||||
if eid_int % 100:
|
||||
# zero the last two digits
|
||||
eid_int -= eid_int % 100
|
||||
# Using the resulting 32 digits as a decimal integer, compute the remainder of that number on division by
|
||||
# 97, Subtract the remainder from 98, and use the decimal result for the two check digits, if the result
|
||||
# is one digit long, its value SHALL be prefixed by one digit of 0.
|
||||
csum = 98 - (eid_int % 97)
|
||||
eid_int += csum
|
||||
return str(eid_int)
|
||||
|
||||
def verify_eid_checksum(eid) -> bool:
|
||||
"""Verify the check digits of an EID value according to GSMA SGP.29 Section 10."""
|
||||
# Using the 32 digits as a decimal integer, compute the remainder of that number on division by 97. If the
|
||||
# remainder of the division is 1, the verification is successful; otherwise the EID is invalid.
|
||||
return int(eid) % 97 == 1
|
||||
|
||||
class VersionAdapter(Adapter):
|
||||
"""convert an EUICC Version (3-int array) to a textual representation."""
|
||||
|
||||
@@ -79,6 +49,15 @@ 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
|
||||
|
||||
@@ -307,22 +286,21 @@ class EimConfigurationDataSeq(BER_TLV_IE, tag=0xa0, nested=[EimConfigurationData
|
||||
class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
|
||||
pass
|
||||
|
||||
class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
def __init__(self):
|
||||
super().__init__(name='ADF.ISD-R', aid=AID_ISD_R,
|
||||
desc='ISD-R (Issuer Security Domain Root) Application')
|
||||
self.adf.decode_select_response = self.decode_select_response
|
||||
self.adf.shell_commands += [self.AddlShellCommands()]
|
||||
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()]
|
||||
|
||||
@staticmethod
|
||||
def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
|
||||
def store_data(scc: SimCardCommands, tx_do: Hexstr) -> Tuple[Hexstr, SwHexstr]:
|
||||
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
|
||||
Only single-block store supported for now."""
|
||||
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
|
||||
return scc.send_apdu_checksw(capdu, exp_sw)
|
||||
return scc._tp.send_apdu_checksw(capdu)
|
||||
|
||||
@staticmethod
|
||||
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw: SwMatchstr = '9000'):
|
||||
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw='9000'):
|
||||
"""Transceive STORE DATA APDU with the card, transparently encoding the command data from TLV
|
||||
and decoding the response data tlv."""
|
||||
if cmd_do:
|
||||
@@ -332,7 +310,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
return ValueError('DO > 255 bytes not supported yet')
|
||||
else:
|
||||
cmd_do_enc = b''
|
||||
(data, _sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc), exp_sw=exp_sw)
|
||||
(data, sw) = ADF_ISDR.store_data(scc, b2h(cmd_do_enc))
|
||||
if data:
|
||||
if resp_cls:
|
||||
resp_do = resp_cls()
|
||||
@@ -358,11 +336,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
@cmd2.with_argparser(es10x_store_data_parser)
|
||||
def do_es10x_store_data(self, opts):
|
||||
"""Perform a raw STORE DATA command as defined for the ES10x eUICC interface."""
|
||||
(_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
|
||||
(data, sw) = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
|
||||
eca = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
|
||||
d = eca.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_configured_addresses']))
|
||||
|
||||
@@ -373,31 +351,31 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
def do_set_default_dp_address(self, opts):
|
||||
"""Perform an ES10a SetDefaultDpAddress function."""
|
||||
sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
|
||||
sdda = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
|
||||
sdda = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEuiccChallenge(), GetEuiccChallenge)
|
||||
gec = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
|
||||
ei1 = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
|
||||
ei2 = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
||||
ln = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
||||
d = ln.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['list_notification_resp']))
|
||||
|
||||
@@ -408,13 +386,13 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
def do_remove_notification_from_list(self, opts):
|
||||
"""Perform an ES10b RemoveNotificationFromList function."""
|
||||
rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
|
||||
rn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
|
||||
rn = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
|
||||
pi = ADF_ISDR.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']))
|
||||
|
||||
@@ -433,7 +411,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
|
||||
ep_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
|
||||
ep = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
|
||||
ep = ADF_ISDR.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']))
|
||||
|
||||
@@ -452,7 +430,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
|
||||
dp_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
|
||||
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
|
||||
dp = ADF_ISDR.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']))
|
||||
|
||||
@@ -470,16 +448,16 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
p_id = Iccid(decoded=opts.iccid)
|
||||
dp_cmd_contents = [p_id]
|
||||
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
|
||||
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
|
||||
dp = ADF_ISDR.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) = CardApplicationISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
|
||||
(data, sw) = ADF_ISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
|
||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||
ged = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
|
||||
ged = ADF_ISDR.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']))
|
||||
|
||||
@@ -493,36 +471,46 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
nickname = opts.profile_nickname or ''
|
||||
sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
|
||||
sn_cmd = SetNicknameReq(children=sn_cmd_contents)
|
||||
sn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
|
||||
sn = ADF_ISDR.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 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
||||
gc = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
||||
d = gc.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_certficiates_resp']))
|
||||
|
||||
def do_get_eim_configuration_data(self, _opts):
|
||||
def do_get_eim_configuration_data(self, opts):
|
||||
"""Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
|
||||
gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
||||
GetEimConfigurationData)
|
||||
gec = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
||||
GetEimConfigurationData)
|
||||
d = gec.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_eim_configuration_data']))
|
||||
|
||||
class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
|
||||
|
||||
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()]
|
||||
|
||||
def decode_select_response(self, data_hex: Hexstr) -> object:
|
||||
t = FciTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fci_template'])
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='ADF.ECASD', aid=AID_ECASD,
|
||||
desc='ECASD (eUICC Controlling Authority Security Domain) Application')
|
||||
self.adf.decode_select_response = self.decode_select_response
|
||||
self.adf.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
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,14 +24,17 @@
|
||||
|
||||
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):
|
||||
|
||||
@@ -24,20 +24,24 @@ 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/>.
|
||||
|
||||
from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
|
||||
import argparse
|
||||
import code
|
||||
import tempfile
|
||||
import json
|
||||
import abc
|
||||
import inspect
|
||||
|
||||
import cmd2
|
||||
from cmd2 import CommandSet, with_default_category
|
||||
from cmd2 import CommandSet, with_default_category, with_argparser
|
||||
import argparse
|
||||
|
||||
from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
|
||||
|
||||
from smartcard.util import toBytes
|
||||
|
||||
from pySim.utils import sw_match, h2b, b2h, is_hex, auto_int, auto_uint8, auto_uint16, is_hexstr
|
||||
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.jsonpath import js_path_modify
|
||||
from pySim.exceptions import *
|
||||
from pySim.jsonpath import js_path_find, js_path_modify
|
||||
from pySim.commands import SimCardCommands
|
||||
|
||||
# int: a single service is associated with this file
|
||||
@@ -67,7 +71,7 @@ class CardFile:
|
||||
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 is None:
|
||||
if not isinstance(self, CardADF) and fid == None:
|
||||
raise ValueError("fid is mandatory")
|
||||
if fid:
|
||||
fid = fid.lower()
|
||||
@@ -132,7 +136,6 @@ 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,
|
||||
@@ -143,6 +146,7 @@ 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]
|
||||
@@ -163,7 +167,7 @@ class CardFile:
|
||||
|
||||
def get_mf(self) -> Optional['CardMF']:
|
||||
"""Return the MF (root) of the file system."""
|
||||
if self.parent is None:
|
||||
if self.parent == None:
|
||||
return None
|
||||
# iterate towards the top. MF has parent == self
|
||||
node = self
|
||||
@@ -278,22 +282,23 @@ class CardFile:
|
||||
"""Assuming the provided list of activated services, should this file exist and be activated?."""
|
||||
if self.service is None:
|
||||
return None
|
||||
if isinstance(self.service, int):
|
||||
elif isinstance(self.service, int):
|
||||
# a single service determines the result
|
||||
return self.service in services
|
||||
if isinstance(self.service, list):
|
||||
elif isinstance(self.service, list):
|
||||
# any of the services active -> true
|
||||
for s in self.service:
|
||||
if s in services:
|
||||
return True
|
||||
return False
|
||||
if isinstance(self.service, tuple):
|
||||
elif isinstance(self.service, tuple):
|
||||
# all of the services active -> true
|
||||
for s in self.service:
|
||||
if not s in services:
|
||||
return False
|
||||
return True
|
||||
raise ValueError("self.service must be either int or list or tuple")
|
||||
else:
|
||||
raise ValueError("self.service must be either int or list or tuple")
|
||||
|
||||
|
||||
class CardDF(CardFile):
|
||||
@@ -301,14 +306,15 @@ class CardDF(CardFile):
|
||||
|
||||
@with_default_category('DF/ADF Commands')
|
||||
class ShellCommands(CommandSet):
|
||||
pass
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not isinstance(self, CardADF):
|
||||
if 'fid' not in kwargs:
|
||||
if not 'fid' in kwargs:
|
||||
raise TypeError('fid is mandatory for all DF')
|
||||
super().__init__(**kwargs)
|
||||
self.children = {}
|
||||
self.children = dict()
|
||||
self.shell_commands = [self.ShellCommands()]
|
||||
# dict of CardFile affected by service(int), indexed by service
|
||||
self.files_by_service = {}
|
||||
@@ -409,7 +415,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 is None:
|
||||
if name == None:
|
||||
return None
|
||||
for i in self.children.values():
|
||||
if i.name and i.name == name:
|
||||
@@ -418,7 +424,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 is None:
|
||||
if sfid == None:
|
||||
return None
|
||||
for i in self.children.values():
|
||||
if i.sfid == int(str(sfid)):
|
||||
@@ -443,7 +449,7 @@ class CardMF(CardDF):
|
||||
# cannot be overridden; use assignment
|
||||
kwargs['parent'] = self
|
||||
super().__init__(**kwargs)
|
||||
self.applications = {}
|
||||
self.applications = dict()
|
||||
|
||||
def __str__(self):
|
||||
return "MF(%s)" % (self.fid)
|
||||
@@ -567,10 +573,13 @@ 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', type=is_hexstr, help='Hex-string of encoded data to decode')
|
||||
dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
|
||||
|
||||
@cmd2.with_argparser(dec_hex_parser)
|
||||
def do_decode_hex(self, opts):
|
||||
@@ -580,14 +589,14 @@ class TransparentEF(CardEF):
|
||||
|
||||
read_bin_parser = argparse.ArgumentParser()
|
||||
read_bin_parser.add_argument(
|
||||
'--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
|
||||
'--offset', type=int, default=0, help='Byte offset for start of read')
|
||||
read_bin_parser.add_argument(
|
||||
'--length', type=auto_uint16, help='Number of bytes to read')
|
||||
'--length', type=int, 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()
|
||||
@@ -597,23 +606,25 @@ 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=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')
|
||||
'--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')
|
||||
|
||||
@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(
|
||||
'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')
|
||||
|
||||
@@ -621,18 +632,18 @@ class TransparentEF(CardEF):
|
||||
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))
|
||||
else:
|
||||
data_json = json.loads(opts.data)
|
||||
(data, _sw) = self._cmd.lchan.update_binary_dec(data_json)
|
||||
(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
|
||||
@@ -645,7 +656,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)
|
||||
|
||||
@@ -686,7 +697,7 @@ class TransparentEF(CardEF):
|
||||
return method(b2h(raw_bin_data))
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -713,7 +724,7 @@ class TransparentEF(CardEF):
|
||||
return method(raw_bin_data)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -739,7 +750,7 @@ class TransparentEF(CardEF):
|
||||
return h2b(method(abstract_data))
|
||||
if self._construct:
|
||||
return build_construct(self._construct, abstract_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return t.to_tlv()
|
||||
@@ -767,7 +778,7 @@ class TransparentEF(CardEF):
|
||||
return b2h(raw_bin_data)
|
||||
if self._construct:
|
||||
return b2h(build_construct(self._construct, abstract_data))
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return b2h(t.to_tlv())
|
||||
@@ -784,10 +795,14 @@ 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', type=is_hexstr, help='Hex-string of encoded data to decode')
|
||||
dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
|
||||
|
||||
@cmd2.with_argparser(dec_hex_parser)
|
||||
def do_decode_hex(self, opts):
|
||||
@@ -797,17 +812,17 @@ class LinFixedEF(CardEF):
|
||||
|
||||
read_rec_parser = argparse.ArgumentParser()
|
||||
read_rec_parser.add_argument(
|
||||
'record_nr', type=auto_uint8, help='Number of record to be read')
|
||||
'record_nr', type=int, help='Number of record to be read')
|
||||
read_rec_parser.add_argument(
|
||||
'--count', type=auto_uint8, default=1, help='Number of records to be read, beginning at record_nr')
|
||||
'--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
|
||||
|
||||
@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:
|
||||
(data, sw) = self._cmd.lchan.read_record(recnr)
|
||||
if (len(data) > 0):
|
||||
recstr = str(data)
|
||||
else:
|
||||
recstr = "(empty)"
|
||||
@@ -815,25 +830,25 @@ class LinFixedEF(CardEF):
|
||||
|
||||
read_rec_dec_parser = argparse.ArgumentParser()
|
||||
read_rec_dec_parser.add_argument(
|
||||
'record_nr', type=auto_uint8, help='Number of record to be read')
|
||||
'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')
|
||||
|
||||
@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)"
|
||||
@@ -850,26 +865,28 @@ 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=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')
|
||||
'record_nr', type=int, help='Number of record to be read')
|
||||
upd_rec_parser.add_argument(
|
||||
'data', 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=auto_uint8, help='Number of record to be read')
|
||||
upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
|
||||
'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')
|
||||
|
||||
@@ -877,24 +894,24 @@ class LinFixedEF(CardEF):
|
||||
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(
|
||||
(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=auto_uint8, help='Number of record to be edited')
|
||||
'record_nr', type=int, 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
|
||||
@@ -907,7 +924,7 @@ 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(
|
||||
(data, sw) = self._cmd.lchan.update_record_dec(
|
||||
opts.record_nr, edited_json)
|
||||
if data:
|
||||
self._cmd.poutput_json(data)
|
||||
@@ -953,7 +970,7 @@ class LinFixedEF(CardEF):
|
||||
return method(raw_bin_data, record_nr=record_nr)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -981,7 +998,7 @@ class LinFixedEF(CardEF):
|
||||
return method(raw_hex_data, record_nr=record_nr)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -1009,7 +1026,7 @@ class LinFixedEF(CardEF):
|
||||
return b2h(raw_bin_data)
|
||||
if self._construct:
|
||||
return b2h(build_construct(self._construct, abstract_data))
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return b2h(t.to_tlv())
|
||||
@@ -1037,7 +1054,7 @@ class LinFixedEF(CardEF):
|
||||
return h2b(method(abstract_data, record_nr=record_nr))
|
||||
if self._construct:
|
||||
return build_construct(self._construct, abstract_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return t.to_tlv()
|
||||
@@ -1100,7 +1117,7 @@ class TransRecEF(TransparentEF):
|
||||
return method(raw_bin_data)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -1127,7 +1144,7 @@ class TransRecEF(TransparentEF):
|
||||
return method(raw_hex_data)
|
||||
if self._construct:
|
||||
return parse_construct(self._construct, raw_bin_data)
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_tlv(raw_bin_data)
|
||||
return t.to_dict()
|
||||
@@ -1153,7 +1170,7 @@ class TransRecEF(TransparentEF):
|
||||
return b2h(method(abstract_data))
|
||||
if self._construct:
|
||||
return b2h(filter_dict(build_construct(self._construct, abstract_data)))
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return b2h(t.to_tlv())
|
||||
@@ -1180,7 +1197,7 @@ class TransRecEF(TransparentEF):
|
||||
return h2b(method(abstract_data))
|
||||
if self._construct:
|
||||
return filter_dict(build_construct(self._construct, abstract_data))
|
||||
if self._tlv:
|
||||
elif self._tlv:
|
||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||
t.from_dict(abstract_data)
|
||||
return t.to_tlv()
|
||||
@@ -1210,6 +1227,9 @@ 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')
|
||||
@@ -1217,10 +1237,10 @@ class BerTlvEF(CardEF):
|
||||
@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)
|
||||
@@ -1228,12 +1248,13 @@ class BerTlvEF(CardEF):
|
||||
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', type=is_hexstr, help='Data bytes (hex format) to write')
|
||||
set_data_parser.add_argument(
|
||||
'data', 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)
|
||||
|
||||
@@ -1244,7 +1265,7 @@ class BerTlvEF(CardEF):
|
||||
@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)
|
||||
(data, sw) = self._cmd.lchan.set_data(opts.tag, None)
|
||||
if data:
|
||||
self._cmd.poutput(data)
|
||||
|
||||
@@ -1296,7 +1317,7 @@ class CardApplication:
|
||||
"""
|
||||
self.name = name
|
||||
self.adf = adf
|
||||
self.sw = sw or {}
|
||||
self.sw = sw or dict()
|
||||
# back-reference from ADF to Applicaiton
|
||||
if self.adf:
|
||||
self.aid = aid or self.adf.aid
|
||||
|
||||
345
pySim/global_platform.py
Normal file
345
pySim/global_platform.py
Normal file
@@ -0,0 +1,345 @@
|
||||
# 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)
|
||||
@@ -1,926 +0,0 @@
|
||||
# coding=utf-8
|
||||
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
|
||||
|
||||
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import io
|
||||
from copy import deepcopy
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
from construct import Optional as COptional
|
||||
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.Cipher import DES, DES3, AES
|
||||
from pySim.global_platform.scp import SCP02, SCP03
|
||||
from pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.tlv import *
|
||||
from pySim.profile import CardProfile
|
||||
|
||||
sw_table = {
|
||||
'Warnings': {
|
||||
'6200': 'Logical Channel already closed',
|
||||
'6283': 'Card Life Cycle State is CARD_LOCKED',
|
||||
'6310': 'More data available',
|
||||
},
|
||||
'Execution errors': {
|
||||
'6400': 'No specific diagnosis',
|
||||
'6581': 'Memory failure',
|
||||
},
|
||||
'Checking errors': {
|
||||
'6700': 'Wrong length in Lc',
|
||||
},
|
||||
'Functions in CLA not supported': {
|
||||
'6881': 'Logical channel not supported or active',
|
||||
'6882': 'Secure messaging not supported',
|
||||
},
|
||||
'Command not allowed': {
|
||||
'6982': 'Security Status not satisfied',
|
||||
'6985': 'Conditions of use not satisfied',
|
||||
},
|
||||
'Wrong parameters': {
|
||||
'6a80': 'Incorrect values in command data',
|
||||
'6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
|
||||
'6a82': 'Application not found',
|
||||
'6a84': 'Not enough memory space',
|
||||
'6a86': 'Incorrect P1 P2',
|
||||
'6a88': 'Referenced data not found',
|
||||
},
|
||||
'GlobalPlatform': {
|
||||
'6d00': 'Invalid instruction',
|
||||
'6e00': 'Invalid class',
|
||||
},
|
||||
'Application errors': {
|
||||
'9484': 'Algorithm not supported',
|
||||
'9485': 'Invalid key check value',
|
||||
},
|
||||
}
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.1.6
|
||||
KeyType = Enum(Byte, des=0x80,
|
||||
tls_psk=0x85, # v2.3.1 Section 11.1.8
|
||||
aes=0x88, # v2.3.1 Section 11.1.8
|
||||
hmac_sha1=0x90, # v2.3.1 Section 11.1.8
|
||||
hmac_sha1_160=0x91, # v2.3.1 Section 11.1.8
|
||||
rsa_public_exponent_e_cleartex=0xA0,
|
||||
rsa_modulus_n_cleartext=0xA1,
|
||||
rsa_modulus_n=0xA2,
|
||||
rsa_private_exponent_d=0xA3,
|
||||
rsa_chines_remainder_p=0xA4,
|
||||
rsa_chines_remainder_q=0xA5,
|
||||
rsa_chines_remainder_pq=0xA6,
|
||||
rsa_chines_remainder_dpi=0xA7,
|
||||
rsa_chines_remainder_dqi=0xA8,
|
||||
ecc_public_key=0xB0, # v2.3.1 Section 11.1.8
|
||||
ecc_private_key=0xB1, # v2.3.1 Section 11.1.8
|
||||
ecc_field_parameter_p=0xB2, # v2.3.1 Section 11.1.8
|
||||
ecc_field_parameter_a=0xB3, # v2.3.1 Section 11.1.8
|
||||
ecc_field_parameter_b=0xB4, # v2.3.1 Section 11.1.8
|
||||
ecc_field_parameter_g=0xB5, # v2.3.1 Section 11.1.8
|
||||
ecc_field_parameter_n=0xB6, # v2.3.1 Section 11.1.8
|
||||
ecc_field_parameter_k=0xB7, # v2.3.1 Section 11.1.8
|
||||
ecc_key_parameters_reference=0xF0, # v2.3.1 Section 11.1.8
|
||||
not_available=0xff)
|
||||
|
||||
# GlobalPlatform 2.3 Section 11.10.2.1 Table 11-86
|
||||
SetStatusScope = Enum(Byte, isd=0x80, app_or_ssd=0x40, isd_and_assoc_apps=0xc0)
|
||||
|
||||
# GlobalPlatform 2.3 section 11.1.1
|
||||
CLifeCycleState = Enum(Byte, loaded=0x01, installed=0x03, selectable=0x07, personalized=0x0f, locked=0x83)
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.3.3.1
|
||||
class KeyInformationData(BER_TLV_IE, tag=0xc0):
|
||||
_test_de_encode = [
|
||||
( 'c00401708010', {"key_identifier": 1, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00402708010', {"key_identifier": 2, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00403708010', {"key_identifier": 3, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00401018010', {"key_identifier": 1, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00402018010', {"key_identifier": 2, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00403018010', {"key_identifier": 3, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00401028010', {"key_identifier": 1, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00402028010', {"key_identifier": 2, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00403038010', {"key_identifier": 3, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00401038010', {"key_identifier": 1, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00402038010', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||
( 'c00402038810', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "aes"} ]} ),
|
||||
]
|
||||
KeyTypeLen = Struct('type'/KeyType, 'length'/Int8ub)
|
||||
_construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
|
||||
'key_types'/GreedyRange(KeyTypeLen))
|
||||
class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
|
||||
pass
|
||||
|
||||
# GP v2.3 11.1.9
|
||||
KeyUsageQualifier = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
|
||||
verification_encryption=0x8000,
|
||||
computation_decipherment=0x4000,
|
||||
sm_response=0x2000,
|
||||
sm_command=0x1000,
|
||||
confidentiality=0x0800,
|
||||
crypto_checksum=0x0400,
|
||||
digital_signature=0x0200,
|
||||
crypto_authorization=0x0100,
|
||||
key_agreement=0x0080)
|
||||
|
||||
# GP v2.3 11.1.10
|
||||
KeyAccess = Enum(Byte, sd_and_any_assoc_app=0x00, sd_only=0x01, any_assoc_app_but_not_sd=0x02,
|
||||
not_available=0xff)
|
||||
|
||||
class KeyLoading:
|
||||
# Global Platform Specification v2.3 Section 11.11.4.2.2.3 DGIs for the CC Private Key
|
||||
|
||||
class KeyUsageQualifier(BER_TLV_IE, tag=0x95):
|
||||
_construct = KeyUsageQualifier
|
||||
|
||||
class KeyAccess(BER_TLV_IE, tag=0x96):
|
||||
_construct = KeyAccess
|
||||
|
||||
class KeyType(BER_TLV_IE, tag=0x80):
|
||||
_construct = KeyType
|
||||
|
||||
class KeyLength(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
class KeyIdentifier(BER_TLV_IE, tag=0x82):
|
||||
_construct = Int8ub
|
||||
|
||||
class KeyVersionNumber(BER_TLV_IE, tag=0x83):
|
||||
_construct = Int8ub
|
||||
|
||||
class KeyParameterReferenceValue(BER_TLV_IE, tag=0x85):
|
||||
_construct = Enum(Byte, secp256r1=0x00, secp384r1=0x01, secp521r1=0x02, brainpoolP256r1=0x03,
|
||||
brainpoolP256t1=0x04, brainpoolP384r1=0x05, brainpoolP384t1=0x06,
|
||||
brainpoolP512r1=0x07, brainpoolP512t1=0x08)
|
||||
|
||||
# pylint: disable=undefined-variable
|
||||
class ControlReferenceTemplate(BER_TLV_IE, tag=0xb9,
|
||||
nested=[KeyUsageQualifier,
|
||||
KeyAccess,
|
||||
KeyType,
|
||||
KeyLength,
|
||||
KeyIdentifier,
|
||||
KeyVersionNumber,
|
||||
KeyParameterReferenceValue]):
|
||||
pass
|
||||
|
||||
# Table 11-103
|
||||
class EccPublicKey(DGI_TLV_IE, tag=0x0036):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# Table 11-105
|
||||
class EccPrivateKey(DGI_TLV_IE, tag=0x8137):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# Global Platform Specification v2.3 Section 11.11.4 / Table 11-91
|
||||
class KeyControlReferenceTemplate(DGI_TLV_IE, tag=0x00b9, nested=[ControlReferenceTemplate]):
|
||||
pass
|
||||
|
||||
|
||||
# GlobalPlatform v2.3.1 Section H.4 / Table H-6
|
||||
class ScpType(BER_TLV_IE, tag=0x80):
|
||||
_construct = HexAdapter(Byte)
|
||||
class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyBytes
|
||||
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
|
||||
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
|
||||
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
|
||||
_consuruct = GreedyRange(Int16ub)
|
||||
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
|
||||
SupportedTlsCipherSuitesForScp81]):
|
||||
pass
|
||||
class PrivilegesAvailableSSD(BER_TLV_IE, tag=0x81):
|
||||
pass
|
||||
class PrivilegesAvailableApplication(BER_TLV_IE, tag=0x82):
|
||||
pass
|
||||
class SupportedLFDBHAlgorithms(BER_TLV_IE, tag=0x83):
|
||||
pass
|
||||
# GlobalPlatform Card Specification v2.3 / Table H-8
|
||||
class CiphersForLFDBEncryption(BER_TLV_IE, tag=0x84):
|
||||
_construct = Enum(Byte, tripledes16=0x01, aes128=0x02, aes192=0x04, aes256=0x08,
|
||||
icv_supported_for_lfdb=0x80)
|
||||
CipherSuitesForSignatures = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
|
||||
rsa1024_pkcsv15_sha1=0x0100,
|
||||
rsa_gt1024_pss_sha256=0x0200,
|
||||
single_des_plus_final_triple_des_mac_16b=0x0400,
|
||||
cmac_aes128=0x0800, cmac_aes192=0x1000, cmac_aes256=0x2000,
|
||||
ecdsa_ecc256_sha256=0x4000, ecdsa_ecc384_sha384=0x8000,
|
||||
ecdsa_ecc512_sha512=0x0001, ecdsa_ecc_521_sha512=0x0002)
|
||||
class CiphersForTokens(BER_TLV_IE, tag=0x85):
|
||||
_construct = CipherSuitesForSignatures
|
||||
class CiphersForReceipts(BER_TLV_IE, tag=0x86):
|
||||
_construct = CipherSuitesForSignatures
|
||||
class CiphersForDAPs(BER_TLV_IE, tag=0x87):
|
||||
_construct = CipherSuitesForSignatures
|
||||
class KeyParameterReferenceList(BER_TLV_IE, tag=0x88, nested=[KeyLoading.KeyParameterReferenceValue]):
|
||||
pass
|
||||
class CardCapabilityInformation(BER_TLV_IE, tag=0x67, nested=[ScpInformation, PrivilegesAvailableSSD,
|
||||
PrivilegesAvailableApplication,
|
||||
SupportedLFDBHAlgorithms,
|
||||
CiphersForLFDBEncryption, CiphersForTokens,
|
||||
CiphersForReceipts, CiphersForDAPs,
|
||||
KeyParameterReferenceList]):
|
||||
pass
|
||||
|
||||
class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
|
||||
_construct = Int8ub
|
||||
|
||||
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
|
||||
class ApplicationAID(BER_TLV_IE, tag=0x4f):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
|
||||
pass
|
||||
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
|
||||
pass
|
||||
|
||||
# GlobalPlatform v2.3.1 Section 11.3.3.1.2 + TS 102 226
|
||||
class NumberOFInstalledApp(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyInteger()
|
||||
class FreeNonVolatileMemory(BER_TLV_IE, tag=0x82):
|
||||
_construct = GreedyInteger()
|
||||
class FreeVolatileMemory(BER_TLV_IE, tag=0x83):
|
||||
_construct = GreedyInteger()
|
||||
class ExtendedCardResourcesInfo(BER_TLV_IE, tag=0xff21, nested=[NumberOFInstalledApp, FreeNonVolatileMemory,
|
||||
FreeVolatileMemory]):
|
||||
pass
|
||||
|
||||
# GlobalPlatform v2.3.1 Section 7.4.2.4 + GP SPDM
|
||||
class SecurityDomainManagerURL(BER_TLV_IE, tag=0x5f50):
|
||||
pass
|
||||
|
||||
|
||||
# card data sample, returned in response to GET DATA (80ca006600):
|
||||
# 66 31
|
||||
# 73 2f
|
||||
# 06 07
|
||||
# 2a864886fc6b01
|
||||
# 60 0c
|
||||
# 06 0a
|
||||
# 2a864886fc6b02020101
|
||||
# 63 09
|
||||
# 06 07
|
||||
# 2a864886fc6b03
|
||||
# 64 0b
|
||||
# 06 09
|
||||
# 2a864886fc6b040215
|
||||
|
||||
# GlobalPlatform 2.1.1 Table F-1
|
||||
class ObjectIdentifier(BER_TLV_IE, tag=0x06):
|
||||
_construct = GreedyBytes
|
||||
class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
|
||||
pass
|
||||
class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
|
||||
pass
|
||||
class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
|
||||
pass
|
||||
class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
|
||||
_construct = GreedyBytes
|
||||
class CardChipDetails(BER_TLV_IE, tag=0x66):
|
||||
_construct = GreedyBytes
|
||||
class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
|
||||
CardManagementTypeAndVersion,
|
||||
CardIdentificationScheme,
|
||||
SecureChannelProtocolOfISD,
|
||||
CardConfigurationDetails,
|
||||
CardChipDetails]):
|
||||
pass
|
||||
class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
|
||||
pass
|
||||
|
||||
# GlobalPlatform 2.1.1 Table F-2
|
||||
class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
|
||||
pass
|
||||
class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
|
||||
CardIdentificationScheme,
|
||||
SecureChannelProtocolOfSelectedSD,
|
||||
CardConfigurationDetails,
|
||||
CardChipDetails]):
|
||||
pass
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.1.1
|
||||
IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
|
||||
card_locked = 0x7f, terminated=0xff)
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||
class ApplicationID(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||
class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||
class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||
class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||
class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
|
||||
ApplicationProductionLifeCycleData,
|
||||
MaximumLengthOfDataFieldInCommandMessage]):
|
||||
pass
|
||||
|
||||
# explicitly define this list and give it a name so pySim.euicc can reference it
|
||||
FciTemplateNestedList = [ApplicationID, SecurityDomainManagementData,
|
||||
ApplicationProductionLifeCycleData,
|
||||
MaximumLengthOfDataFieldInCommandMessage,
|
||||
ProprietaryData]
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||
class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
|
||||
pass
|
||||
|
||||
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class CardImageNumber(BER_TLV_IE, tag=0x45):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
|
||||
_construct = GreedyInteger()
|
||||
|
||||
# Collection of all the data objects we can get from GET DATA
|
||||
class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
|
||||
CardImageNumber,
|
||||
CardData,
|
||||
KeyInformation,
|
||||
SequenceCounterOfDefaultKvn,
|
||||
ConfirmationCounter,
|
||||
# v2.3.1
|
||||
CardCapabilityInformation,
|
||||
CurrentSecurityLevel,
|
||||
ListOfApplications,
|
||||
ExtendedCardResourcesInfo,
|
||||
SecurityDomainManagerURL]):
|
||||
pass
|
||||
|
||||
def decode_select_response(resp_hex: str) -> object:
|
||||
t = FciTemplate()
|
||||
t.from_tlv(h2b(resp_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fci_template'])
|
||||
|
||||
# 11.4.2.1
|
||||
StatusSubset = Enum(Byte, isd=0x80, applications=0x40, files=0x20, files_and_modules=0x10)
|
||||
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class LifeCycleState(BER_TLV_IE, tag=0x9f70):
|
||||
_construct = CLifeCycleState
|
||||
|
||||
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
|
||||
class Privileges(BER_TLV_IE, tag=0xc5):
|
||||
# we only support 3-byte encoding. Can't use StripTrailerAdapter as length==2 is not permitted. sigh.
|
||||
_construct = FlagsEnum(Int24ub,
|
||||
security_domain=0x800000, dap_verification=0x400000,
|
||||
delegated_management=0x200000, card_lock=0x100000, card_terminate=0x080000,
|
||||
card_reset=0x040000, cvm_management=0x020000,
|
||||
mandated_dap_verification=0x010000,
|
||||
trusted_path=0x8000, authorized_management=0x4000,
|
||||
token_management=0x2000, global_delete=0x1000, global_lock=0x0800,
|
||||
global_registry=0x0400, final_application=0x0200, global_service=0x0100,
|
||||
receipt_generation=0x80, ciphered_load_file_data_block=0x40,
|
||||
contactless_activation=0x20, contactless_self_activation=0x10)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36 + Section 11.1.7
|
||||
class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
|
||||
_construct = BitStruct('contactless_io'/Flag,
|
||||
'contact_io'/Flag,
|
||||
'_rfu'/Flag,
|
||||
'logical_channel_number'/BitsInteger(5))
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
|
||||
# Note: the Executable Load File Version Number format and contents are beyond the scope of this
|
||||
# specification. It shall consist of the version information contained in the original Load File: on a
|
||||
# Java Card based card, this version number represents the major and minor version attributes of the
|
||||
# original Load File Data Block.
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
|
||||
ImplicitSelectionParameter, ExecutableLoadFileAID,
|
||||
ExecutableLoadFileVersionNumber,
|
||||
ExecutableModuleAID, AssociatedSecurityDomainAID]):
|
||||
pass
|
||||
|
||||
|
||||
# Section 11.6.2.3 / Table 11-58
|
||||
class SecurityDomainAid(BER_TLV_IE, tag=0x4f):
|
||||
_construct = GreedyBytes
|
||||
class LoadFileDataBlockSignature(BER_TLV_IE, tag=0xc3):
|
||||
_construct = GreedyBytes
|
||||
class DapBlock(BER_TLV_IE, tag=0xe2, nested=[SecurityDomainAid, LoadFileDataBlockSignature]):
|
||||
pass
|
||||
class LoadFileDataBlock(BER_TLV_IE, tag=0xc4):
|
||||
_construct = GreedyBytes
|
||||
class Icv(BER_TLV_IE, tag=0xd3):
|
||||
_construct = GreedyBytes
|
||||
class CipheredLoadFileDataBlock(BER_TLV_IE, tag=0xd4):
|
||||
_construct = GreedyBytes
|
||||
class LoadFile(TLV_IE_Collection, nested=[DapBlock, LoadFileDataBlock, Icv, CipheredLoadFileDataBlock]):
|
||||
pass
|
||||
|
||||
# Application Dedicated File of a Security Domain
|
||||
class ADF_SD(CardADF):
|
||||
StoreData = BitStruct('last_block'/Flag,
|
||||
'encryption'/Enum(BitsInteger(2), none=0, application_dependent=1, rfu=2, encrypted=3),
|
||||
'structure'/Enum(BitsInteger(2), none=0, dgi=1, ber_tlv=2, rfu=3),
|
||||
'_pad'/Padding(2),
|
||||
'response'/Enum(Bit, not_expected=0, may_be_returned=1))
|
||||
|
||||
def __init__(self, aid: str, name: str, desc: str):
|
||||
super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
def decode_select_response(self, data_hex: str) -> object:
|
||||
return decode_select_response(data_hex)
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
get_data_parser = argparse.ArgumentParser()
|
||||
get_data_parser.add_argument('data_object_name', type=str,
|
||||
help='Name of the data object to be retrieved from the card')
|
||||
|
||||
@cmd2.with_argparser(get_data_parser)
|
||||
def do_get_data(self, opts):
|
||||
"""Perform the GlobalPlatform GET DATA command in order to obtain some card-specific data."""
|
||||
tlv_cls_name = opts.data_object_name
|
||||
try:
|
||||
tlv_cls = DataCollection().members_by_name[tlv_cls_name]
|
||||
except KeyError:
|
||||
do_names = [camel_to_snake(str(x.__name__)) for x in DataCollection.possible_nested]
|
||||
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
|
||||
do_names))
|
||||
return
|
||||
(data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
|
||||
ie = tlv_cls()
|
||||
ie.from_tlv(h2b(data))
|
||||
self._cmd.poutput_json(ie.to_dict())
|
||||
|
||||
def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
|
||||
data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
|
||||
index_dict = {1: data_dict}
|
||||
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
|
||||
|
||||
store_data_parser = argparse.ArgumentParser()
|
||||
store_data_parser.add_argument('--data-structure', type=str, choices=['none','dgi','ber_tlv','rfu'], default='none')
|
||||
store_data_parser.add_argument('--encryption', type=str, choices=['none','application_dependent', 'rfu', 'encrypted'], default='none')
|
||||
store_data_parser.add_argument('--response', type=str, choices=['not_expected','may_be_returned'], default='not_expected')
|
||||
store_data_parser.add_argument('DATA', type=is_hexstr)
|
||||
|
||||
@cmd2.with_argparser(store_data_parser)
|
||||
def do_store_data(self, opts):
|
||||
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
|
||||
See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
|
||||
response_permitted = opts.response == 'may_be_returned'
|
||||
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
|
||||
|
||||
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
|
||||
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
|
||||
See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
|
||||
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
|
||||
# Table 11-89 of GP Card Specification v2.3
|
||||
remainder = data
|
||||
block_nr = 0
|
||||
response = ''
|
||||
while len(remainder):
|
||||
chunk = remainder[:max_cmd_len]
|
||||
remainder = remainder[max_cmd_len:]
|
||||
p1b = build_construct(ADF_SD.StoreData,
|
||||
{'last_block': len(remainder) == 0, 'encryption': encryption,
|
||||
'structure': structure, 'response': response_permitted})
|
||||
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
|
||||
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk))
|
||||
block_nr += 1
|
||||
response += data
|
||||
return data
|
||||
|
||||
put_key_parser = argparse.ArgumentParser()
|
||||
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
|
||||
put_key_parser.add_argument('--key-version-nr', type=auto_uint8, required=True, help='Key Version Number')
|
||||
put_key_parser.add_argument('--key-id', type=auto_uint7, required=True, help='Key Identifier (base)')
|
||||
put_key_parser.add_argument('--key-type', choices=KeyType.ksymapping.values(), action='append', required=True, help='Key Type')
|
||||
put_key_parser.add_argument('--key-data', type=is_hexstr, action='append', required=True, help='Key Data Block')
|
||||
put_key_parser.add_argument('--key-check', type=is_hexstr, action='append', help='Key Check Value')
|
||||
put_key_parser.add_argument('--suppress-key-check', action='store_true', help='Suppress generation of Key Check Values')
|
||||
|
||||
@cmd2.with_argparser(put_key_parser)
|
||||
def do_put_key(self, opts):
|
||||
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
|
||||
|
||||
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
|
||||
otherwise be automatically generated for DES and AES keys. You can suppress the latter using
|
||||
`--suppress-key-check`.
|
||||
|
||||
Example (SCP80 KIC/KID/KIK):
|
||||
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
|
||||
--key-type aes --key-data 101112131415161718191a1b1c1d1e1f
|
||||
--key-type aes --key-data 202122232425262728292a2b2c2d2e2f
|
||||
|
||||
Example (SCP81 TLS-PSK/KEK):
|
||||
put_key --key-version-nr 0x40 --key-id 0x01 --key-type tls_psk --key-data 303132333435363738393a3b3c3d3e3f
|
||||
--key-type des --key-data 404142434445464748494a4b4c4d4e4f
|
||||
|
||||
"""
|
||||
if len(opts.key_type) != len(opts.key_data):
|
||||
raise ValueError('There must be an equal number of key-type and key-data arguments')
|
||||
kdb = []
|
||||
for i in range(0, len(opts.key_type)):
|
||||
if opts.key_check and len(opts.key_check) > i:
|
||||
kcv = opts.key_check[i]
|
||||
elif opts.suppress_key_check:
|
||||
kcv = ''
|
||||
else:
|
||||
kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
||||
kcv = b2h(kcv_bin)
|
||||
if self._cmd.lchan.scc.scp:
|
||||
# encrypte key data with DEK of current SCP
|
||||
kcb = b2h(self._cmd.lchan.scc.scp.card_keys.encrypt_key(h2b(opts.key_data[i])))
|
||||
else:
|
||||
# (for example) during personalization, DEK might not be required)
|
||||
kcb = opts.key_data[i]
|
||||
kdb.append({'key_type': opts.key_type[i], 'kcb': kcb, 'kcv': kcv})
|
||||
p2 = opts.key_id
|
||||
if len(opts.key_type) > 1:
|
||||
p2 |= 0x80
|
||||
self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
|
||||
|
||||
# Table 11-68: Key Data Field - Format 1 (Basic Format)
|
||||
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
|
||||
'kcb'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'kcv'/HexAdapter(Prefixed(Int8ub, GreedyBytes))))
|
||||
|
||||
def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
|
||||
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
|
||||
key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
|
||||
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
|
||||
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data))
|
||||
return data
|
||||
|
||||
get_status_parser = argparse.ArgumentParser()
|
||||
get_status_parser.add_argument('subset', choices=StatusSubset.ksymapping.values(),
|
||||
help='Subset of statuses to be included in the response')
|
||||
get_status_parser.add_argument('--aid', type=is_hexstr, default='',
|
||||
help='AID Search Qualifier (search only for given AID)')
|
||||
|
||||
@cmd2.with_argparser(get_status_parser)
|
||||
def do_get_status(self, opts):
|
||||
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
|
||||
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
|
||||
grd_list = self.get_status(opts.subset, opts.aid)
|
||||
for grd in grd_list:
|
||||
self._cmd.poutput_json(grd.to_dict())
|
||||
|
||||
def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]:
|
||||
subset_hex = b2h(build_construct(StatusSubset, subset))
|
||||
aid = ApplicationAID(decoded=aid_search_qualifier)
|
||||
cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc')
|
||||
p2 = 0x02 # TLV format according to Table 11-36
|
||||
grd_list = []
|
||||
while True:
|
||||
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
|
||||
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data))
|
||||
remainder = h2b(data)
|
||||
while len(remainder):
|
||||
# tlv sequence, each element is one GpRegistryRelatedData()
|
||||
grd = GpRegistryRelatedData()
|
||||
_dec, remainder = grd.from_tlv(remainder)
|
||||
grd_list.append(grd)
|
||||
if sw != '6310':
|
||||
return grd_list
|
||||
else:
|
||||
p2 |= 0x01
|
||||
return grd_list
|
||||
|
||||
set_status_parser = argparse.ArgumentParser()
|
||||
set_status_parser.add_argument('scope', choices=SetStatusScope.ksymapping.values(),
|
||||
help='Defines the scope of the requested status change')
|
||||
set_status_parser.add_argument('status', choices=CLifeCycleState.ksymapping.values(),
|
||||
help='Specify the new intended status')
|
||||
set_status_parser.add_argument('--aid', type=is_hexstr,
|
||||
help='AID of the target Application or Security Domain')
|
||||
|
||||
@cmd2.with_argparser(set_status_parser)
|
||||
def do_set_status(self, opts):
|
||||
"""Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
|
||||
Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
|
||||
prior authentication with a Secure Channel Protocol."""
|
||||
self.set_status(opts.scope, opts.status, opts.aid)
|
||||
|
||||
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
|
||||
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
|
||||
'scope'/SetStatusScope, 'status'/CLifeCycleState,
|
||||
'aid'/HexAdapter(Prefixed(Int8ub, COptional(GreedyBytes))))
|
||||
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
|
||||
_data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
|
||||
|
||||
inst_perso_parser = argparse.ArgumentParser()
|
||||
inst_perso_parser.add_argument('application-aid', type=is_hexstr, help='Application AID')
|
||||
|
||||
@cmd2.with_argparser(inst_perso_parser)
|
||||
def do_install_for_personalization(self, opts):
|
||||
"""Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security
|
||||
Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
|
||||
# Section 11.5.2.3.6 / Table 11-47
|
||||
self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid))
|
||||
|
||||
inst_inst_parser = argparse.ArgumentParser()
|
||||
inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
|
||||
help='Executable Load File AID')
|
||||
inst_inst_parser.add_argument('--module-aid', type=is_hexstr, default='',
|
||||
help='Executable Module AID')
|
||||
inst_inst_parser.add_argument('--application-aid', type=is_hexstr, required=True,
|
||||
help='Application AID')
|
||||
inst_inst_parser.add_argument('--install-parameters', type=is_hexstr, default='',
|
||||
help='Install Parameters')
|
||||
inst_inst_parser.add_argument('--privilege', action='append', dest='privileges', default=[],
|
||||
choices=Privileges._construct.flags.keys(),
|
||||
help='Privilege granted to newly installed Application')
|
||||
inst_inst_parser.add_argument('--install-token', type=is_hexstr, default='',
|
||||
help='Install Token (Section GPCS C.4.2/C.4.7)')
|
||||
inst_inst_parser.add_argument('--make-selectable', action='store_true',
|
||||
help='Install and make selectable')
|
||||
|
||||
@cmd2.with_argparser(inst_inst_parser)
|
||||
def do_install_for_install(self, opts):
|
||||
"""Perform GlobalPlatform INSTALL [for install] command in order to install an application."""
|
||||
InstallForInstallCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'module_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'application_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'privileges'/Prefixed(Int8ub, Privileges._construct),
|
||||
'install_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'install_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
|
||||
p1 = 0x04
|
||||
if opts.make_selectable:
|
||||
p1 |= 0x08
|
||||
decoded = vars(opts)
|
||||
# convert from list to "true-dict" as required by construct.FlagsEnum
|
||||
decoded['privileges'] = {x: True for x in decoded['privileges']}
|
||||
ifi_bytes = build_construct(InstallForInstallCD, decoded)
|
||||
self.install(p1, 0x00, b2h(ifi_bytes))
|
||||
|
||||
inst_load_parser = argparse.ArgumentParser()
|
||||
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
|
||||
help='AID of the loded file')
|
||||
inst_load_parser.add_argument('--security-domain-aid', type=is_hexstr, default='',
|
||||
help='AID of the Security Domain into which the file shalle be added')
|
||||
inst_load_parser.add_argument('--load-file-hash', type=is_hexstr, default='',
|
||||
help='Load File Data Block Hash')
|
||||
inst_inst_parser.add_argument('--load-parameters', type=is_hexstr, default='',
|
||||
help='Load Token (Section GPCS C.4.1)')
|
||||
inst_inst_parser.add_argument('--load-token', type=is_hexstr, default='',
|
||||
help='Load Token (Section GPCS C.4.1)')
|
||||
|
||||
@cmd2.with_argparser(inst_load_parser)
|
||||
def do_install_for_load(self, opts):
|
||||
"""Perform GlobalPlatform INSTALL [for load] command."""
|
||||
if opts.load_token != '' and opts.load_file_hash == '':
|
||||
raise ValueError('Load File Data Block Hash is mandatory if a Load Token is present')
|
||||
InstallForLoadCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'security_domain_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'load_file_hash'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'load_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'load_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
|
||||
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
|
||||
self.install(0x02, 0x00, b2h(ifl_bytes))
|
||||
|
||||
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
|
||||
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
del_cc_parser = argparse.ArgumentParser()
|
||||
del_cc_parser.add_argument('aid', type=is_hexstr,
|
||||
help='Executable Load File or Application AID')
|
||||
del_cc_parser.add_argument('--delete-related-objects', action='store_true',
|
||||
help='Delete not only the object but also its related objects')
|
||||
|
||||
@cmd2.with_argparser(del_cc_parser)
|
||||
def do_delete_card_content(self, opts):
|
||||
"""Perform a GlobalPlatform DELETE [card content] command in order to delete an Executable Load
|
||||
File, an Application or an Executable Load File and its related Applications."""
|
||||
p2 = 0x80 if opts.delete_related_objects else 0x00
|
||||
aid = ApplicationAID(decoded=opts.aid)
|
||||
self.delete(0x00, p2, b2h(aid.to_tlv()))
|
||||
|
||||
del_key_parser = argparse.ArgumentParser()
|
||||
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
|
||||
del_key_parser.add_argument('--key-ver', type=auto_uint8, help='Key Version Number (KVN)')
|
||||
del_key_parser.add_argument('--delete-related-objects', action='store_true',
|
||||
help='Delete not only the object but also its related objects')
|
||||
|
||||
@cmd2.with_argparser(del_key_parser)
|
||||
def do_delete_key(self, opts):
|
||||
"""Perform GlobalPlaform DELETE (Key) command.
|
||||
If both KID and KVN are specified, exactly one key is deleted. If only either of the two is
|
||||
specified, multiple matching keys may be deleted."""
|
||||
if opts.key_id is None and opts.key_ver is None:
|
||||
raise ValueError('At least one of KID or KVN must be specified')
|
||||
p2 = 0x80 if opts.delete_related_objects else 0x00
|
||||
cmd = ""
|
||||
if opts.key_id is not None:
|
||||
cmd += "d001%02x" % opts.key_id
|
||||
if opts.key_ver is not None:
|
||||
cmd += "d201%02x" % opts.key_ver
|
||||
self.delete(0x00, p2, cmd)
|
||||
|
||||
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
|
||||
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
load_parser = argparse.ArgumentParser()
|
||||
# we make this a required --optional argument now, so we can later have other sources for load data
|
||||
load_parser.add_argument('--from-file', required=True)
|
||||
|
||||
@cmd2.with_argparser(load_parser)
|
||||
def do_load(self, opts):
|
||||
"""Perform a GlobalPlatform LOAD command. We currently only support loading without DAP and
|
||||
without ciphering."""
|
||||
with open(opts.from_file, 'rb') as f:
|
||||
self.load(f)
|
||||
|
||||
def load(self, stream: io.RawIOBase, chunk_len:int = 240):
|
||||
# we might want to tune chunk_len based on the overhead of the used SCP?
|
||||
contents = stream.readall()
|
||||
# build TLV according to 11.6.2.3 / Table 11-58 for unencrypted case
|
||||
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
|
||||
# transfer this in vaious chunks to the card
|
||||
total_size = len(remainder)
|
||||
block_nr = 0
|
||||
while len(remainder):
|
||||
block = remainder[:chunk_len]
|
||||
remainder = remainder[chunk_len:]
|
||||
# build LOAD command APDU according to 11.6.2 / Table 11-56
|
||||
p1 = 0x80 if len(remainder) else 0x00
|
||||
p2 = block_nr % 256
|
||||
block_nr += 1
|
||||
cmd_hex = "80E8%02x%02x%02x%s" % (p1, p2, len(block), b2h(block))
|
||||
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_load now!" % (total_size, block_nr))
|
||||
|
||||
|
||||
est_scp02_parser = argparse.ArgumentParser()
|
||||
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True,
|
||||
help='Key Version Number (KVN)')
|
||||
est_scp02_parser.add_argument('--key-enc', type=is_hexstr, required=True,
|
||||
help='Secure Channel Encryption Key')
|
||||
est_scp02_parser.add_argument('--key-mac', type=is_hexstr, required=True,
|
||||
help='Secure Channel MAC Key')
|
||||
est_scp02_parser.add_argument('--key-dek', type=is_hexstr, required=True,
|
||||
help='Data Encryption Key')
|
||||
est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
|
||||
help='Hard-code the host challenge; default: random')
|
||||
est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
|
||||
help='Security Level. Default: 0x01 (C-MAC only)')
|
||||
|
||||
@cmd2.with_argparser(est_scp02_parser)
|
||||
def do_establish_scp02(self, opts):
|
||||
"""Establish a secure channel using the GlobalPlatform SCP02 protocol. It can be released
|
||||
again by using `release_scp`."""
|
||||
if self._cmd.lchan.scc.scp:
|
||||
self._cmd.poutput("Cannot establish SCP02 as this lchan already has a SCP instance!")
|
||||
return
|
||||
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
|
||||
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
|
||||
scp02 = SCP02(card_keys=kset)
|
||||
self._establish_scp(scp02, host_challenge, opts.security_level)
|
||||
|
||||
est_scp03_parser = deepcopy(est_scp02_parser)
|
||||
est_scp03_parser.add_argument('--s16-mode', action='store_true', help='S16 mode (S8 is default)')
|
||||
|
||||
@cmd2.with_argparser(est_scp03_parser)
|
||||
def do_establish_scp03(self, opts):
|
||||
"""Establish a secure channel using the GlobalPlatform SCP03 protocol. It can be released
|
||||
again by using `release_scp`."""
|
||||
if self._cmd.lchan.scc.scp:
|
||||
self._cmd.poutput("Cannot establish SCP03 as this lchan already has a SCP instance!")
|
||||
return
|
||||
s_mode = 16 if opts.s16_mode else 8
|
||||
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
|
||||
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
|
||||
scp03 = SCP03(card_keys=kset, s_mode = s_mode)
|
||||
self._establish_scp(scp03, host_challenge, opts.security_level)
|
||||
|
||||
def _establish_scp(self, scp, host_challenge, security_level):
|
||||
# perform the common functionality shared by SCP02 and SCP03 establishment
|
||||
init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
|
||||
init_update_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
|
||||
scp.parse_init_update_resp(h2b(init_update_resp))
|
||||
ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
|
||||
_ext_auth_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
|
||||
self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
|
||||
# store a reference to the SCP instance
|
||||
self._cmd.lchan.scc.scp = scp
|
||||
self._cmd.update_prompt()
|
||||
|
||||
|
||||
def do_release_scp(self, _opts):
|
||||
"""Release a previously establiehed secure channel."""
|
||||
if not self._cmd.lchan.scc.scp:
|
||||
self._cmd.poutput("Cannot release SCP as none is established")
|
||||
return
|
||||
self._cmd.lchan.scc.scp = None
|
||||
self._cmd.update_prompt()
|
||||
|
||||
|
||||
# Card Application of a Security Domain
|
||||
class CardApplicationSD(CardApplication):
|
||||
__intermediate = True
|
||||
def __init__(self, aid: str, name: str, desc: str):
|
||||
super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
|
||||
|
||||
# Card Application of Issuer Security Domain
|
||||
class CardApplicationISD(CardApplicationSD):
|
||||
# FIXME: ISD AID is not static, but could be different. One can select the empty
|
||||
# application using '00a4040000' and then parse the response FCI to get the ISD AID
|
||||
def __init__(self, aid='a000000003000000'):
|
||||
super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
|
||||
|
||||
#class CardProfileGlobalPlatform(CardProfile):
|
||||
# ORDER = 23
|
||||
#
|
||||
# def __init__(self, name='GlobalPlatform'):
|
||||
# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
|
||||
|
||||
|
||||
class GpCardKeyset:
|
||||
"""A single set of GlobalPlatform card keys and the associated KVN."""
|
||||
def __init__(self, kvn: int, enc: bytes, mac: bytes, dek: bytes):
|
||||
assert 0 < kvn < 256
|
||||
assert len(enc) == len(mac) == len(dek)
|
||||
self.kvn = kvn
|
||||
self.enc = enc
|
||||
self.mac = mac
|
||||
self.dek = dek
|
||||
|
||||
@classmethod
|
||||
def from_single_key(cls, kvn: int, base_key: bytes) -> 'GpCardKeyset':
|
||||
return cls(kvn, base_key, base_key, base_key)
|
||||
|
||||
def __str__(self):
|
||||
return "%s(KVN=%u, ENC=%s, MAC=%s, DEK=%s)" % (self.__class__.__name__,
|
||||
self.kvn, b2h(self.enc), b2h(self.mac), b2h(self.dek))
|
||||
|
||||
|
||||
def compute_kcv_des(key:bytes) -> bytes:
|
||||
# GP Card Spec B.6: For a DES key, the key check value is computed by encrypting 8 bytes, each with
|
||||
# value '00', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
|
||||
# result.
|
||||
plaintext = b'\x00' * 8
|
||||
cipher = DES3.new(key, DES.MODE_ECB)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
def compute_kcv_aes(key:bytes) -> bytes:
|
||||
# GP Card Spec B.6: For a AES key, the key check value is computed by encrypting 16 bytes, each with
|
||||
# value '01', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
|
||||
# result.
|
||||
plaintext = b'\x01' * 16
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
# dict is keyed by the string name of the KeyType enum above in this file
|
||||
KCV_CALCULATOR = {
|
||||
'aes': compute_kcv_aes,
|
||||
'des': compute_kcv_des,
|
||||
}
|
||||
|
||||
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
|
||||
"""Compute the KCV (Key Check Value) for given key type and key."""
|
||||
kcv_calculator = KCV_CALCULATOR.get(key_type)
|
||||
if not kcv_calculator:
|
||||
return None
|
||||
else:
|
||||
return kcv_calculator(key)[:3]
|
||||
@@ -1,534 +0,0 @@
|
||||
# Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
import logging
|
||||
from typing import Optional
|
||||
from Cryptodome.Cipher import DES3, DES
|
||||
from Cryptodome.Util.strxor import strxor
|
||||
from construct import Struct, Bytes, Int8ub, Int16ub, Const
|
||||
from construct import Optional as COptional
|
||||
from pySim.utils import b2h, bertlv_parse_len, bertlv_encode_len
|
||||
from pySim.secure_channel import SecureChannel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes:
|
||||
assert len(constant) == 2
|
||||
assert(counter >= 0 and counter <= 65535)
|
||||
assert len(base_key) == 16
|
||||
|
||||
derivation_data = constant + counter.to_bytes(2, 'big') + b'\x00' * 12
|
||||
cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8)
|
||||
return cipher.encrypt(derivation_data)
|
||||
|
||||
# TODO: resolve duplication with BspAlgoCryptAES128
|
||||
def pad80(s: bytes, BS=8) -> bytes:
|
||||
""" Pad bytestring s: add '\x80' and '\0'* so the result to be multiple of BS."""
|
||||
l = BS-1 - len(s) % BS
|
||||
return s + b'\x80' + b'\0'*l
|
||||
|
||||
# TODO: resolve duplication with BspAlgoCryptAES128
|
||||
def unpad80(padded: bytes) -> bytes:
|
||||
"""Remove the customary 80 00 00 ... padding used for AES."""
|
||||
# first remove any trailing zero bytes
|
||||
stripped = padded.rstrip(b'\0')
|
||||
# then remove the final 80
|
||||
assert stripped[-1] == 0x80
|
||||
return stripped[:-1]
|
||||
|
||||
class Scp02SessionKeys:
|
||||
"""A single set of GlobalPlatform session keys."""
|
||||
DERIV_CONST_CMAC = b'\x01\x01'
|
||||
DERIV_CONST_RMAC = b'\x01\x02'
|
||||
DERIV_CONST_ENC = b'\x01\x82'
|
||||
DERIV_CONST_DENC = b'\x01\x81'
|
||||
blocksize = 8
|
||||
|
||||
def calc_mac_1des(self, data: bytes, reset_icv: bool = False) -> bytes:
|
||||
"""Pad and calculate MAC according to B.1.2.2 - Single DES plus final 3DES"""
|
||||
e = DES.new(self.c_mac[:8], DES.MODE_ECB)
|
||||
d = DES.new(self.c_mac[8:], DES.MODE_ECB)
|
||||
padded_data = pad80(data, 8)
|
||||
q = len(padded_data) // 8
|
||||
icv = b'\x00' * 8 if reset_icv else self.icv
|
||||
h = icv
|
||||
for i in range(q):
|
||||
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
|
||||
h = d.decrypt(h)
|
||||
h = e.encrypt(h)
|
||||
logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
|
||||
if self.des_icv_enc:
|
||||
self.icv = self.des_icv_enc.encrypt(h)
|
||||
else:
|
||||
self.icv = h
|
||||
return h
|
||||
|
||||
def calc_mac_3des(self, data: bytes) -> bytes:
|
||||
e = DES3.new(self.enc, DES.MODE_ECB)
|
||||
padded_data = pad80(data, 8)
|
||||
q = len(padded_data) // 8
|
||||
h = b'\x00' * 8
|
||||
for i in range(q):
|
||||
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
|
||||
logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
|
||||
return h
|
||||
|
||||
def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True):
|
||||
self.icv = None
|
||||
self.counter = counter
|
||||
self.card_keys = card_keys
|
||||
self.c_mac = scp02_key_derivation(self.DERIV_CONST_CMAC, self.counter, card_keys.mac)
|
||||
self.r_mac = scp02_key_derivation(self.DERIV_CONST_RMAC, self.counter, card_keys.mac)
|
||||
self.enc = scp02_key_derivation(self.DERIV_CONST_ENC, self.counter, card_keys.enc)
|
||||
self.data_enc = scp02_key_derivation(self.DERIV_CONST_DENC, self.counter, card_keys.dek)
|
||||
self.des_icv_enc = DES.new(self.c_mac[:8], DES.MODE_ECB) if icv_encrypt else None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s(CTR=%u, ICV=%s, ENC=%s, D-ENC=%s, MAC-C=%s, MAC-R=%s)" % (
|
||||
self.__class__.__name__, self.counter, b2h(self.icv) if self.icv else "None",
|
||||
b2h(self.enc), b2h(self.data_enc), b2h(self.c_mac), b2h(self.r_mac))
|
||||
|
||||
INS_INIT_UPDATE = 0x50
|
||||
INS_EXT_AUTH = 0x82
|
||||
CLA_SM = 0x04
|
||||
|
||||
class SCP(SecureChannel, abc.ABC):
|
||||
"""Abstract base class containing some common interface + functionality for SCP protocols."""
|
||||
def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0):
|
||||
if hasattr(self, 'kvn_range'):
|
||||
if not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1):
|
||||
raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' %
|
||||
(self.__class__.__name__, self.kvn_range[0], self.kvn_range[1]))
|
||||
self.lchan_nr = lchan_nr
|
||||
self.card_keys = card_keys
|
||||
self.sk = None
|
||||
self.mac_on_unmodified = False
|
||||
self.security_level = 0x00
|
||||
|
||||
@property
|
||||
def do_cmac(self) -> bool:
|
||||
"""Should we perform C-MAC?"""
|
||||
return self.security_level & 0x01
|
||||
|
||||
@property
|
||||
def do_rmac(self) -> bool:
|
||||
"""Should we perform R-MAC?"""
|
||||
return self.security_level & 0x10
|
||||
|
||||
@property
|
||||
def do_cenc(self) -> bool:
|
||||
"""Should we perform C-ENC?"""
|
||||
return self.security_level & 0x02
|
||||
|
||||
@property
|
||||
def do_renc(self) -> bool:
|
||||
"""Should we perform R-ENC?"""
|
||||
return self.security_level & 0x20
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
|
||||
|
||||
def _cla(self, sm: bool = False, b8: bool = True) -> int:
|
||||
ret = 0x80 if b8 else 0x00
|
||||
if sm:
|
||||
ret = ret | CLA_SM
|
||||
return ret + self.lchan_nr
|
||||
|
||||
def wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
|
||||
# Generic handling of GlobalPlatform SCP, implements SecureChannel.wrap_cmd_apdu
|
||||
# only protect those APDUs that actually are global platform commands
|
||||
if apdu[0] & 0x80:
|
||||
return self._wrap_cmd_apdu(apdu, *args, **kwargs)
|
||||
return apdu
|
||||
|
||||
@abc.abstractmethod
|
||||
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
|
||||
"""Method implementation to be provided by derived class."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def gen_init_update_apdu(self, host_challenge: Optional[bytes]) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def parse_init_update_resp(self, resp_bin: bytes):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
||||
pass
|
||||
|
||||
def encrypt_key(self, key: bytes) -> bytes:
|
||||
"""Encrypt a key with the DEK."""
|
||||
num_pad = len(key) % self.sk.blocksize
|
||||
if num_pad:
|
||||
return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad)
|
||||
return self.dek_encrypt(key)
|
||||
|
||||
def decrypt_key(self, encrypted_key:bytes) -> bytes:
|
||||
"""Decrypt a key with the DEK."""
|
||||
if len(encrypted_key) % self.sk.blocksize:
|
||||
# If the length of the Key Component Block is not a multiple of the block size of the encryption #
|
||||
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that the key
|
||||
# component value was right-padded prior to encryption and that the Key Component Block was
|
||||
# formatted as described in Table 11-70. In this case, the first byte(s) of the Key Component
|
||||
# Block provides the actual length of the key component value, which allows recovering the
|
||||
# clear-text key component value after decryption of the encrypted key component value and removal
|
||||
# of padding bytes.
|
||||
decrypted = self.dek_decrypt(encrypted_key)
|
||||
key_len, remainder = bertlv_parse_len(decrypted)
|
||||
return remainder[:key_len]
|
||||
else:
|
||||
# If the length of the Key Component Block is a multiple of the block size of the encryption
|
||||
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding
|
||||
# bytes were added before encrypting the key component value and that the Key Component Block is
|
||||
# only composed of the encrypted key component value (as shown in Table 11-71). In this case, the
|
||||
# clear-text key component value is simply recovered by decrypting the Key Component Block.
|
||||
return self.dek_decrypt(encrypted_key)
|
||||
|
||||
@abc.abstractmethod
|
||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
||||
pass
|
||||
|
||||
|
||||
class SCP02(SCP):
|
||||
"""An instance of the GlobalPlatform SCP02 secure channel protocol."""
|
||||
|
||||
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x02'),
|
||||
'seq_counter'/Int16ub, 'card_challenge'/Bytes(6), 'card_cryptogram'/Bytes(8))
|
||||
kvn_range = [0x20, 0x2f]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.overhead = 8
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||
cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
||||
cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
|
||||
return cipher.decrypt(ciphertext)
|
||||
|
||||
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
|
||||
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
|
||||
self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge)
|
||||
self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge)
|
||||
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
|
||||
|
||||
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
|
||||
"""Generate INITIALIZE UPDATE APDU."""
|
||||
self.host_challenge = host_challenge
|
||||
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
|
||||
|
||||
def parse_init_update_resp(self, resp_bin: bytes):
|
||||
"""Parse response to INITIALZIE UPDATE."""
|
||||
resp = self.constr_iur.parse(resp_bin)
|
||||
self.card_challenge = resp['card_challenge']
|
||||
self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys)
|
||||
logger.debug(self.sk)
|
||||
self._compute_cryptograms(self.card_challenge, self.host_challenge)
|
||||
if self.card_cryptogram != resp['card_cryptogram']:
|
||||
raise ValueError("card cryptogram doesn't match")
|
||||
|
||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
||||
"""Generate EXTERNAL AUTHENTICATE APDU."""
|
||||
if security_level & 0xf0:
|
||||
raise NotImplementedError('R-MAC/R-ENC for SCP02 not implemented yet.')
|
||||
self.security_level = security_level
|
||||
if self.mac_on_unmodified:
|
||||
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, 8])
|
||||
else:
|
||||
header = bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16])
|
||||
#return self.wrap_cmd_apdu(header + self.host_cryptogram)
|
||||
mac = self.sk.calc_mac_1des(header + self.host_cryptogram, True)
|
||||
return bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16]) + self.host_cryptogram + mac
|
||||
|
||||
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
|
||||
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
|
||||
lc = len(apdu) - 5
|
||||
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
|
||||
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
|
||||
|
||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
||||
|
||||
cla = apdu[0]
|
||||
b8 = cla & 0x80
|
||||
if cla & 0x03 or cla & CLA_SM:
|
||||
# nonzero logical channel in APDU, check that are the same
|
||||
assert cla == self._cla(False, b8), "CLA mismatch"
|
||||
# CLA without log. channel can be 80 or 00 only
|
||||
if self.do_cmac:
|
||||
if self.mac_on_unmodified:
|
||||
mlc = lc
|
||||
clac = cla
|
||||
else: # CMAC on modified APDU
|
||||
mlc = lc + 8
|
||||
clac = cla | CLA_SM
|
||||
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
|
||||
if self.do_cenc:
|
||||
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
|
||||
data = k.encrypt(pad80(apdu[5:], 8))
|
||||
lc = len(data)
|
||||
else:
|
||||
data = apdu[5:]
|
||||
lc += 8
|
||||
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
|
||||
return apdu
|
||||
|
||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
||||
# TODO: Implement R-MAC / R-ENC
|
||||
return rsp_apdu
|
||||
|
||||
|
||||
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Hash import CMAC
|
||||
|
||||
def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Optional[int] = None) -> bytes:
|
||||
"""SCP03 Key Derivation Function as specified in Annex D 4.1.5."""
|
||||
# Data derivation shall use KDF in counter mode as specified in NIST SP 800-108 ([NIST 800-108]). The PRF
|
||||
# used in the KDF shall be CMAC as specified in [NIST 800-38B], used with full 16-byte output length.
|
||||
def prf(key: bytes, data:bytes):
|
||||
return CMAC.new(key, data, AES).digest()
|
||||
|
||||
if l is None:
|
||||
l = len(base_key) * 8
|
||||
|
||||
logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
|
||||
output_len = l // 8
|
||||
# SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the
|
||||
# existing Cryptodome.Protocol.KDF.SP800_108_Counter function :(
|
||||
# A 12-byte “label” consisting of 11 bytes with value '00' followed by a 1-byte derivation constant
|
||||
assert len(constant) == 1
|
||||
label = b'\x00' *11 + constant
|
||||
i = 1
|
||||
dk = b''
|
||||
while len(dk) < output_len:
|
||||
# 12B label, 1B separation, 2B L, 1B i, Context
|
||||
info = label + b'\x00' + l.to_bytes(2, 'big') + bytes([i]) + context
|
||||
dk += prf(base_key, info)
|
||||
i += 1
|
||||
if i > 0xffff:
|
||||
raise ValueError("Overflow in SP800 108 counter")
|
||||
return dk[:output_len]
|
||||
|
||||
|
||||
class Scp03SessionKeys:
|
||||
# GPC 2.3 Amendment D v1.2 Section 4.1.5 Table 4-1
|
||||
DERIV_CONST_AUTH_CGRAM_CARD = b'\x00'
|
||||
DERIV_CONST_AUTH_CGRAM_HOST = b'\x01'
|
||||
DERIV_CONST_CARD_CHLG_GEN = b'\x02'
|
||||
DERIV_CONST_KDERIV_S_ENC = b'\x04'
|
||||
DERIV_CONST_KDERIV_S_MAC = b'\x06'
|
||||
DERIV_CONST_KDERIV_S_RMAC = b'\x07'
|
||||
blocksize = 16
|
||||
|
||||
def __init__(self, card_keys: 'GpCardKeyset', host_challenge: bytes, card_challenge: bytes):
|
||||
# GPC 2.3 Amendment D v1.2 Section 6.2.1
|
||||
context = host_challenge + card_challenge
|
||||
self.s_enc = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_ENC, context, card_keys.enc)
|
||||
self.s_mac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_MAC, context, card_keys.mac)
|
||||
self.s_rmac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_RMAC, context, card_keys.mac)
|
||||
|
||||
|
||||
# The first MAC chaining value is set to 16 bytes '00'
|
||||
self.mac_chaining_value = b'\x00' * 16
|
||||
# The encryption 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
|
||||
|
||||
def parse_init_update_resp(self, resp_bin: bytes):
|
||||
"""Parse response to INITIALIZE UPDATE."""
|
||||
if len(resp_bin) not in [10+3+8+8, 10+3+16+16, 10+3+8+8+3, 10+3+16+16+3]:
|
||||
raise ValueError('Invalid length of Initialize Update Response')
|
||||
resp = self.constr_iur.parse(resp_bin, s_mode=self.s_mode)
|
||||
self.card_challenge = resp['card_challenge']
|
||||
self.i_param = resp['i_param']
|
||||
# derive session keys and compute cryptograms
|
||||
self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge)
|
||||
logger.debug(self.sk)
|
||||
self._compute_cryptograms()
|
||||
# verify computed cryptogram matches received cryptogram
|
||||
if self.card_cryptogram != resp['card_cryptogram']:
|
||||
raise ValueError("card cryptogram doesn't match")
|
||||
|
||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
||||
"""Generate EXTERNAL AUTHENTICATE APDU."""
|
||||
self.security_level = security_level
|
||||
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, self.s_mode])
|
||||
# bypass encryption for EXTERNAL AUTHENTICATE
|
||||
return self.wrap_cmd_apdu(header + self.host_cryptogram, skip_cenc=True)
|
||||
|
||||
def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
|
||||
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
|
||||
cla = apdu[0]
|
||||
ins = apdu[1]
|
||||
p1 = apdu[2]
|
||||
p2 = apdu[3]
|
||||
lc = apdu[4]
|
||||
assert lc == len(apdu) - 5
|
||||
cmd_data = apdu[5:]
|
||||
|
||||
if self.do_cenc and not skip_cenc:
|
||||
assert self.do_cmac
|
||||
if lc == 0:
|
||||
# No encryption shall be applied to a command where there is no command data field. In this
|
||||
# case, the encryption counter shall still be incremented
|
||||
self.sk.block_nr += 1
|
||||
else:
|
||||
# data shall be padded as defined in [GPCS] section B.2.3
|
||||
padded_data = pad80(cmd_data, 16)
|
||||
lc = len(padded_data)
|
||||
if lc >= 256:
|
||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending padding' % (lc))
|
||||
# perform AES-CBC with ICV + S_ENC
|
||||
cmd_data = self.sk._encrypt(padded_data)
|
||||
|
||||
if self.do_cmac:
|
||||
# The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
|
||||
# mode) to indicate the inclusion of the C-MAC in the data field of the command message.
|
||||
mlc = lc + self.s_mode
|
||||
if mlc >= 256:
|
||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
|
||||
# The class byte shall be modified for the generation or verification of the C-MAC: The logical
|
||||
# channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
|
||||
# GlobalPlatform proprietary secure messaging.
|
||||
mcla = (cla & 0xF0) | CLA_SM
|
||||
mapdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
|
||||
cmac = self.sk.calc_cmac(mapdu)
|
||||
mapdu += cmac[:self.s_mode]
|
||||
|
||||
return mapdu
|
||||
|
||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
||||
# No R-MAC shall be generated and no protection shall be applied to a response that includes an error
|
||||
# status word: in this case only the status word shall be returned in the response. All status words
|
||||
# except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status
|
||||
# words.
|
||||
logger.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu)
|
||||
if not self.do_rmac:
|
||||
assert not self.do_renc
|
||||
return rsp_apdu
|
||||
|
||||
if sw != b'\x90\x00' and sw[0] not in [0x62, 0x63]:
|
||||
return rsp_apdu
|
||||
response_data = rsp_apdu[:-self.s_mode]
|
||||
rmac = rsp_apdu[-self.s_mode:]
|
||||
rmac_exp = self.sk.calc_rmac(response_data + sw)[:self.s_mode]
|
||||
if rmac != rmac_exp:
|
||||
raise ValueError("R-MAC value not matching: received: %s, computed: %s" % (rmac, rmac_exp))
|
||||
|
||||
if self.do_renc:
|
||||
# decrypt response data
|
||||
decrypted = self.sk._decrypt(response_data)
|
||||
logger.debug("decrypted: %s", b2h(decrypted))
|
||||
# remove padding
|
||||
response_data = unpad80(decrypted)
|
||||
logger.debug("response_data: %s", b2h(response_data))
|
||||
|
||||
return response_data
|
||||
@@ -1,10 +1,13 @@
|
||||
# -*- 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>
|
||||
@@ -25,13 +28,16 @@ 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 Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
|
||||
from pySim.construct import *
|
||||
import enum
|
||||
|
||||
from pySim.profile import CardProfileAddon
|
||||
from pySim.filesystem import *
|
||||
import pySim.ts_51_011
|
||||
|
||||
######################################################################
|
||||
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
|
||||
@@ -71,15 +77,15 @@ class PlConfAdapter(Adapter):
|
||||
num = int(obj) & 0x7
|
||||
if num == 0:
|
||||
return 'None'
|
||||
if num == 1:
|
||||
elif num == 1:
|
||||
return 4
|
||||
if num == 2:
|
||||
elif num == 2:
|
||||
return 3
|
||||
if num == 3:
|
||||
elif num == 3:
|
||||
return 2
|
||||
if num == 4:
|
||||
elif num == 4:
|
||||
return 1
|
||||
if num == 5:
|
||||
elif num == 5:
|
||||
return 0
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
@@ -88,13 +94,13 @@ class PlConfAdapter(Adapter):
|
||||
obj = int(obj)
|
||||
if obj == 4:
|
||||
return 1
|
||||
if obj == 3:
|
||||
elif obj == 3:
|
||||
return 2
|
||||
if obj == 2:
|
||||
elif obj == 2:
|
||||
return 3
|
||||
if obj == 1:
|
||||
elif obj == 1:
|
||||
return 4
|
||||
if obj == 0:
|
||||
elif obj == 0:
|
||||
return 5
|
||||
|
||||
|
||||
@@ -105,19 +111,19 @@ class PlCallAdapter(Adapter):
|
||||
num = int(obj) & 0x7
|
||||
if num == 0:
|
||||
return 'None'
|
||||
if num == 1:
|
||||
elif num == 1:
|
||||
return 4
|
||||
if num == 2:
|
||||
elif num == 2:
|
||||
return 3
|
||||
if num == 3:
|
||||
elif num == 3:
|
||||
return 2
|
||||
if num == 4:
|
||||
elif num == 4:
|
||||
return 1
|
||||
if num == 5:
|
||||
elif num == 5:
|
||||
return 0
|
||||
if num == 6:
|
||||
elif num == 6:
|
||||
return 'B'
|
||||
if num == 7:
|
||||
elif num == 7:
|
||||
return 'A'
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
@@ -125,17 +131,17 @@ class PlCallAdapter(Adapter):
|
||||
return 0
|
||||
if obj == 4:
|
||||
return 1
|
||||
if obj == 3:
|
||||
elif obj == 3:
|
||||
return 2
|
||||
if obj == 2:
|
||||
elif obj == 2:
|
||||
return 3
|
||||
if obj == 1:
|
||||
elif obj == 1:
|
||||
return 4
|
||||
if obj == 0:
|
||||
elif obj == 0:
|
||||
return 5
|
||||
if obj == 'B':
|
||||
elif obj == 'B':
|
||||
return 6
|
||||
if obj == 'A':
|
||||
elif obj == 'A':
|
||||
return 7
|
||||
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ telecom-related protocol traces over UDP.
|
||||
#
|
||||
|
||||
import socket
|
||||
from typing import List, Dict, Optional
|
||||
from construct import Optional as COptional
|
||||
from construct import Int8ub, Int8sb, Int32ub, BitStruct, Enum, GreedyBytes, Struct, Switch
|
||||
from construct import this, PaddedString
|
||||
from construct import *
|
||||
from pySim.construct import *
|
||||
|
||||
# The root definition of GSMTAP can be found at
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from construct import GreedyBytes, GreedyString
|
||||
from construct import *
|
||||
from pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# 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,
|
||||
@@ -5,8 +10,6 @@ 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
|
||||
|
||||
@@ -753,7 +753,7 @@ class GrcardSim(SimCard):
|
||||
|
||||
# Set the Ki using proprietary command
|
||||
pdu = '80d4020010' + p['ki']
|
||||
data, sw = self._scc.send_apdu(pdu)
|
||||
data, sw = self._scc._tp.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.send_apdu_checksw(
|
||||
data, sw = self._scc._tp.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.send_apdu_checksw("0099000033" + par)
|
||||
data, sw = self._scc._tp.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.send_apdu(pdu)
|
||||
data, sw = self._scc._tp.send_apdu(pdu)
|
||||
|
||||
# authenticate as ADM (enough to write file, and can set PINs)
|
||||
|
||||
|
||||
36
pySim/ota.py
36
pySim/ota.py
@@ -15,16 +15,14 @@
|
||||
# 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 construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
|
||||
from construct import Flag, Padding, Switch, this
|
||||
|
||||
from pySim.construct import *
|
||||
from pySim.utils import b2h
|
||||
from pySim.sms import UserDataHeader
|
||||
|
||||
# ETS TS 102 225 gives the general command structure and the dialects for CAT_TP, TCP/IP and HTTPS
|
||||
# 3GPP TS 31.115 gives the dialects for SMS-PP, SMS-CB, USSD and HTTP
|
||||
@@ -114,12 +112,12 @@ class OtaKeyset:
|
||||
@property
|
||||
def auth(self):
|
||||
"""Return an instance of the matching OtaAlgoAuth."""
|
||||
return OtaAlgoAuth.from_keyset(self)
|
||||
return OtaAlgoAuth.fromKeyset(self)
|
||||
|
||||
@property
|
||||
def crypt(self):
|
||||
"""Return an instance of the matching OtaAlgoCrypt."""
|
||||
return OtaAlgoCrypt.from_keyset(self)
|
||||
return OtaAlgoCrypt.fromKeyset(self)
|
||||
|
||||
class OtaCheckError(Exception):
|
||||
pass
|
||||
@@ -130,24 +128,26 @@ class OtaDialect(abc.ABC):
|
||||
def _compute_sig_len(self, spi:SPI):
|
||||
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
|
||||
return 0
|
||||
if spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||
elif spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||
return 4
|
||||
if spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
|
||||
elif spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
|
||||
# TODO: this is not entirely correct, as in AES case it could be 4 or 8
|
||||
return 8
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
else:
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
|
||||
@abc.abstractmethod
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, apdu: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
|
||||
def decode_resp(self, otak: OtaKeyset, 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 +190,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(padded_data)
|
||||
return self._encrypt(data)
|
||||
|
||||
def decrypt(self, data:bytes) -> bytes:
|
||||
"""Decrypt given input bytes using the key material given in constructor."""
|
||||
@@ -199,13 +199,15 @@ 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 from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
|
||||
def fromKeyset(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:
|
||||
@@ -237,7 +239,7 @@ class OtaAlgoAuth(OtaAlgo, abc.ABC):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
|
||||
def fromKeyset(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:
|
||||
@@ -397,7 +399,7 @@ class OtaDialectSms(OtaDialect):
|
||||
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
|
||||
if data[0] != 0x02:
|
||||
raise ValueError('Unexpected UDL=0x%02x' % data[0])
|
||||
udhd, remainder = UserDataHeader.from_bytes(data)
|
||||
udhd, remainder = UserDataHeader.fromBytes(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
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
# 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
|
||||
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardApplication, interpret_sw
|
||||
from pySim.utils import all_subclasses
|
||||
|
||||
def _mf_select_test(scc: SimCardCommands,
|
||||
cla_byte: str, sel_ctrl: str,
|
||||
@@ -166,7 +166,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,6 +186,7 @@ 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
|
||||
@@ -193,3 +194,4 @@ 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,7 +19,7 @@
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from pySim.utils import h2b, i2h, is_hex, bertlv_parse_one, Hexstr
|
||||
from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
|
||||
from pySim.exceptions import *
|
||||
from pySim.filesystem import *
|
||||
|
||||
@@ -29,10 +29,11 @@ def lchan_nr_from_cla(cla: int) -> int:
|
||||
if cla >> 4 in [0x0, 0xA, 0x8]:
|
||||
# Table 10.3
|
||||
return cla & 0x03
|
||||
if cla & 0xD0 in [0x40, 0xC0]:
|
||||
elif cla & 0xD0 in [0x40, 0xC0]:
|
||||
# Table 10.4a
|
||||
return 4 + (cla & 0x0F)
|
||||
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
|
||||
else:
|
||||
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
|
||||
|
||||
class RuntimeState:
|
||||
"""Represent the runtime state of a session with a card."""
|
||||
@@ -115,7 +116,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))
|
||||
@@ -263,20 +264,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])) from swm
|
||||
raise(swm)
|
||||
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
|
||||
|
||||
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:
|
||||
@@ -303,9 +304,6 @@ 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:
|
||||
@@ -339,13 +337,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)
|
||||
|
||||
@@ -393,7 +391,7 @@ 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):
|
||||
@@ -523,8 +521,8 @@ class RuntimeLchan:
|
||||
"""
|
||||
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))
|
||||
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):
|
||||
@@ -543,3 +541,6 @@ class RuntimeLchan:
|
||||
if cmd_app and self.selected_file.shell_commands:
|
||||
for c in self.selected_file.shell_commands:
|
||||
cmd_app.unregister_command_set(c)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Generic code related to Secure Channel processing
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
from pySim.utils import b2h, h2b, ResTuple, Hexstr
|
||||
|
||||
class SecureChannel(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
|
||||
"""Wrap Command APDU according to specific Secure Channel Protocol."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
||||
"""UnWrap Response-APDU according to specific Secure Channel Protocol."""
|
||||
pass
|
||||
|
||||
def send_apdu_wrapper(self, send_fn: callable, pdu: Hexstr, *args, **kwargs) -> ResTuple:
|
||||
"""Wrapper function to wrap command APDU and unwrap repsonse APDU around send_apdu callable."""
|
||||
pdu_wrapped = b2h(self.wrap_cmd_apdu(h2b(pdu)))
|
||||
res, sw = send_fn(pdu_wrapped, *args, **kwargs)
|
||||
res_unwrapped = b2h(self.unwrap_rsp_apdu(h2b(sw), h2b(res)))
|
||||
return res_unwrapped, sw
|
||||
57
pySim/sms.py
57
pySim/sms.py
@@ -20,7 +20,7 @@
|
||||
import typing
|
||||
import abc
|
||||
from bidict import bidict
|
||||
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger
|
||||
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger, Flag
|
||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
||||
from construct import Prefixed, GreedyRange, GreedyBytes
|
||||
|
||||
@@ -51,13 +51,13 @@ class UserDataHeader:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
|
||||
def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
|
||||
if isinstance(inb, str):
|
||||
inb = h2b(inb)
|
||||
res = cls._construct.parse(inb)
|
||||
return cls(res['ies']), res['data']
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
def toBytes(self) -> bytes:
|
||||
return self._construct.build({'ies':self.ies, 'data':b''})
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class AddressField:
|
||||
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
|
||||
def fromBytes(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 +129,16 @@ class AddressField:
|
||||
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
|
||||
|
||||
@classmethod
|
||||
def from_smpp(cls, addr, ton, npi) -> 'AddressField':
|
||||
def fromSmpp(cls, addr, ton, npi) -> 'AddressField':
|
||||
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
|
||||
# return the resulting instance
|
||||
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
|
||||
|
||||
def to_smpp(self):
|
||||
def toSmpp(self):
|
||||
"""Return smpp.pdo.*.source,dest}_addr_{,ton,npi} attributes for given AddressField."""
|
||||
return (self.digits, self.smpp_map_ton.inverse[self.ton], self.smpp_map_npi.inverse[self.npi])
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
def toBytes(self) -> bytes:
|
||||
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
||||
num_digits = len(self.digits)
|
||||
if num_digits % 2:
|
||||
@@ -185,12 +185,13 @@ 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 from_bytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
|
||||
def fromBytes(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.from_bytes(inb[1:])
|
||||
oa, remainder = AddressField.fromBytes(inb[1:])
|
||||
d['tp_oa'] = oa
|
||||
offset = 0
|
||||
d['tp_pid'] = remainder[offset]
|
||||
@@ -205,7 +206,7 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
d['tp_ud'] = remainder[offset:]
|
||||
return cls(**d)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
def toBytes(self) -> bytes:
|
||||
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
|
||||
outb = bytearray()
|
||||
d = {
|
||||
@@ -214,7 +215,7 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
}
|
||||
flags = SMS_DELIVER.flags_construct.build(d)
|
||||
outb.extend(flags)
|
||||
outb.extend(self.tp_oa.to_bytes())
|
||||
outb.extend(self.tp_oa.toBytes())
|
||||
outb.append(self.tp_pid)
|
||||
outb.append(self.tp_dcs)
|
||||
outb.extend(self.tp_scts)
|
||||
@@ -224,18 +225,18 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
return outb
|
||||
|
||||
@classmethod
|
||||
def from_smpp(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||
def fromSmpp(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||
"""Construct a SMS_DELIVER instance from the deliver format used by smpp.pdu."""
|
||||
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
||||
return cls.from_smpp_submit(smpp_pdu)
|
||||
return cls.fromSmppSubmit(smpp_pdu)
|
||||
else:
|
||||
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
||||
|
||||
@classmethod
|
||||
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
|
||||
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
||||
tp_oa = AddressField.from_smpp(smpp_pdu.params['source_addr'],
|
||||
tp_oa = AddressField.fromSmpp(smpp_pdu.params['source_addr'],
|
||||
smpp_pdu.params['source_addr_ton'],
|
||||
smpp_pdu.params['source_addr_npi'])
|
||||
tp_ud = smpp_pdu.params['short_message']
|
||||
@@ -275,7 +276,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 from_bytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
|
||||
def fromBytes(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):
|
||||
@@ -284,7 +285,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
offset += 1
|
||||
d['tp_mr']= inb[offset]
|
||||
offset += 1
|
||||
da, remainder = AddressField.from_bytes(inb[2:])
|
||||
da, remainder = AddressField.fromBytes(inb[2:])
|
||||
d['tp_da'] = da
|
||||
|
||||
offset = 0
|
||||
@@ -302,10 +303,12 @@ 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]
|
||||
@@ -313,7 +316,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
d['tp_ud'] = remainder[offset:]
|
||||
return cls(**d)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
def toBytes(self) -> bytes:
|
||||
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
|
||||
outb = bytearray()
|
||||
d = {
|
||||
@@ -323,7 +326,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.to_bytes())
|
||||
outb.extend(self.tp_da.toBytes())
|
||||
outb.append(self.tp_pid)
|
||||
outb.append(self.tp_dcs)
|
||||
if self.tp_vpf != 'none':
|
||||
@@ -333,20 +336,20 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
return outb
|
||||
|
||||
@classmethod
|
||||
def from_smpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||
def fromSmpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
|
||||
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
||||
return cls.from_smpp_submit(smpp_pdu)
|
||||
return cls.fromSmppSubmit(smpp_pdu)
|
||||
else:
|
||||
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
||||
|
||||
@classmethod
|
||||
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
|
||||
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
||||
tp_da = AddressField.from_smpp(smpp_pdu.params['destination_addr'],
|
||||
smpp_pdu.params['dest_addr_ton'],
|
||||
smpp_pdu.params['dest_addr_npi'])
|
||||
tp_da = AddressField.fromSmpp(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:
|
||||
@@ -367,7 +370,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
||||
}
|
||||
return cls(**d)
|
||||
|
||||
def to_smpp(self) -> pdu_types.PDU:
|
||||
def toSmpp(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)
|
||||
@@ -379,7 +382,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.to_smpp()
|
||||
(daddr, ton, npi) = self.tp_da.toSmpp()
|
||||
return operations.SubmitSM(service_type='',
|
||||
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
|
||||
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
|
||||
|
||||
@@ -17,15 +17,14 @@ 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 struct import unpack
|
||||
from construct import FlagsEnum, Byte, Struct, Int8ub, Bytes, Mapping, Enum, Padding, BitsInteger
|
||||
from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange
|
||||
from construct import Optional as COptional
|
||||
|
||||
from pytlv.TLV import *
|
||||
from struct import pack, unpack
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.runtime import RuntimeState
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.construct import *
|
||||
from construct import *
|
||||
import pySim
|
||||
|
||||
key_type2str = {
|
||||
@@ -71,7 +70,7 @@ class EF_PIN(TransparentEF):
|
||||
'attempts_remaining'/Int8ub,
|
||||
'maximum_attempts'/Int8ub,
|
||||
'pin'/HexAdapter(Rpad(Bytes(8))),
|
||||
'puk'/COptional(PukStruct))
|
||||
'puk'/Optional(PukStruct))
|
||||
|
||||
|
||||
class EF_MILENAGE_CFG(TransparentEF):
|
||||
|
||||
72
pySim/tlv.py
72
pySim/tlv.py
@@ -16,18 +16,21 @@
|
||||
# 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 inspect
|
||||
import abc
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
|
||||
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.utils import dgi_parse_tag_raw, dgi_parse_len, dgi_encode_tag, dgi_encode_len
|
||||
|
||||
from pySim.construct import build_construct, parse_construct
|
||||
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)
|
||||
@@ -37,9 +40,9 @@ class TlvMeta(abc.ABCMeta):
|
||||
"""Metaclass which we use to set some class variables at the time of defining a subclass.
|
||||
This allows us to create subclasses for each TLV/IE type, where the class represents fixed
|
||||
parameters like the tag/type and instances of it represent the actual TLV data."""
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
#print("TlvMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
|
||||
x = super().__new__(mcs, name, bases, namespace)
|
||||
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))
|
||||
@@ -60,9 +63,9 @@ class TlvCollectionMeta(abc.ABCMeta):
|
||||
"""Metaclass which we use to set some class variables at the time of defining a subclass.
|
||||
This allows us to create subclasses for each Collection type, where the class represents fixed
|
||||
parameters like the nested IE classes and instances of it represent the actual TLV data."""
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
#print("TlvCollectionMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
|
||||
x = super().__new__(mcs, name, bases, namespace)
|
||||
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
|
||||
@@ -83,7 +86,7 @@ class Transcodable(abc.ABC):
|
||||
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 is None:
|
||||
if self.decoded == None:
|
||||
do = b''
|
||||
elif self._construct:
|
||||
do = build_construct(self._construct, self.decoded, context)
|
||||
@@ -164,7 +167,10 @@ class IE(Transcodable, metaclass=TlvMeta):
|
||||
|
||||
def is_constructed(self):
|
||||
"""Is this IE constructed by further nested IEs?"""
|
||||
return bool(len(self.children) > 0)
|
||||
if len(self.children):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@abc.abstractmethod
|
||||
def to_ie(self, context: dict = {}) -> bytes:
|
||||
@@ -193,6 +199,9 @@ class IE(Transcodable, metaclass=TlvMeta):
|
||||
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
|
||||
@@ -245,6 +254,9 @@ class TLV_IE(IE):
|
||||
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)
|
||||
@@ -290,24 +302,6 @@ class COMPR_TLV_IE(TLV_IE):
|
||||
return bertlv_encode_len(len(val))
|
||||
|
||||
|
||||
class DGI_TLV_IE(TLV_IE):
|
||||
"""TLV_IE formated as GlobalPlatform Systems Scripting Language Specification v1.1.0 Annex B."""
|
||||
|
||||
@classmethod
|
||||
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
return dgi_parse_tag_raw(do)
|
||||
|
||||
@classmethod
|
||||
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
|
||||
return dgi_parse_len(do)
|
||||
|
||||
def _encode_tag(self) -> bytes:
|
||||
return dgi_encode_tag(self._compute_tag())
|
||||
|
||||
def _encode_len(self, val: bytes) -> bytes:
|
||||
return dgi_encode_len(len(val))
|
||||
|
||||
|
||||
class TLV_IE_Collection(metaclass=TlvCollectionMeta):
|
||||
# we specify the metaclass so any downstream subclasses will automatically use it
|
||||
"""A TLV_IE_Collection consists of multiple TLV_IE classes identified by their tags.
|
||||
@@ -364,14 +358,14 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
|
||||
while len(remainder):
|
||||
context['siblings'] = res
|
||||
# obtain the tag at the start of the remainder
|
||||
tag, _r = first._parse_tag_raw(remainder)
|
||||
if tag is None:
|
||||
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)
|
||||
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
|
||||
@@ -382,7 +376,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
|
||||
cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
|
||||
# create an instance and parse accordingly
|
||||
inst = cls()
|
||||
_dec, remainder = inst.from_tlv(remainder, context=context)
|
||||
dec, remainder = inst.from_tlv(remainder, context=context)
|
||||
res.append(inst)
|
||||
self.children = res
|
||||
return res
|
||||
@@ -443,7 +437,7 @@ def flatten_dict_lists(inp):
|
||||
return True
|
||||
|
||||
def are_elements_unique(lod):
|
||||
set_of_keys = {list(x.keys())[0] for x in lod}
|
||||
set_of_keys = set([list(x.keys())[0] for x in lod])
|
||||
return len(lod) == len(set_of_keys)
|
||||
|
||||
if isinstance(inp, list):
|
||||
@@ -455,10 +449,10 @@ def flatten_dict_lists(inp):
|
||||
newdict[key] = e[key]
|
||||
inp = newdict
|
||||
# process result as any native dict
|
||||
return {k:flatten_dict_lists(v) for k,v in inp.items()}
|
||||
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(v) for k,v in inp.items()}
|
||||
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
|
||||
else:
|
||||
return inp
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Optional, Tuple
|
||||
from construct import Construct
|
||||
|
||||
from pySim.exceptions import *
|
||||
from pySim.construct import filter_dict
|
||||
from pySim.utils import sw_match, b2h, h2b, i2h, Hexstr, SwHexstr, SwMatchstr, ResTuple
|
||||
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
|
||||
|
||||
@@ -136,7 +137,7 @@ class LinkBase(abc.ABC):
|
||||
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
|
||||
# xx is the number of response bytes available.
|
||||
# See also:
|
||||
if sw is not None:
|
||||
if (sw 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
|
||||
@@ -218,6 +219,56 @@ 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.
|
||||
|
||||
Args:
|
||||
cla : string (in hex) ISO 7816 class byte
|
||||
ins : string (in hex) ISO 7816 instruction byte
|
||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
||||
cmd_cosntr : defining how to generate binary APDU command data
|
||||
cmd_data : command data passed to cmd_constr
|
||||
resp_cosntr : defining how to decode binary APDU response data
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
|
||||
p3 = i2h([len(cmd)])
|
||||
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
|
||||
(data, sw) = self.send_apdu(pdu)
|
||||
if data:
|
||||
# filter the resulting dict to avoid '_io' members inside
|
||||
rsp = filter_dict(resp_constr.parse(h2b(data)))
|
||||
else:
|
||||
rsp = None
|
||||
return (rsp, sw)
|
||||
|
||||
def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
|
||||
cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
|
||||
sw_exp: SwMatchstr="9000") -> Tuple[dict, SwHexstr]:
|
||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
||||
|
||||
Args:
|
||||
cla : string (in hex) ISO 7816 class byte
|
||||
ins : string (in hex) ISO 7816 instruction byte
|
||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
||||
cmd_cosntr : defining how to generate binary APDU command data
|
||||
cmd_data : command data passed to cmd_constr
|
||||
resp_cosntr : defining how to decode binary APDU response data
|
||||
exp_sw : string (in hex) of status word (ex. "9000")
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
(rsp, sw) = self.send_apdu_constr(cla, ins,
|
||||
p1, p2, cmd_constr, cmd_data, resp_constr)
|
||||
if not sw_match(sw, sw_exp):
|
||||
raise SwMatchError(sw, sw_exp.lower(), self.sw_interpreter)
|
||||
return (rsp, sw)
|
||||
|
||||
|
||||
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
||||
"""Add all reader related arguments to the given argparse.Argumentparser instance."""
|
||||
from pySim.transport.serial import SerialSimLink
|
||||
|
||||
@@ -24,7 +24,7 @@ import argparse
|
||||
from typing import Optional
|
||||
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.exceptions import ReaderError, ProtocolError
|
||||
from pySim.exceptions import *
|
||||
from pySim.utils import h2b, b2h, Hexstr, ResTuple
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ class L1CTLMessageReset(L1CTLMessage):
|
||||
L1CTL_RES_T_FULL = 0x01
|
||||
L1CTL_RES_T_SCHED = 0x02
|
||||
|
||||
def __init__(self, ttype=L1CTL_RES_T_FULL):
|
||||
super().__init__(self.L1CTL_RESET_REQ)
|
||||
self.data += struct.pack("Bxxx", ttype)
|
||||
def __init__(self, type=L1CTL_RES_T_FULL):
|
||||
super(L1CTLMessageReset, self).__init__(self.L1CTL_RESET_REQ)
|
||||
self.data += struct.pack("Bxxx", type)
|
||||
|
||||
|
||||
class L1CTLMessageSIM(L1CTLMessage):
|
||||
@@ -70,7 +70,7 @@ class L1CTLMessageSIM(L1CTLMessage):
|
||||
L1CTL_SIM_CONF = 0x17
|
||||
|
||||
def __init__(self, pdu):
|
||||
super().__init__(self.L1CTL_SIM_REQ)
|
||||
super(L1CTLMessageSIM, self).__init__(self.L1CTL_SIM_REQ)
|
||||
self.data += pdu
|
||||
|
||||
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
#
|
||||
|
||||
import logging as log
|
||||
import serial
|
||||
import time
|
||||
import re
|
||||
import argparse
|
||||
from typing import Optional
|
||||
import serial
|
||||
|
||||
from pySim.utils import Hexstr, ResTuple
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.exceptions import ReaderError, ProtocolError
|
||||
from pySim.exceptions import *
|
||||
|
||||
# HACK: if somebody needs to debug this thing
|
||||
# log.root.setLevel(log.DEBUG)
|
||||
@@ -57,7 +57,7 @@ class ModemATCommandLink(LinkBase):
|
||||
|
||||
def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
|
||||
# Convert from string to bytes, if needed
|
||||
bcmd = cmd if isinstance(cmd, bytes) else cmd.encode()
|
||||
bcmd = cmd if type(cmd) is bytes else cmd.encode()
|
||||
bcmd += b'\r'
|
||||
|
||||
# Clean input buffer from previous/unexpected data
|
||||
@@ -67,9 +67,9 @@ class ModemATCommandLink(LinkBase):
|
||||
log.debug('Sending AT command: %s', cmd)
|
||||
try:
|
||||
wlen = self._sl.write(bcmd)
|
||||
assert wlen == len(bcmd)
|
||||
except Exception as exc:
|
||||
raise ReaderError('Failed to send AT command: %s' % cmd) from exc
|
||||
assert(wlen == len(bcmd))
|
||||
except:
|
||||
raise ReaderError('Failed to send AT command: %s' % cmd)
|
||||
|
||||
rsp = b''
|
||||
its = 1
|
||||
@@ -91,7 +91,8 @@ 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
|
||||
@@ -119,10 +120,11 @@ class ModemATCommandLink(LinkBase):
|
||||
if result[-1] == b'OK':
|
||||
self._echo = False
|
||||
return
|
||||
if result[-1] == b'AT\r\r\nOK':
|
||||
elif result[-1] == b'AT\r\r\nOK':
|
||||
self._echo = True
|
||||
return
|
||||
raise ReaderError('Interface \'%s\' does not respond to \'AT\' command' % self._device)
|
||||
raise ReaderError(
|
||||
'Interface \'%s\' does not respond to \'AT\' command' % self._device)
|
||||
|
||||
def reset_card(self):
|
||||
# Reset the modem, just to be sure
|
||||
@@ -133,7 +135,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 ...
|
||||
@@ -163,9 +165,9 @@ 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 Exception as exc:
|
||||
raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc
|
||||
(rsp_pdu_len, rsp_pdu) = result.groups()
|
||||
except:
|
||||
raise ReaderError('Failed to parse response from modem: %s' % rsp)
|
||||
|
||||
# TODO: make sure we have at least SW
|
||||
data = rsp_pdu[:-4].decode().lower()
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from smartcard.CardConnection import CardConnection
|
||||
from smartcard.CardRequest import CardRequest
|
||||
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException
|
||||
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException, CardConnectionException
|
||||
from smartcard.System import readers
|
||||
|
||||
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
|
||||
@@ -63,14 +63,15 @@ 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 as exc:
|
||||
raise NoCardError() from exc
|
||||
except CardRequestTimeoutException:
|
||||
raise NoCardError()
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
@@ -81,10 +82,10 @@ class PcscSimLink(LinkBase):
|
||||
|
||||
# Explicitly select T=0 communication protocol
|
||||
self._con.connect(CardConnection.T0_protocol)
|
||||
except CardConnectionException as exc:
|
||||
raise ProtocolError() from exc
|
||||
except NoCardException as exc:
|
||||
raise NoCardError() from exc
|
||||
except CardConnectionException:
|
||||
raise ProtocolError()
|
||||
except NoCardException:
|
||||
raise NoCardError()
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return self._con.getATR()
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
# 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 pySim.exceptions import NoCardError, ProtocolError
|
||||
from pySim.transport import LinkBase
|
||||
@@ -51,7 +51,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,7 +62,8 @@ class SerialSimLink(LinkBase):
|
||||
self.reset_card()
|
||||
if not newcardonly:
|
||||
return
|
||||
existing = True
|
||||
else:
|
||||
existing = True
|
||||
except NoCardError:
|
||||
pass
|
||||
|
||||
@@ -85,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 ...
|
||||
@@ -104,7 +105,7 @@ class SerialSimLink(LinkBase):
|
||||
rv = self._reset_card()
|
||||
if rv == 0:
|
||||
raise NoCardError()
|
||||
if rv < 0:
|
||||
elif rv < 0:
|
||||
raise ProtocolError()
|
||||
return rv
|
||||
|
||||
@@ -119,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 Exception as exc:
|
||||
raise ValueError('Invalid reset pin %s' % self._rst_pin) from exc
|
||||
except:
|
||||
raise ValueError('Invalid reset pin %s' % self._rst_pin)
|
||||
|
||||
rst_meth(rst_val)
|
||||
time.sleep(0.1) # 100 ms
|
||||
@@ -202,7 +203,7 @@ class SerialSimLink(LinkBase):
|
||||
b = self._rx_byte()
|
||||
if ord(b) == pdu[1]:
|
||||
break
|
||||
if b != '\x60':
|
||||
elif b != '\x60':
|
||||
# Ok, it 'could' be SW1
|
||||
sw1 = b
|
||||
sw2 = self._rx_byte()
|
||||
@@ -221,7 +222,7 @@ class SerialSimLink(LinkBase):
|
||||
to_recv = data_len - len(pdu) + 5 + 2
|
||||
|
||||
data = bytes(0)
|
||||
while len(data) < to_recv:
|
||||
while (len(data) < to_recv):
|
||||
b = self._rx_byte()
|
||||
if (to_recv == 2) and (b == '\x60'): # Ignore NIL if we have no RX data (hack ?)
|
||||
continue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding=utf-8
|
||||
"""Utilities / Functions related to ETSI TS 102 221, the core UICC spec.
|
||||
|
||||
(C) 2021-2024 by Harald Welte <laforge@osmocom.org>
|
||||
(C) 2021 by Harald Welte <laforge@osmocom.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -16,22 +16,22 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from bidict import bidict
|
||||
|
||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
|
||||
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 import iso7816_4
|
||||
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 AddonSIM
|
||||
from pySim.ts_51_011 import DF_GSM, DF_TELECOM, AddonSIM
|
||||
from pySim.gsm_r import AddonGSMR
|
||||
from pySim.cdma_ruim import AddonRUIM
|
||||
|
||||
@@ -80,10 +80,6 @@ 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)
|
||||
@@ -135,7 +131,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'/SupplyVoltageClasses,
|
||||
_construct = Struct('voltage_class'/Int8ub,
|
||||
'power_consumption_ma'/Int8ub,
|
||||
'reference_freq_100k'/Int8ub)
|
||||
|
||||
@@ -238,19 +234,20 @@ class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A):
|
||||
def _to_bytes(self):
|
||||
if self.decoded == 'no_information':
|
||||
return b'\x00'
|
||||
if self.decoded == 'creation':
|
||||
elif self.decoded == 'creation':
|
||||
return b'\x01'
|
||||
if self.decoded == 'initialization':
|
||||
elif self.decoded == 'initialization':
|
||||
return b'\x03'
|
||||
if self.decoded == 'operational_activated':
|
||||
elif self.decoded == 'operational_activated':
|
||||
return b'\x05'
|
||||
if self.decoded == 'operational_deactivated':
|
||||
elif self.decoded == 'operational_deactivated':
|
||||
return b'\x04'
|
||||
if self.decoded == 'termination':
|
||||
elif self.decoded == 'termination':
|
||||
return b'\x0c'
|
||||
if isinstance(self.decoded, int):
|
||||
elif isinstance(self.decoded, int):
|
||||
return self.decoded.to_bytes(1, 'big')
|
||||
raise ValueError
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
# ETSI TS 102 221 11.1.1.4.9
|
||||
class PS_DO(BER_TLV_IE, tag=0x90):
|
||||
@@ -287,33 +284,6 @@ 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):
|
||||
@@ -511,11 +481,12 @@ 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:]
|
||||
@@ -549,7 +520,7 @@ class SecCondByte_DO(DataObject):
|
||||
if inb & 0x10:
|
||||
res.append('user_auth')
|
||||
rd = {'mode': cond}
|
||||
if len(res) > 0:
|
||||
if len(res):
|
||||
rd['conditions'] = res
|
||||
self.decoded = rd
|
||||
|
||||
@@ -752,7 +723,7 @@ class EF_ARR(LinFixedEF):
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
def _decode_record_bin(self, raw_bin_data, **_kwargs):
|
||||
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)
|
||||
@@ -760,17 +731,20 @@ class EF_ARR(LinFixedEF):
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
def _encode_record_bin(self, in_json, **_kwargs):
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
return arr_seq.encode_multi(in_json)
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
def do_read_arr_record(self, opts):
|
||||
"""Read one EF.ARR record in flattened, human-friendly form."""
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
(data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
@@ -781,7 +755,7 @@ class EF_ARR(LinFixedEF):
|
||||
# collect all results in list so they are rendered as JSON list when printing
|
||||
data_list = []
|
||||
for recnr in range(1, 1 + num_of_rec):
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
|
||||
(data, sw) = self._cmd.lchan.read_record_dec(recnr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
@@ -834,7 +808,7 @@ class CardProfileUICC(CardProfile):
|
||||
'6200': 'No information given, state of non-volatile memory unchanged',
|
||||
'6281': 'Part of returned data may be corrupted',
|
||||
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
|
||||
'6283': 'Selected file invalidated/disabled; needs to be activated before use',
|
||||
'6283': 'Selected file invalidated',
|
||||
'6284': 'Selected file in termination state',
|
||||
'62f1': 'More data available',
|
||||
'62f2': 'More data available and proactive command pending',
|
||||
@@ -895,10 +869,10 @@ class CardProfileUICC(CardProfile):
|
||||
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
def decode_select_response(resp_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
t = FcpTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
t.from_tlv(h2b(resp_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fcp_template'])
|
||||
|
||||
@@ -932,81 +906,3 @@ class CardProfileUICC(CardProfile):
|
||||
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)
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import List
|
||||
import argparse
|
||||
|
||||
import cmd2
|
||||
from cmd2 import CommandSet, with_default_category
|
||||
from cmd2 import CommandSet, with_default_category, with_argparser
|
||||
import argparse
|
||||
|
||||
from pySim.utils import b2h, auto_uint8, auto_uint16, is_hexstr
|
||||
from pySim.exceptions import *
|
||||
from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
|
||||
|
||||
from pySim.ts_102_221 import *
|
||||
|
||||
@@ -31,6 +32,9 @@ 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!')
|
||||
@@ -45,7 +49,7 @@ class Ts102222Commands(CommandSet):
|
||||
self._cmd.perror("Refusing to permanently delete the file, please read the help text.")
|
||||
return
|
||||
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
|
||||
(_data, _sw) = self._cmd.lchan.scc.delete_file(f.fid)
|
||||
(data, sw) = self._cmd.lchan.scc.delete_file(f.fid)
|
||||
|
||||
def complete_delete_file(self, text, line, begidx, endidx) -> List[str]:
|
||||
"""Command Line tab completion for DELETE FILE"""
|
||||
@@ -66,7 +70,7 @@ class Ts102222Commands(CommandSet):
|
||||
self._cmd.perror("Refusing to terminate the file, please read the help text.")
|
||||
return
|
||||
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
|
||||
(_data, _sw) = self._cmd.lchan.scc.terminate_df(f.fid)
|
||||
(data, sw) = self._cmd.lchan.scc.terminate_df(f.fid)
|
||||
|
||||
def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]:
|
||||
"""Command Line tab completion for TERMINATE DF"""
|
||||
@@ -82,7 +86,7 @@ class Ts102222Commands(CommandSet):
|
||||
self._cmd.perror("Refusing to terminate the file, please read the help text.")
|
||||
return
|
||||
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
|
||||
(_data, _sw) = self._cmd.lchan.scc.terminate_ef(f.fid)
|
||||
(data, sw) = self._cmd.lchan.scc.terminate_ef(f.fid)
|
||||
|
||||
def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]:
|
||||
"""Command Line tab completion for TERMINATE EF"""
|
||||
@@ -100,21 +104,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=is_hexstr, help='File Identifier as 4-character hex string')
|
||||
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=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('--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('--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=auto_uint16, help='Length of each record in octets')
|
||||
create_optional.add_argument('--record-length', type=int, help='Length of each record in octets')
|
||||
|
||||
@cmd2.with_argparser(create_parser)
|
||||
def do_create_ef(self, opts):
|
||||
@@ -145,22 +149,22 @@ 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=is_hexstr, help='File Identifier as 4-character hex string')
|
||||
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=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
|
||||
createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, 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=is_hexstr, help='Application ID (creates an ADF, instead of a DF)')
|
||||
createdf_optional.add_argument('--aid', type=str, 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=auto_uint16, help='Physical memory allocated for DF/ADi in octets')
|
||||
createdf_optional.add_argument('--total-file-size', type=int, 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')
|
||||
@@ -188,15 +192,15 @@ 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)
|
||||
|
||||
@@ -204,7 +208,7 @@ class Ts102222Commands(CommandSet):
|
||||
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
|
||||
resize_ef_parser._action_groups.pop()
|
||||
resize_ef_required = resize_ef_parser.add_argument_group('required arguments')
|
||||
resize_ef_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
|
||||
resize_ef_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
|
||||
|
||||
@cmd2.with_argparser(resize_ef_parser)
|
||||
def do_resize_ef(self, opts):
|
||||
@@ -213,7 +217,7 @@ class Ts102222Commands(CommandSet):
|
||||
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)
|
||||
|
||||
|
||||
@@ -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-2024 Harald Welte <laforge@osmocom.org>
|
||||
# Copyright (C) 2021-2023 Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -26,12 +26,7 @@ Various constants from 3GPP TS 31.102 V17.9.0
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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, PaddedString
|
||||
|
||||
# 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
|
||||
@@ -44,10 +39,12 @@ from pySim.tlv import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.ts_31_102_telecom import DF_PHONEBOOK, EF_UServiceTable
|
||||
from pySim.construct import *
|
||||
from pySim.utils import is_hexstr
|
||||
from pySim.cat import SMS_TPDU, DeviceIdentities, SMSPPDownload
|
||||
|
||||
# Mapping between USIM Service Number and its description
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
from typing import Tuple
|
||||
from struct import unpack, pack
|
||||
import enum
|
||||
EF_UST_map = {
|
||||
1: 'Local Phone Book',
|
||||
2: 'Fixed Dialling Numbers (FDN)',
|
||||
@@ -402,15 +399,15 @@ class EF_LI(TransRecEF):
|
||||
desc='Language Indication'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len)
|
||||
|
||||
def _decode_record_bin(self, in_bin, **_kwargs):
|
||||
def _decode_record_bin(self, in_bin, **kwargs):
|
||||
if in_bin == b'\xff\xff':
|
||||
return None
|
||||
else:
|
||||
# officially this is 7-bit GSM alphabet with one padding bit in each byte
|
||||
return in_bin.decode('ascii')
|
||||
|
||||
def _encode_record_bin(self, in_json, **_kwargs):
|
||||
if in_json is None:
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
if in_json == None:
|
||||
return b'\xff\xff'
|
||||
else:
|
||||
# officially this is 7-bit GSM alphabet with one padding bit in each byte
|
||||
@@ -440,6 +437,9 @@ 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 +450,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
|
||||
@@ -502,7 +502,7 @@ class EF_ECC(LinFixedEF):
|
||||
desc='Emergency Call Codes'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(4, 20))
|
||||
|
||||
def _decode_record_bin(self, in_bin, **_kwargs):
|
||||
def _decode_record_bin(self, in_bin, **kwargs):
|
||||
# mandatory parts
|
||||
code = in_bin[:3]
|
||||
if code == b'\xff\xff\xff':
|
||||
@@ -516,7 +516,7 @@ class EF_ECC(LinFixedEF):
|
||||
ret['alpha_id'] = parse_construct(EF_ECC.alpha_construct, alpha_id)
|
||||
return ret
|
||||
|
||||
def _encode_record_bin(self, in_json, **_kwargs):
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
if in_json is None:
|
||||
return b'\xff\xff\xff\xff'
|
||||
code = EF_ECC.cc_construct.build(in_json['call_code'])
|
||||
@@ -637,6 +637,9 @@ 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
|
||||
@@ -679,7 +682,7 @@ class EF_RPLMNAcT(TransRecEF):
|
||||
def __init__(self, fid='6f65', sfid=None, name='EF.RPLMNAcTD', size=(2, 4), rec_len=2,
|
||||
desc='RPLMN Last used Access Technology', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
|
||||
def _decode_record_hex(self, in_hex, **_kwargs):
|
||||
def _decode_record_hex(self, in_hex, **kwargs):
|
||||
return dec_act(in_hex)
|
||||
# TODO: Encode
|
||||
|
||||
@@ -1135,6 +1138,9 @@ 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
|
||||
@@ -1443,13 +1449,14 @@ 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', has_imsi=True):
|
||||
desc='USIM Application'):
|
||||
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'),
|
||||
@@ -1570,10 +1577,6 @@ class ADF_USIM(CardADF):
|
||||
DF_5G_ProSe(service=139),
|
||||
DF_SAIP(),
|
||||
]
|
||||
|
||||
if has_imsi:
|
||||
files.append(EF_IMSI(sfid=0x07))
|
||||
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
@@ -1581,19 +1584,22 @@ class ADF_USIM(CardADF):
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
authenticate_parser = argparse.ArgumentParser()
|
||||
authenticate_parser.add_argument('rand', type=is_hexstr, help='Random challenge')
|
||||
authenticate_parser.add_argument('autn', type=is_hexstr, help='Authentication Nonce')
|
||||
authenticate_parser.add_argument('rand', help='Random challenge')
|
||||
authenticate_parser.add_argument('autn', 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', type=is_hexstr, help='Hexstring of encoded terminal profile')
|
||||
term_prof_parser.add_argument('PROFILE', help='Hexstring of encoded terminal profile')
|
||||
|
||||
@cmd2.with_argparser(term_prof_parser)
|
||||
def do_terminal_profile(self, opts):
|
||||
@@ -1607,7 +1613,7 @@ class ADF_USIM(CardADF):
|
||||
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
|
||||
|
||||
envelope_parser = argparse.ArgumentParser()
|
||||
envelope_parser.add_argument('PAYLOAD', type=is_hexstr, help='Hexstring of encoded payload to ENVELOPE')
|
||||
envelope_parser.add_argument('PAYLOAD', help='Hexstring of encoded payload to ENVELOPE')
|
||||
|
||||
@cmd2.with_argparser(envelope_parser)
|
||||
def do_envelope(self, opts):
|
||||
@@ -1619,7 +1625,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', type=is_hexstr, help='Hexstring of encoded SMS TPDU')
|
||||
envelope_sms_parser.add_argument('TPDU', help='Hexstring of encoded SMS TPDU')
|
||||
|
||||
@cmd2.with_argparser(envelope_sms_parser)
|
||||
def do_envelope_sms(self, opts):
|
||||
@@ -1647,7 +1653,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()
|
||||
@@ -1669,10 +1675,3 @@ 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,12 +26,11 @@ Needs to be a separate python module to avoid cyclic imports
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from construct import Optional as COptional
|
||||
from construct import Struct, Int16ub, Int32ub
|
||||
|
||||
from pySim.tlv import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.construct import *
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
|
||||
# TS 31.102 Section 4.2.8
|
||||
class EF_UServiceTable(TransparentEF):
|
||||
@@ -43,7 +42,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):
|
||||
@@ -74,7 +73,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'] is True:
|
||||
if in_json[srv]['activated'] == True:
|
||||
bit = 1
|
||||
else:
|
||||
bit = 0
|
||||
@@ -83,7 +82,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']:
|
||||
@@ -122,7 +121,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:
|
||||
|
||||
@@ -22,7 +22,6 @@ Various constants from 3GPP TS 31.103 V16.1.0
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from construct import Struct, Switch, this, Bytes, GreedyString
|
||||
from pySim.filesystem import *
|
||||
from pySim.utils import *
|
||||
from pySim.tlv import *
|
||||
|
||||
@@ -1034,7 +1034,7 @@ class DF_GSM(CardDF):
|
||||
super().__init__()
|
||||
|
||||
authenticate_parser = argparse.ArgumentParser()
|
||||
authenticate_parser.add_argument('rand', type=is_hexstr, help='Random challenge')
|
||||
authenticate_parser.add_argument('rand', help='Random challenge')
|
||||
|
||||
@cmd2.with_argparser(authenticate_parser)
|
||||
def do_authenticate(self, opts):
|
||||
|
||||
130
pySim/utils.py
130
pySim/utils.py
@@ -6,10 +6,8 @@
|
||||
import json
|
||||
import abc
|
||||
import string
|
||||
import datetime
|
||||
import argparse
|
||||
from io import BytesIO
|
||||
from typing import Optional, List, Dict, Any, Tuple, NewType, Union
|
||||
from typing import Optional, List, Dict, Any, Tuple, NewType
|
||||
|
||||
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
||||
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
|
||||
@@ -150,12 +148,12 @@ def comprehensiontlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
|
||||
# three-byte tag
|
||||
tag = (binary[1] & 0x7f) << 8
|
||||
tag |= binary[2]
|
||||
compr = bool(binary[1] & 0x80)
|
||||
compr = True if binary[1] & 0x80 else False
|
||||
return ({'comprehension': compr, 'tag': tag}, binary[3:])
|
||||
else:
|
||||
# single byte tag
|
||||
tag = binary[0] & 0x7f
|
||||
compr = bool(binary[0] & 0x80)
|
||||
compr = True if binary[0] & 0x80 else False
|
||||
return ({'comprehension': compr, 'tag': tag}, binary[1:])
|
||||
|
||||
|
||||
@@ -163,7 +161,7 @@ def comprehensiontlv_encode_tag(tag) -> bytes:
|
||||
"""Encode a single Tag according to ETSI TS 101 220 Section 7.1.1"""
|
||||
# permit caller to specify tag also as integer value
|
||||
if isinstance(tag, int):
|
||||
compr = bool(tag < 0xff and tag & 0x80)
|
||||
compr = True if tag < 0xff and tag & 0x80 else False
|
||||
tag = {'tag': tag, 'comprehension': compr}
|
||||
compr = tag.get('comprehension', False)
|
||||
if tag['tag'] in [0x00, 0x80, 0xff] or tag['tag'] > 0xff:
|
||||
@@ -219,7 +217,7 @@ def bertlv_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
|
||||
i = 1
|
||||
last = False
|
||||
while not last:
|
||||
last = not bool(binary[i] & 0x80)
|
||||
last = False if binary[i] & 0x80 else True
|
||||
tag <<= 8
|
||||
tag |= binary[i]
|
||||
i += 1
|
||||
@@ -234,7 +232,7 @@ def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
|
||||
Tuple of ({class:int, constructed:bool, tag:int}, remainder:bytes)
|
||||
"""
|
||||
cls = binary[0] >> 6
|
||||
constructed = bool(binary[0] & 0x20)
|
||||
constructed = True if binary[0] & 0x20 else False
|
||||
tag = binary[0] & 0x1f
|
||||
if tag <= 30:
|
||||
return ({'class': cls, 'constructed': constructed, 'tag': tag}, binary[1:])
|
||||
@@ -243,7 +241,7 @@ def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
|
||||
i = 1
|
||||
last = False
|
||||
while not last:
|
||||
last = not bool(binary[i] & 0x80)
|
||||
last = False if binary[i] & 0x80 else True
|
||||
tag <<= 7
|
||||
tag |= binary[i] & 0x7f
|
||||
i += 1
|
||||
@@ -276,7 +274,7 @@ def bertlv_encode_tag(t) -> bytes:
|
||||
if isinstance(t, int):
|
||||
# first convert to a dict representation
|
||||
tag_size = count_int_bytes(t)
|
||||
t, _remainder = bertlv_parse_tag(t.to_bytes(tag_size, 'big'))
|
||||
t, remainder = bertlv_parse_tag(t.to_bytes(tag_size, 'big'))
|
||||
tag = t['tag']
|
||||
constructed = t['constructed']
|
||||
cls = t['class']
|
||||
@@ -360,42 +358,6 @@ def bertlv_parse_one(binary: bytes) -> Tuple[dict, int, bytes, bytes]:
|
||||
return (tagdict, length, value, remainder)
|
||||
|
||||
|
||||
def dgi_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
|
||||
# In absence of any clear spec guidance we assume it's always 16 bit
|
||||
return int.from_bytes(binary[:2], 'big'), binary[2:]
|
||||
|
||||
def dgi_encode_tag(t: int) -> bytes:
|
||||
return t.to_bytes(2, 'big')
|
||||
|
||||
def dgi_encode_len(length: int) -> bytes:
|
||||
"""Encode a single Length value according to GlobalPlatform Systems Scripting Language
|
||||
Specification v1.1.0 Annex B.
|
||||
Args:
|
||||
length : length value to be encoded
|
||||
Returns:
|
||||
binary output data of encoded length field
|
||||
"""
|
||||
if length < 255:
|
||||
return length.to_bytes(1, 'big')
|
||||
elif length <= 0xffff:
|
||||
return b'\xff' + length.to_bytes(2, 'big')
|
||||
else:
|
||||
raise ValueError("Length > 32bits not supported")
|
||||
|
||||
def dgi_parse_len(binary: bytes) -> Tuple[int, bytes]:
|
||||
"""Parse a single Length value according to GlobalPlatform Systems Scripting Language
|
||||
Specification v1.1.0 Annex B.
|
||||
Args:
|
||||
binary : binary input data of BER-TLV length field
|
||||
Returns:
|
||||
Tuple of (length, remainder)
|
||||
"""
|
||||
if binary[0] == 255:
|
||||
assert len(binary) >= 3
|
||||
return ((binary[1] << 8) | binary[2]), binary[3:]
|
||||
else:
|
||||
return binary[0], binary[1:]
|
||||
|
||||
# IMSI encoded format:
|
||||
# For IMSI 0123456789ABCDE:
|
||||
#
|
||||
@@ -446,30 +408,6 @@ def dec_iccid(ef: Hexstr) -> str:
|
||||
def enc_iccid(iccid: str) -> Hexstr:
|
||||
return swap_nibbles(rpad(iccid, 20))
|
||||
|
||||
def sanitize_iccid(iccid: Union[int, str]) -> str:
|
||||
iccid = str(iccid)
|
||||
if len(iccid) < 18:
|
||||
raise ValueError('ICCID input value must be at least 18 digits')
|
||||
if len(iccid) > 20:
|
||||
raise ValueError('ICCID input value must be at most 20 digits')
|
||||
if len(iccid) == 18:
|
||||
# 18 digits means we must add a luhn check digit to reach 19 digits
|
||||
iccid += str(calculate_luhn(iccid))
|
||||
if len(iccid) == 20:
|
||||
# 20 digits means we're actually exceeding E.118 by one digit, and
|
||||
# the luhn check digit must already be included
|
||||
verify_luhn(iccid)
|
||||
if len(iccid) == 19:
|
||||
# 19 digits means that it's either an in-spec 19-digits ICCID with
|
||||
# its luhn check digit already present, or it's an out-of-spec 20-digit
|
||||
# ICCID without that check digit...
|
||||
try:
|
||||
verify_luhn(iccid)
|
||||
except ValueError:
|
||||
# 19th digit was not luhn check digit; we must add it
|
||||
iccid += str(calculate_luhn(iccid))
|
||||
return iccid
|
||||
|
||||
|
||||
def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
|
||||
"""Converts integer MCC/MNC into 3 bytes for EF"""
|
||||
@@ -562,7 +500,7 @@ def dec_act(twohexbytes: Hexstr) -> List[str]:
|
||||
sel.add(a['name'])
|
||||
# TS 31.102 Section 4.2.5 Table 4.2.5.1
|
||||
eutran_bits = u16t & 0x7000
|
||||
if eutran_bits in [0x4000, 0x7000]:
|
||||
if eutran_bits == 0x4000 or eutran_bits == 0x7000:
|
||||
sel.add("E-UTRAN WB-S1")
|
||||
sel.add("E-UTRAN NB-S1")
|
||||
elif eutran_bits == 0x5000:
|
||||
@@ -571,7 +509,7 @@ def dec_act(twohexbytes: Hexstr) -> List[str]:
|
||||
sel.add("E-UTRAN WB-S1")
|
||||
# TS 31.102 Section 4.2.5 Table 4.2.5.2
|
||||
gsm_bits = u16t & 0x008C
|
||||
if gsm_bits in [0x0080, 0x008C]:
|
||||
if gsm_bits == 0x0080 or gsm_bits == 0x008C:
|
||||
sel.add("GSM")
|
||||
sel.add("EC-GSM-IoT")
|
||||
elif u16t & 0x008C == 0x0084:
|
||||
@@ -630,17 +568,12 @@ def calculate_luhn(cc) -> int:
|
||||
for d in num[::-2]]) % 10
|
||||
return 0 if check_digit == 10 else check_digit
|
||||
|
||||
def verify_luhn(digits: str):
|
||||
"""Verify the Luhn check digit; raises ValueError if it is incorrect."""
|
||||
cd = calculate_luhn(digits[:-1])
|
||||
if str(cd) != digits[-1]:
|
||||
raise ValueError('Luhn check digit mismatch: should be %s but is %s' % (str(cd), digits[-1]))
|
||||
|
||||
def mcc_from_imsi(imsi: str) -> Optional[str]:
|
||||
"""
|
||||
Derive the MCC (Mobile Country Code) from the first three digits of an IMSI
|
||||
"""
|
||||
if imsi is None:
|
||||
if imsi == None:
|
||||
return None
|
||||
|
||||
if len(imsi) > 3:
|
||||
@@ -653,7 +586,7 @@ def mnc_from_imsi(imsi: str, long: bool = False) -> Optional[str]:
|
||||
"""
|
||||
Derive the MNC (Mobile Country Code) from the 4th to 6th digit of an IMSI
|
||||
"""
|
||||
if imsi is None:
|
||||
if imsi == None:
|
||||
return None
|
||||
|
||||
if len(imsi) > 3:
|
||||
@@ -759,7 +692,7 @@ def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
|
||||
"""
|
||||
|
||||
# If no MSISDN is supplied then encode the file contents as all "ff"
|
||||
if msisdn in ["", "+"]:
|
||||
if msisdn == "" or msisdn == "+":
|
||||
return "ff" * 14
|
||||
|
||||
# Leading '+' indicates International Number
|
||||
@@ -800,7 +733,7 @@ def is_hex(string: str, minlen: int = 2, maxlen: Optional[int] = None) -> bool:
|
||||
|
||||
# Try actual encoding to be sure
|
||||
try:
|
||||
_try_encode = h2b(string)
|
||||
try_encode = h2b(string)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
@@ -828,10 +761,12 @@ def sanitize_pin_adm(pin_adm, pin_adm_hex=None) -> Hexstr:
|
||||
# Ensure that it's hex-encoded
|
||||
try:
|
||||
try_encode = h2b(pin_adm)
|
||||
except ValueError as exc:
|
||||
raise ValueError("PIN-ADM needs to be hex encoded using this option") from exc
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"PIN-ADM needs to be hex encoded using this option")
|
||||
else:
|
||||
raise ValueError("PIN-ADM needs to be exactly 16 digits (hex encoded)")
|
||||
raise ValueError(
|
||||
"PIN-ADM needs to be exactly 16 digits (hex encoded)")
|
||||
|
||||
return pin_adm
|
||||
|
||||
@@ -845,7 +780,7 @@ def get_addr_type(addr):
|
||||
"""
|
||||
|
||||
# Empty address string
|
||||
if len(addr) == 0:
|
||||
if not len(addr):
|
||||
return None
|
||||
|
||||
addr_list = addr.split('.')
|
||||
@@ -860,7 +795,7 @@ def get_addr_type(addr):
|
||||
return 0x01
|
||||
elif ipa.version == 6:
|
||||
return 0x02
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
invalid_ipv4 = True
|
||||
for i in addr_list:
|
||||
# Invalid IPv4 may qualify for a valid FQDN, so make check here
|
||||
@@ -916,7 +851,7 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
|
||||
Returns:
|
||||
multi-line string containing formatted table
|
||||
"""
|
||||
if str_list is None:
|
||||
if str_list == None:
|
||||
return ""
|
||||
if len(str_list) <= 0:
|
||||
return ""
|
||||
@@ -927,7 +862,7 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
|
||||
table = []
|
||||
for i in iter(range(rows)):
|
||||
str_list_row = str_list[i::rows]
|
||||
if align_left:
|
||||
if (align_left):
|
||||
format_str_cell = '%%-%ds'
|
||||
else:
|
||||
format_str_cell = '%%%ds'
|
||||
@@ -941,21 +876,6 @@ def auto_int(x):
|
||||
"""Helper function for argparse to accept hexadecimal integers."""
|
||||
return int(x, 0)
|
||||
|
||||
def _auto_uint(x, max_val: int):
|
||||
"""Helper function for argparse to accept hexadecimal or decimal integers."""
|
||||
ret = int(x, 0)
|
||||
if ret < 0 or ret > max_val:
|
||||
raise argparse.ArgumentTypeError('Number exceeds permited value range (0, %u)' % max_val)
|
||||
return ret
|
||||
|
||||
def auto_uint7(x):
|
||||
return _auto_uint(x, 127)
|
||||
|
||||
def auto_uint8(x):
|
||||
return _auto_uint(x, 255)
|
||||
|
||||
def auto_uint16(x):
|
||||
return _auto_uint(x, 65535)
|
||||
|
||||
def expand_hex(hexstring, length):
|
||||
"""Expand a given hexstring to a specified length by replacing "." or ".."
|
||||
@@ -1015,10 +935,8 @@ class JsonEncoder(json.JSONEncoder):
|
||||
"""Extend the standard library JSONEncoder with support for more types."""
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, (BytesIO, bytes, bytearray)):
|
||||
if isinstance(o, BytesIO) or isinstance(o, bytes) or isinstance(o, bytearray):
|
||||
return b2h(o)
|
||||
elif isinstance(o, datetime.datetime):
|
||||
return o.isoformat()
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,6 @@ termcolor
|
||||
colorlog
|
||||
pycryptodomex
|
||||
cryptography
|
||||
git+https://github.com/osmocom/asn1tools
|
||||
asn1tools
|
||||
packaging
|
||||
git+https://github.com/hologram-io/smpp.pdu
|
||||
|
||||
100
saip-test.py
100
saip-test.py
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pySim.utils import b2h, h2b
|
||||
from pySim.esim.saip import *
|
||||
from pySim.esim.saip.validation import *
|
||||
|
||||
from pySim.pprint import HexBytesPrettyPrinter
|
||||
|
||||
pp = HexBytesPrettyPrinter(indent=4,width=500)
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
|
||||
|
||||
with open('smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der', 'rb') as f:
|
||||
pes = ProfileElementSequence.from_der(f.read())
|
||||
|
||||
if False:
|
||||
# iterate over each pe in the pes.pe_list
|
||||
for pe in pes.pe_list:
|
||||
print("="*70 + " " + pe.type)
|
||||
pp.pprint(pe.decoded)
|
||||
|
||||
if False:
|
||||
# sort by PE type and show all PE within that type
|
||||
for pe_type in pes.pe_by_type.keys():
|
||||
print("="*70 + " " + pe_type)
|
||||
for pe in pes.pe_by_type[pe_type]:
|
||||
pp.pprint(pe)
|
||||
pp.pprint(pe.decoded)
|
||||
|
||||
checker = CheckBasicStructure()
|
||||
checker.check(pes)
|
||||
|
||||
if False:
|
||||
for naa in pes.pes_by_naa:
|
||||
i = 0
|
||||
for naa_instance in pes.pes_by_naa[naa]:
|
||||
print("="*70 + " " + naa + str(i))
|
||||
i += 1
|
||||
for pe in naa_instance:
|
||||
pp.pprint(pe.type)
|
||||
for d in pe.decoded:
|
||||
print(" %s" % d)
|
||||
#pp.pprint(pe.decoded[d])
|
||||
#if pe.type in ['akaParameter', 'pinCodes', 'pukCodes']:
|
||||
# pp.pprint(pe.decoded)
|
||||
|
||||
|
||||
from pySim.esim.saip.personalization import *
|
||||
|
||||
params = [Iccid('984944000000000000'), Imsi('901990123456789'),
|
||||
Puk1(value='01234567'), Puk2(value='98765432'), Pin1('1111'), Pin2('2222'), Adm1('11111111'),
|
||||
K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f')),
|
||||
SdKeyScp80_01Kic(h2b('000102030405060708090a0b0c0d0e0f'))]
|
||||
|
||||
from pySim.esim.saip.templates import *
|
||||
|
||||
for p in params:
|
||||
p.apply(pes)
|
||||
|
||||
if False:
|
||||
for pe in pes:
|
||||
pp.pprint(pe.decoded)
|
||||
pass
|
||||
|
||||
if True:
|
||||
naas = pes.pes_by_naa.keys()
|
||||
for naa in naas:
|
||||
for pe in pes.pes_by_naa[naa][0]:
|
||||
print(pe)
|
||||
#pp.pprint(pe.decoded)
|
||||
#print(pe.header)
|
||||
tpl_id = pe.templateID
|
||||
if tpl_id:
|
||||
prof = ProfileTemplateRegistry.get_by_oid(tpl_id)
|
||||
print(prof)
|
||||
#pp.pprint(pe.decoded)
|
||||
for fname, fdata in pe.files.items():
|
||||
print()
|
||||
print("============== %s" % fname)
|
||||
ftempl = None
|
||||
if prof:
|
||||
ftempl = prof.files_by_pename[fname]
|
||||
print("Template: %s" % repr(ftempl))
|
||||
print("Data: %s" % fdata)
|
||||
file = File(fname, fdata, ftempl)
|
||||
print(repr(file))
|
||||
#pp.pprint(pe.files)
|
||||
|
||||
if True:
|
||||
# iterate over each pe in the pes (using its __iter__ method)
|
||||
for pe in pes:
|
||||
print("="*70 + " " + pe.type)
|
||||
pp.pprint(pe.decoded)
|
||||
|
||||
|
||||
|
||||
#print(ProfileTemplateRegistry.by_oid)
|
||||
3
setup.py
3
setup.py
@@ -3,8 +3,7 @@ from setuptools import setup
|
||||
setup(
|
||||
name='pySim',
|
||||
version='1.0',
|
||||
packages=['pySim', 'pySim.legacy', 'pySim.transport', 'pySim.apdu', 'pySim.apdu_source',
|
||||
'pySim.esim'],
|
||||
packages=['pySim', 'pySim.legacy', 'pySim.transport', 'pySim.apdu', 'pySim.apdu_source'],
|
||||
url='https://osmocom.org/projects/pysim/wiki',
|
||||
license='GPLv2',
|
||||
author_email='simtrace@lists.osmocom.org',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user