1 Commits

Author SHA1 Message Date
Harald Welte
bef85dbc28 WIP: osmo-smdpp ES9+ support for ASN.1 endpoint 2024-01-10 19:59:04 +01:00
309 changed files with 5722 additions and 55140 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
open_collective: osmocom

4
.gitignore vendored
View File

@@ -3,7 +3,3 @@
/docs/_*
/docs/generated
/.cache
/.local
/build
/pySim.egg-info

View File

@@ -77,7 +77,7 @@ Please install the following dependencies:
- cmd2 >= 1.5.0
- colorlog
- construct >= 2.9.51
- pyosmocom
- gsm0338
- jsonpath-ng
- packaging
- pycryptodomex
@@ -123,34 +123,19 @@ sudo pacman -Rs python-pysim-git
```
Forum
-----
We welcome any pySim related discussions in the
[SIM Card Technology](https://discourse.osmocom.org/c/sim-card-technology/)
section of the osmocom discourse (web based Forum).
Mailing List
------------
There is no separate mailing list for this project. However,
discussions related to pySim are happening on the simtrace
<simtrace@lists.osmocom.org> mailing list, please see
<https://lists.osmocom.org/mailman/listinfo/simtrace> for subscription
discussions related to pysim-prog are happening on the
<openbsc@lists.osmocom.org> mailing list, please see
<https://lists.osmocom.org/mailman/listinfo/openbsc> for subscription
options and the list archive.
Please observe the [Osmocom Mailing List
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
when posting.
Issue Tracker
-------------
We use the [issue tracker of the pysim project on osmocom.org](https://osmocom.org/projects/pysim/issues) for
tracking the state of bug reports and feature requests. Feel free to submit any issues you may find, or help
us out by resolving existing issues.
Contributing
------------

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env python3
# Utility program to perform column-based encryption of a CSV file holding SIM/UICC
# related key materials.
#
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import csv
import argparse
from Cryptodome.Cipher import AES
from osmocom.utils import h2b, b2h, Hexstr
from pySim.card_key_provider import CardKeyProviderCsv
def dict_keys_to_upper(d: dict) -> dict:
return {k.upper():v for k,v in d.items()}
class CsvColumnEncryptor:
def __init__(self, filename: str, transport_keys: dict):
self.filename = filename
self.transport_keys = dict_keys_to_upper(transport_keys)
def encrypt_col(self, colname:str, value: str) -> Hexstr:
key = self.transport_keys[colname]
cipher = AES.new(h2b(key), AES.MODE_CBC, CardKeyProviderCsv.IV)
return b2h(cipher.encrypt(h2b(value)))
def encrypt(self) -> None:
with open(self.filename, 'r') as infile:
cr = csv.DictReader(infile)
cr.fieldnames = [field.upper() for field in cr.fieldnames]
with open(self.filename + '.encr', 'w') as outfile:
cw = csv.DictWriter(outfile, dialect=csv.unix_dialect, fieldnames=cr.fieldnames)
cw.writeheader()
for row in cr:
for key_colname in self.transport_keys:
if key_colname in row:
row[key_colname] = self.encrypt_col(key_colname, row[key_colname])
cw.writerow(row)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('CSVFILE', help="CSV file name")
parser.add_argument('--csv-column-key', action='append', required=True,
help='per-CSV-column AES transport key')
opts = parser.parse_args()
csv_column_keys = {}
for par in opts.csv_column_key:
name, key = par.split(':')
csv_column_keys[name] = key
if len(csv_column_keys) == 0:
print("You must specify at least one key!")
sys.exit(1)
csv_column_keys = CardKeyProviderCsv.process_transport_keys(csv_column_keys)
for name, key in csv_column_keys.items():
print("Encrypting column %s using AES key %s" % (name, key))
cce = CsvColumnEncryptor(opts.CSVFILE, csv_column_keys)
cce.encrypt()

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,318 +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 os
import sys
import argparse
import logging
import hashlib
from typing import List
from urllib.parse import urlparse
from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.asymmetric import ec
from osmocom.utils import h2b, b2h, swap_nibbles, is_hexstr
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
import pySim.esim.rsp as rsp
from pySim.esim import es9p, PMO
from pySim.esim.x509_cert import CertAndPrivkey
from pySim.esim.es8p import BoundProfilePackage
logging.basicConfig(level=logging.DEBUG)
parser = argparse.ArgumentParser(description="""
Utility to manually issue requests against the ES9+ API of an SM-DP+ according to GSMA SGP.22.""")
parser.add_argument('--url', required=True, help='Base URL of ES9+ API endpoint')
parser.add_argument('--server-ca-cert', help="""X.509 CA certificates acceptable for the server side. In
production use cases, this would be the GSMA Root CA (CI) certificate.""")
parser.add_argument('--certificate-path', default='.',
help="Path in which to look for certificate and key files.")
parser.add_argument('--euicc-certificate', default='CERT_EUICC_ECDSA_NIST.der',
help="File name of DER-encoded eUICC certificate file.")
parser.add_argument('--euicc-private-key', default='SK_EUICC_ECDSA_NIST.pem',
help="File name of PEM-format eUICC secret key file.")
parser.add_argument('--eum-certificate', default='CERT_EUM_ECDSA_NIST.der',
help="File name of DER-encoded EUM certificate file.")
parser.add_argument('--ci-certificate', default='CERT_CI_ECDSA_NIST.der',
help="File name of DER-encoded CI certificate file.")
subparsers = parser.add_subparsers(dest='command',help="The command (API function) to call", required=True)
# download
parser_dl = subparsers.add_parser('download', help="ES9+ download")
parser_dl.add_argument('--matchingId', required=True,
help='MatchingID that shall be used by profile download')
parser_dl.add_argument('--output-path', default='.',
help="Path to which the output files will be written.")
parser_dl.add_argument('--confirmation-code',
help="Confirmation Code for the eSIM download")
# notification
parser_ntf = subparsers.add_parser('notification', help='ES9+ (other) notification')
parser_ntf.add_argument('operation', choices=['enable','disable','delete'],
help='Profile Management Opreation whoise occurrence shall be notififed')
parser_ntf.add_argument('--sequence-nr', type=int, required=True,
help='eUICC global notification sequence number')
parser_ntf.add_argument('--notification-address', help='notificationAddress, if different from URL')
parser_ntf.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
# notification-install
parser_ntfi = subparsers.add_parser('notification-install', help='ES9+ installation notification')
parser_ntfi.add_argument('--sequence-nr', type=int, required=True,
help='eUICC global notification sequence number')
parser_ntfi.add_argument('--transaction-id', required=True,
help='transactionId of previous ES9+ download')
parser_ntfi.add_argument('--notification-address', help='notificationAddress, if different from URL')
parser_ntfi.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
parser_ntfi.add_argument('--smdpp-oid', required=True, help='SM-DP+ OID (as in CERT.DPpb.ECDSA)')
parser_ntfi.add_argument('--isdp-aid', type=is_hexstr, required=True,
help='AID of the ISD-P of the installed profile')
parser_ntfi.add_argument('--sima-response', type=is_hexstr, required=True,
help='hex digits of BER-encoded SAIP EUICCResponse')
class Es9pClient:
def __init__(self, opts):
self.opts = opts
self.cert_and_key = CertAndPrivkey()
self.cert_and_key.cert_from_der_file(os.path.join(opts.certificate_path, opts.euicc_certificate))
self.cert_and_key.privkey_from_pem_file(os.path.join(opts.certificate_path, opts.euicc_private_key))
with open(os.path.join(opts.certificate_path, opts.eum_certificate), 'rb') as f:
self.eum_cert = x509.load_der_x509_certificate(f.read())
with open(os.path.join(opts.certificate_path, opts.ci_certificate), 'rb') as f:
self.ci_cert = x509.load_der_x509_certificate(f.read())
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), self.ci_cert.extensions))
subject_pkid = subject_exts[0].value
self.ci_pkid = subject_pkid.key_identifier
print("EUICC: %s" % self.cert_and_key.cert.subject)
print("EUM: %s" % self.eum_cert.subject)
print("CI: %s" % self.ci_cert.subject)
self.eid = self.cert_and_key.cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
print("EID: %s" % self.eid)
print("CI PKID: %s" % b2h(self.ci_pkid))
print()
self.peer = es9p.Es9pApiClient(opts.url, server_cert_verify=opts.server_ca_cert)
def do_notification(self):
ntf_metadata = {
'seqNumber': self.opts.sequence_nr,
'profileManagementOperation': PMO(self.opts.operation).to_bitstring(),
'notificationAddress': self.opts.notification_address or urlparse(self.opts.url).netloc,
}
if opts.iccid:
ntf_metadata['iccid'] = h2b(swap_nibbles(opts.iccid))
if self.opts.operation == 'download':
pird = {
'transactionId': self.opts.transaction_id,
'notificationMetadata': ntf_metadata,
'smdpOid': self.opts.smdpp_oid,
'finalResult': ('successResult', {
'aid': self.opts.isdp_aid,
'simaResponse': self.opts.sima_response,
}),
}
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
signature = self.cert_and_key.ecdsa_sign(pird_bin)
pn_dict = ('profileInstallationResult', {
'profileInstallationResultData': pird,
'euiccSignPIR': signature,
})
else:
ntf_bin = rsp.asn1.encode('NotificationMetadata', ntf_metadata)
signature = self.cert_and_key.ecdsa_sign(ntf_bin)
pn_dict = ('otherSignedNotification', {
'tbsOtherNotification': ntf_metadata,
'euiccNotificationSignature': signature,
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER)),
})
data = {
'pendingNotification': pn_dict,
}
#print(data)
res = self.peer.call_handleNotification(data)
def do_download(self):
print("Step 1: InitiateAuthentication...")
euiccInfo1 = {
'svn': b'\x02\x04\x00',
'euiccCiPKIdListForVerification': [
self.ci_pkid,
],
'euiccCiPKIdListForSigning': [
self.ci_pkid,
],
}
data = {
'euiccChallenge': os.urandom(16),
'euiccInfo1': euiccInfo1,
'smdpAddress': urlparse(self.opts.url).netloc,
}
init_auth_res = self.peer.call_initiateAuthentication(data)
print(init_auth_res)
print("Step 2: AuthenticateClient...")
#res['serverSigned1']
#res['serverSignature1']
print("TODO: verify serverSignature1 over serverSigned1")
#res['transactionId']
print("TODO: verify transactionId matches the signed one in serverSigned1")
#res['euiccCiPKIdToBeUsed']
# TODO: select eUICC certificate based on CI
#res['serverCertificate']
# TODO: verify server certificate against CI
euiccInfo2 = {
'profileVersion': b'\x02\x03\x01',
'svn': euiccInfo1['svn'],
'euiccFirmwareVer': b'\x23\x42\x00',
'extCardResource': b'\x81\x01\x00\x82\x04\x00\x04\x9ch\x83\x02"#',
'uiccCapability': (b'k6\xd3\xc3', 32),
'javacardVersion': b'\x11\x02\x00',
'globalplatformVersion': b'\x02\x03\x00',
'rspCapability': (b'\x9c', 6),
'euiccCiPKIdListForVerification': euiccInfo1['euiccCiPKIdListForVerification'],
'euiccCiPKIdListForSigning': euiccInfo1['euiccCiPKIdListForSigning'],
#'euiccCategory':
#'forbiddenProfilePolicyRules':
'ppVersion': b'\x01\x00\x00',
'sasAcreditationNumber': 'OSMOCOM-TEST-1', #TODO: make configurable
#'certificationDataObject':
}
euiccSigned1 = {
'transactionId': h2b(init_auth_res['transactionId']),
'serverAddress': init_auth_res['serverSigned1']['serverAddress'],
'serverChallenge': init_auth_res['serverSigned1']['serverChallenge'],
'euiccInfo2': euiccInfo2,
'ctxParams1':
('ctxParamsForCommonAuthentication', {
'matchingId': self.opts.matchingId,
'deviceInfo': {
'tac': b'\x35\x23\x01\x45', # same as lpac
'deviceCapabilities': {},
#imei:
}
}),
}
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
euiccSignature1 = self.cert_and_key.ecdsa_sign(euiccSigned1_bin)
auth_clnt_req = {
'transactionId': init_auth_res['transactionId'],
'authenticateServerResponse':
('authenticateResponseOk', {
'euiccSigned1': euiccSigned1,
'euiccSignature1': euiccSignature1,
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER))
})
}
auth_clnt_res = self.peer.call_authenticateClient(auth_clnt_req)
print(auth_clnt_res)
#auth_clnt_res['transactionId']
print("TODO: verify transactionId matches previous ones")
#auth_clnt_res['profileMetadata']
# TODO: what's in here?
#auth_clnt_res['smdpSigned2']['bppEuiccOtpk']
#auth_clnt_res['smdpSignature2']
print("TODO: verify serverSignature2 over smdpSigned2")
smdp_cert = x509.load_der_x509_certificate(auth_clnt_res['smdpCertificate'])
print("Step 3: GetBoundProfilePackage...")
# Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
# Reference value of CERT.DPpb.ECDSA
euicc_ot = ec.generate_private_key(smdp_cert.public_key().public_numbers().curve)
# extract the public key in (hopefully) the right format for the ES8+ interface
euicc_otpk = euicc_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
euiccSigned2 = {
'transactionId': h2b(auth_clnt_res['transactionId']),
'euiccOtpk': euicc_otpk,
#hashCC
}
# check for smdpSigned2 ccRequiredFlag, and send it in PrepareDownloadRequest hashCc
if auth_clnt_res['smdpSigned2']['ccRequiredFlag']:
if not self.opts.confirmation_code:
raise ValueError('Confirmation Code required but not provided')
cc_hash = hashlib.sha256(self.opts.confirmation_code.encode('ascii')).digest()
euiccSigned2['hashCc'] = hashlib.sha256(cc_hash + euiccSigned2['transactionId']).digest()
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
euiccSignature2 = self.cert_and_key.ecdsa_sign(euiccSigned2_bin + auth_clnt_res['smdpSignature2'])
gbp_req = {
'transactionId': auth_clnt_res['transactionId'],
'prepareDownloadResponse':
('downloadResponseOk', {
'euiccSigned2': euiccSigned2,
'euiccSignature2': euiccSignature2,
})
}
gbp_res = self.peer.call_getBoundProfilePackage(gbp_req)
print(gbp_res)
#gbp_res['transactionId']
# TODO: verify transactionId
print("TODO: verify transactionId matches previous ones")
bpp_bin = gbp_res['boundProfilePackage']
print("TODO: verify boundProfilePackage smdpSignature")
bpp = BoundProfilePackage()
upp_bin = bpp.decode(euicc_ot, self.eid, bpp_bin)
iccid = swap_nibbles(b2h(bpp.storeMetadataRequest['iccid']))
base_name = os.path.join(self.opts.output_path, '%s' % iccid)
print("SUCCESS: Storing files as %s.*.der" % base_name)
# write various output files
with open(base_name+'.upp.der', 'wb') as f:
f.write(bpp.upp)
with open(base_name+'.isdp.der', 'wb') as f:
f.write(bpp.encoded_configureISDPRequest)
with open(base_name+'.smr.der', 'wb') as f:
f.write(bpp.encoded_storeMetadataRequest)
if __name__ == '__main__':
opts = parser.parse_args()
c = Es9pClient(opts)
if opts.command == 'download':
c.do_download()
elif opts.command == 'notification':
c.do_notification()
elif opts.command == 'notification-install':
opts.operation = 'install'
c.do_notification()

View File

@@ -1,169 +0,0 @@
#!/usr/bin/env python3
# The purpose of this script is to
# * load two SIM card 'fsdump' files
# * determine which file contents in "B" differs from that of "A"
# * create a pySim-shell script to update the contents of "A" to match that of "B"
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import argparse
# Files that we should not update
FILES_TO_SKIP = [
"MF/EF.ICCID",
#"MF/DF.GSM/EF.IMSI",
#"MF/ADF.USIM/EF.IMSI",
]
# Files that need zero-padding at the end, not ff-padding
FILES_PAD_ZERO = [
"DF.GSM/EF.SST",
"MF/ADF.USIM/EF.UST",
"MF/ADF.USIM/EF.EST",
"MF/ADF.ISIM/EF.IST",
]
def pad_file(path, instr, byte_len):
if path in FILES_PAD_ZERO:
pad = '0'
else:
pad = 'f'
return pad_hexstr(instr, byte_len, pad)
def pad_hexstr(instr, byte_len:int, pad='f'):
"""Pad given hex-string to the number of bytes given in byte_len, using ff as padding."""
if len(instr) == byte_len*2:
return instr
elif len(instr) > byte_len*2:
raise ValueError('Cannot pad string of length %u to smaller length %u' % (len(instr)/2, byte_len))
else:
return instr + pad * (byte_len*2 - len(instr))
def is_all_ff(instr):
"""Determine if the entire input hex-string consists of f-digits."""
if all([x == 'f' for x in instr.lower()]):
return True
else:
return False
parser = argparse.ArgumentParser()
parser.add_argument('file_a')
parser.add_argument('file_b')
if __name__ == '__main__':
opts = parser.parse_args()
with open(opts.file_a, 'r') as file_a:
json_a = json.loads(file_a.read())
with open(opts.file_b, 'r') as file_b:
json_b = json.loads(file_b.read())
for path in json_b.keys():
print()
print("# %s" % path)
if not path in json_a:
raise ValueError("%s doesn't exist in file_a!" % path)
if path in FILES_TO_SKIP:
print("# skipped explicitly as it is in FILES_TO_SKIP")
continue
if not 'body' in json_b[path]:
print("# file doesn't exist in B so we cannot possibly need to modify A")
continue
if not 'body' in json_a[path]:
# file was not readable in original (permissions? deactivated?)
print("# ERROR: %s not readable in A; please fix that" % path)
continue
body_a = json_a[path]['body']
body_b = json_b[path]['body']
if body_a == body_b:
print("# file body is identical")
continue
file_size_a = json_a[path]['fcp']['file_size']
file_size_b = json_b[path]['fcp']['file_size']
cmds = []
structure = json_b[path]['fcp']['file_descriptor']['file_descriptor_byte']['structure']
if structure == 'transparent':
val_a = body_a
val_b = body_b
if file_size_a < file_size_b:
if not is_all_ff(val_b[2*file_size_a:]):
print("# ERROR: file_size_a (%u) < file_size_b (%u); please fix!" % (file_size_a, file_size_b))
continue
else:
print("# WARN: file_size_a (%u) < file_size_b (%u); please fix!" % (file_size_a, file_size_b))
# truncate val_b to fit in A
val_b = val_b[:file_size_a*2]
elif file_size_a != file_size_b:
print("# NOTE: file_size_a (%u) != file_size_b (%u)" % (file_size_a, file_size_b))
# Pad to file_size_a
val_b = pad_file(path, val_b, file_size_a)
if val_b != val_a:
cmds.append("update_binary %s" % val_b)
else:
print("# padded file body is identical")
elif structure in ['linear_fixed', 'cyclic']:
record_len_a = json_a[path]['fcp']['file_descriptor']['record_len']
record_len_b = json_b[path]['fcp']['file_descriptor']['record_len']
if record_len_a < record_len_b:
print("# ERROR: record_len_a (%u) < record_len_b (%u); please fix!" % (file_size_a, file_size_b))
continue
elif record_len_a != record_len_b:
print("# NOTE: record_len_a (%u) != record_len_b (%u)" % (record_len_a, record_len_b))
num_rec_a = file_size_a // record_len_a
num_rec_b = file_size_b // record_len_b
if num_rec_a < num_rec_b:
if not all([is_all_ff(x) for x in body_b[num_rec_a:]]):
print("# ERROR: num_rec_a (%u) < num_rec_b (%u); please fix!" % (num_rec_a, num_rec_b))
continue
else:
print("# WARN: num_rec_a (%u) < num_rec_b (%u); but they're empty" % (num_rec_a, num_rec_b))
elif num_rec_a != num_rec_b:
print("# NOTE: num_rec_a (%u) != num_rec_b (%u)" % (num_rec_a, num_rec_b))
i = 0
for r in body_b:
if i < len(body_a):
break
val_a = body_a[i]
# Pad to record_len_a
val_b = pad_file(path, body_b[i], record_len_a)
if val_a != val_b:
cmds.append("update_record %u %s" % (i+1, val_b))
i = i + 1
if len(cmds) == 0:
print("# padded file body is identical")
elif structure == 'ber_tlv':
print("# FIXME: Implement BER-TLV")
else:
raise ValueError('Unsupported structure %s' % structure)
if len(cmds):
print("select %s" % path)
for cmd in cmds:
print(cmd)

View File

@@ -4,23 +4,18 @@
# environment variables:
# * WITH_MANUALS: build manual PDFs if set to "1"
# * PUBLISH: upload manuals after building if set to "1" (ignored without WITH_MANUALS = "1")
# * JOB_TYPE: one of 'test', 'distcheck', 'pylint', 'docs'
# * SKIP_CLEAN_WORKSPACE: don't run osmo-clean-workspace.sh (for pyosmocom CI)
# * JOB_TYPE: one of 'test', 'pylint', 'docs'
#
export PYTHONUNBUFFERED=1
if [ ! -d "./tests/" ] ; then
if [ ! -d "./pysim-testdata/" ] ; then
echo "###############################################"
echo "Please call from pySim-prog top directory"
echo "###############################################"
exit 1
fi
if [ -z "$SKIP_CLEAN_WORKSPACE" ]; then
osmo-clean-workspace.sh
fi
case "$JOB_TYPE" in
"test")
virtualenv -p python3 venv --system-site-packages
@@ -30,39 +25,16 @@ case "$JOB_TYPE" in
pip install pyshark
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/unittests
python -m unittest discover -v -s tests/
# Run pySim-prog integration tests (requires physical cards)
cd tests/pySim-prog_test/
./pySim-prog_test.sh
cd ../../
# Run pySim-trace test
tests/pySim-trace_test/pySim-trace_test.sh
# Run pySim-shell integration tests (requires physical cards)
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
;;
"distcheck")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install .
pip install pyshark
for prog in venv/bin/pySim-*.py; do
$prog --help > /dev/null
done
# Run the test with physical cards
cd pysim-testdata
../tests/pySim-prog_test.sh
../tests/pySim-trace_test.sh
;;
"pylint")
# Print pylint version
pip3 freeze | grep pylint
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install .
# Run pylint to find potential errors
# Ignore E1102: not-callable
# pySim/filesystem.py: E1102: method is not callable (not-callable)
@@ -73,15 +45,9 @@ case "$JOB_TYPE" in
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim tests/unittests/*.py *.py \
contrib/*.py
pySim *.py
;;
"docs")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt
rm -rf docs/_build
make -C "docs" html latexpdf
@@ -94,5 +60,3 @@ case "$JOB_TYPE" in
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
exit 1
esac
osmo-clean-workspace.sh

View File

@@ -1,258 +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 os
import sys
import argparse
import logging
import zipfile
from pathlib import Path as PlPath
from typing import List
from osmocom.utils import h2b, b2h, swap_nibbles
from pySim.esim.saip import *
from pySim.esim.saip.validation import CheckBasicStructure
from pySim import javacard
from pySim.pprint import HexBytesPrettyPrinter
pp = HexBytesPrettyPrinter(indent=4,width=500)
parser = argparse.ArgumentParser(description="""
Utility program to work with eSIM SAIP (SimAlliance Interoperable Profile) files.""")
parser.add_argument('INPUT_UPP', help='Unprotected Profile Package Input file')
parser.add_argument("--loglevel", dest="loglevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
default='INFO', help="Set the logging level")
parser.add_argument('--debug', action='store_true', help='Enable DEBUG logging')
subparsers = parser.add_subparsers(dest='command', help="The command to perform", required=True)
parser_split = subparsers.add_parser('split', help='Split PE-Sequence into individual PEs')
parser_split.add_argument('--output-prefix', default='.', help='Prefix path/filename for output files')
parser_dump = subparsers.add_parser('dump', help='Dump information on PE-Sequence')
parser_dump.add_argument('mode', choices=['all_pe', 'all_pe_by_type', 'all_pe_by_naa'])
parser_dump.add_argument('--dump-decoded', action='store_true', help='Dump decoded PEs')
parser_check = subparsers.add_parser('check', help='Run constraint checkers on PE-Sequence')
parser_rpe = subparsers.add_parser('remove-pe', help='Remove specified PEs from PE-Sequence')
parser_rpe.add_argument('--output-file', required=True, help='Output file name')
parser_rpe.add_argument('--identification', type=int, action='append', help='Remove PEs matching specified identification')
parser_rn = subparsers.add_parser('remove-naa', help='Remove speciifed NAAs from PE-Sequence')
parser_rn.add_argument('--output-file', required=True, help='Output file name')
parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='Network Access Application type to remove')
# TODO: add an --naa-index or the like, so only one given instance can be removed
parser_info = subparsers.add_parser('info', help='Display information about the profile')
parser_eapp = subparsers.add_parser('extract-apps', help='Extract applications as loadblock file')
parser_eapp.add_argument('--output-dir', default='.', help='Output directory (where to store files)')
parser_eapp.add_argument('--format', default='cap', choices=['ijc', 'cap'], help='Data format of output files')
parser_info = subparsers.add_parser('tree', help='Display the filesystem tree')
def do_split(pes: ProfileElementSequence, opts):
i = 0
for pe in pes.pe_list:
basename = PlPath(opts.INPUT_UPP).stem
if not pe.identification:
fname = '%s-%02u-%s.der' % (basename, i, pe.type)
else:
fname = '%s-%02u-%05u-%s.der' % (basename, i, pe.identification, pe.type)
print("writing single PE to file '%s'" % fname)
with open(os.path.join(opts.output_prefix, fname), 'wb') as outf:
outf.write(pe.to_der())
i += 1
def do_dump(pes: ProfileElementSequence, opts):
def print_all_pe(pes: ProfileElementSequence, dump_decoded:bool = False):
# iterate over each pe in the pes (using its __iter__ method)
for pe in pes:
print("="*70 + " " + pe.type)
if dump_decoded:
pp.pprint(pe.decoded)
def print_all_pe_by_type(pes: ProfileElementSequence, dump_decoded:bool = False):
# sort by PE type and show all PE within that type
for pe_type in pes.pe_by_type.keys():
print("="*70 + " " + pe_type)
for pe in pes.pe_by_type[pe_type]:
pp.pprint(pe)
if dump_decoded:
pp.pprint(pe.decoded)
def print_all_pe_by_naa(pes: ProfileElementSequence, dump_decoded:bool = False):
for naa in pes.pes_by_naa:
i = 0
for naa_instance in pes.pes_by_naa[naa]:
print("="*70 + " " + naa + str(i))
i += 1
for pe in naa_instance:
pp.pprint(pe.type)
if dump_decoded:
for d in pe.decoded:
print(" %s" % d)
if opts.mode == 'all_pe':
print_all_pe(pes, opts.dump_decoded)
elif opts.mode == 'all_pe_by_type':
print_all_pe_by_type(pes, opts.dump_decoded)
elif opts.mode == 'all_pe_by_naa':
print_all_pe_by_naa(pes, opts.dump_decoded)
def do_check(pes: ProfileElementSequence, opts):
print("Checking PE-Sequence structure...")
checker = CheckBasicStructure()
checker.check(pes)
print("All good!")
def do_remove_pe(pes: ProfileElementSequence, opts):
new_pe_list = []
for pe in pes.pe_list:
identification = pe.identification
if identification:
if identification in opts.identification:
print("Removing PE %s (id=%u) from Sequence..." % (pe, identification))
continue
new_pe_list.append(pe)
pes.pe_list = new_pe_list
pes._process_pelist()
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
with open(opts.output_file, 'wb') as f:
f.write(pes.to_der())
def do_remove_naa(pes: ProfileElementSequence, opts):
if not opts.naa_type in NAAs:
raise ValueError('unsupported NAA type %s' % opts.naa_type)
naa = NAAs[opts.naa_type]
print("Removing NAAs of type '%s' from Sequence..." % opts.naa_type)
pes.remove_naas_of_type(naa)
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
with open(opts.output_file, 'wb') as f:
f.write(pes.to_der())
def do_info(pes: ProfileElementSequence, opts):
def get_naa_count(pes: ProfileElementSequence) -> dict:
"""return a dict with naa-type (usim, isim) as key and the count of NAA instances as value."""
ret = {}
for naa_type in pes.pes_by_naa:
ret[naa_type] = len(pes.pes_by_naa[naa_type])
return ret
pe_hdr_dec = pes.pe_by_type['header'][0].decoded
print()
print("SAIP Profile Version: %u.%u" % (pe_hdr_dec['major-version'], pe_hdr_dec['minor-version']))
print("Profile Type: '%s'" % pe_hdr_dec['profileType'])
print("ICCID: %s" % b2h(pe_hdr_dec['iccid']))
print("Mandatory Services: %s" % ', '.join(pe_hdr_dec['eUICC-Mandatory-services'].keys()))
print()
naa_strs = ["%s[%u]" % (k, v) for k, v in get_naa_count(pes).items()]
print("NAAs: %s" % ', '.join(naa_strs))
for naa_type in pes.pes_by_naa:
for naa_inst in pes.pes_by_naa[naa_type]:
first_pe = naa_inst[0]
adf_name = ''
if hasattr(first_pe, 'adf_name'):
adf_name = '(' + first_pe.adf_name + ')'
print("NAA %s %s" % (first_pe.type, adf_name))
if hasattr(first_pe, 'imsi'):
print("\tIMSI: %s" % first_pe.imsi)
# applications
print()
apps = pes.pe_by_type.get('application', [])
print("Number of applications: %u" % len(apps))
for app_pe in apps:
print("App Load Package AID: %s" % b2h(app_pe.decoded['loadBlock']['loadPackageAID']))
print("\tMandated: %s" % ('mandated' in app_pe.decoded['app-Header']))
print("\tLoad Block Size: %s" % len(app_pe.decoded['loadBlock']['loadBlockObject']))
for inst in app_pe.decoded.get('instanceList', []):
print("\tInstance AID: %s" % b2h(inst['instanceAID']))
# security domains
print()
sds = pes.pe_by_type.get('securityDomain', [])
print("Number of security domains: %u" % len(sds))
for sd in sds:
print("Security domain Instance AID: %s" % b2h(sd.decoded['instance']['instanceAID']))
# FIXME: 'applicationSpecificParametersC9' parsing to figure out enabled SCP
for key in sd.keys:
print("\tKVN=0x%02x, KID=0x%02x, %s" % (key.key_version_number, key.key_identifier, key.key_components))
# RFM
print()
rfms = pes.pe_by_type.get('rfm', [])
print("Number of RFM instances: %u" % len(rfms))
for rfm in rfms:
inst_aid = rfm.decoded['instanceAID']
print("RFM instanceAID: %s" % b2h(inst_aid))
print("\tMSL: 0x%02x" % rfm.decoded['minimumSecurityLevel'][0])
adf = rfm.decoded.get('adfRFMAccess', None)
if adf:
print("\tADF AID: %s" % b2h(adf['adfAID']))
tar_list = rfm.decoded.get('tarList', [inst_aid[-3:]])
for tar in tar_list:
print("\tTAR: %s" % b2h(tar))
def do_extract_apps(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
fname = os.path.join(opts.output_dir, '%s-%s.%s' % (pes.iccid, package_aid, opts.format))
load_block_obj = app_pe.decoded['loadBlock']['loadBlockObject']
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
if opts.format == 'ijc':
with open(fname, 'wb') as f:
f.write(load_block_obj)
else:
with io.BytesIO(load_block_obj) as f, zipfile.ZipFile(fname, 'w') as z:
javacard.ijc_to_cap(f, z, package_aid)
def do_tree(pes:ProfileElementSequence, opts):
pes.mf.print_tree()
if __name__ == '__main__':
opts = parser.parse_args()
if opts.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.getLevelName(opts.loglevel))
with open(opts.INPUT_UPP, 'rb') as f:
pes = ProfileElementSequence.from_der(f.read())
print("Read %u PEs from file '%s'" % (len(pes.pe_list), opts.INPUT_UPP))
if opts.command == 'split':
do_split(pes, opts)
elif opts.command == 'dump':
do_dump(pes, opts)
elif opts.command == 'check':
do_check(pes, opts)
elif opts.command == 'remove-pe':
do_remove_pe(pes, opts)
elif opts.command == 'remove-naa':
do_remove_naa(pes, opts)
elif opts.command == 'info':
do_info(pes, opts)
elif opts.command == 'extract-apps':
do_extract_apps(pes, opts)
elif opts.command == 'tree':
do_tree(pes, opts)

View File

@@ -162,7 +162,6 @@ def main(argv):
parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0)
subp = parser.add_subparsers()
subp.required = True
auth_p = subp.add_parser('auth', help='UMTS AKA Authentication')
auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10)

View File

@@ -6,8 +6,7 @@
import sys
import argparse
from osmocom.utils import b2h, h2b
from osmocom.tlv import bertlv_parse_one, bertlv_encode_tag
from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
def process_one_level(content: bytes, indent: int):
remainder = content
@@ -36,8 +35,5 @@ if __name__ == '__main__':
content = f.read()
elif opts.hex:
content = h2b(opts.hex)
else:
# avoid pylint "(possibly-used-before-assignment)" below
sys.exit(2)
process_one_level(content, 0)

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""Connect smartcard to a remote server, so the remote server can take control and
perform commands on it."""
import sys
import json
import logging
import argparse
from osmocom.utils import b2h
import websockets
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
from pySim.commands import SimCardCommands
from pySim.wsrc.client_blocking import WsClientBlocking
from pySim.exceptions import NoCardError
from pySim.wsrc import WSRC_DEFAULT_PORT_CARD
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
class CardWsClientBlocking(WsClientBlocking):
"""Implementation of the card (reader) client of the WSRC (WebSocket Remote Card) protocol"""
def __init__(self, ws_uri, tp: LinkBase):
super().__init__('card', ws_uri)
self.tp = tp
def perform_outbound_hello(self):
hello_data = {
'atr': b2h(self.tp.get_atr()),
# TODO: include various card information in the HELLO message
}
super().perform_outbound_hello(hello_data)
def handle_rx_c_apdu(self, rx: dict):
"""handle an inbound APDU transceive command"""
data, sw = self.tp.send_apdu(rx['command'])
tx = {
'response': data,
'sw': sw,
}
self.tx_json('r_apdu', tx)
def handle_rx_disconnect(self, rx: dict):
"""server tells us to disconnect"""
self.tx_json('disconnect_ack')
# FIXME: tear down connection and/or terminate entire program
def handle_rx_state_notification(self, rx: dict):
logger.info("State Notification: %s" % rx['new_state'])
def handle_rx_print(self, rx: dict):
"""print a message (text) given by server to the local console/log"""
logger.info("SERVER MSG: %s" % rx['message'])
# no response
def handle_rx_reset_req(self, rx: dict):
"""server tells us to reset the card"""
self.tp.reset_card()
self.tx_json('reset_resp', {'atr': b2h(self.tp.get_atr())})
parser = argparse.ArgumentParser()
argparse_add_reader_args(parser)
parser.add_argument("--uri", default="ws://localhost:%u/" % (WSRC_DEFAULT_PORT_CARD),
help="URI of the sever to which to connect")
if __name__ == '__main__':
opts = parser.parse_args()
# open the card reader / slot
logger.info("Initializing Card Reader...")
try:
tp = init_reader(opts)
except Exception as e:
logger.fatal("Error opening reader: %s" % e)
sys.exit(1)
logger.info("Connecting to Card...")
try:
tp.connect()
except NoCardError as e:
logger.fatal("Error opening card! Is a card inserted in the reader?")
sys.exit(1)
scc = SimCardCommands(transport=tp)
logger.info("Detected Card with ATR: %s" % b2h(tp.get_atr()))
# TODO: gather various information about the card; print it
# create + connect the client to the server
cl = CardWsClientBlocking(opts.uri, tp)
logger.info("Connecting to remote server...")
try:
cl.connect()
logger.info("Successfully connected to Server")
except ConnectionRefusedError as e:
logger.fatal(e)
sys.exit(1)
try:
while True:
# endless loop: wait for inbound command from server + execute it
cl.rx_and_execute_cmd()
except websockets.exceptions.ConnectionClosedOK as e:
print(e)
sys.exit(1)
except KeyboardInterrupt as e:
print(e.__class__.__name__)
sys.exit(2)

View File

@@ -1,354 +0,0 @@
#!/usr/bin/env python
import json
import asyncio
import logging
import argparse
from typing import Optional, Tuple
from websockets.asyncio.server import serve
from websockets.exceptions import ConnectionClosedError
from osmocom.utils import Hexstr, swap_nibbles
from pySim.utils import SwMatchstr, ResTuple, sw_match, dec_iccid
from pySim.exceptions import SwMatchError
from pySim.wsrc import WSRC_DEFAULT_PORT_USER, WSRC_DEFAULT_PORT_CARD
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
card_clients = set()
user_clients = set()
class WsClientLogAdapter(logging.LoggerAdapter):
"""LoggerAdapter adding context (for now: remote IP/Port of client) to log"""
def process(self, msg, kwargs):
return '%s([%s]:%u) %s' % (self.extra['type'], self.extra['remote_addr'][0],
self.extra['remote_addr'][1], msg), kwargs
class WsClient:
def __init__(self, websocket, hello: dict):
self.websocket = websocket
self.hello = hello
self.identity = {}
self.logger = WsClientLogAdapter(logger, {'type': self.__class__.__name__,
'remote_addr': websocket.remote_address})
def __str__(self):
return '%s([%s]:%u)' % (self.__class__.__name__, self.websocket.remote_address[0],
self.websocket.remote_address[1])
async def rx_json(self):
rx = await self.websocket.recv()
rx_js = json.loads(rx)
self.logger.debug("Rx: %s", rx_js)
assert 'msg_type' in rx
return rx_js
async def tx_json(self, msg_type:str, d: dict = {}):
"""Transmit a json-serializable dict to the peer"""
d['msg_type'] = msg_type
d_js = json.dumps(d)
self.logger.debug("Tx: %s", d_js)
await self.websocket.send(d_js)
async def tx_hello_ack(self):
await self.tx_json('hello_ack')
async def xceive_json(self, msg_type:str, d:dict = {}, exp_msg_type:Optional[str] = None) -> dict:
await self.tx_json(msg_type, d)
rx = await self.rx_json()
if exp_msg_type:
assert rx['msg_type'] == exp_msg_type
return rx;
async def tx_error(self, message: str):
"""Transmit an error message to the peer"""
event = {
"message": message,
}
self.logger.error("Transmitting error message: '%s'" % message)
await self.tx_json('error', event)
# async def ws_hdlr(self):
# """kind of a 'main' function for the websocket client: wait for incoming message,
# and handle it."""
# try:
# async for message in self.websocket:
# method = getattr(self, 'handle_rx_%s' % message['msg_type'], None)
# if not method:
# await self.tx_error("Unknonw msg_type: %s" % message['msg_type'])
# else:
# method(message)
# except ConnectionClosedError:
# # we handle this in the outer loop
# pass
class CardClient(WsClient):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.identity['ATR'] = self.hello['atr']
self.state = 'init'
def __str__(self):
eid = self.identity.get('EID', None)
if eid:
return '%s(EID=%s)' % (self.__class__.__name__, eid)
iccid = self.identity.get('ICCID', None)
if iccid:
return '%s(ICCID=%s)' % (self.__class__.__name__, iccid)
return super().__str__()
def listing_entry(self) -> dict:
return {'remote_addr': self.websocket.remote_address,
'identities': self.identity,
'state': self.state}
"""A websocket client that represents a reader/card. This is what we use to talk to a card"""
async def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
"""transceive a single APDU with the card"""
message = await self.xceive_json('c_apdu', {'command': cmd}, 'r_apdu')
return message['response'], message['sw']
async def xceive_apdu(self, pdu: Hexstr) -> ResTuple:
"""transceive an APDU with the card, handling T=0 GET_RESPONSE cases"""
prev_pdu = pdu
data, sw = await self.xceive_apdu_raw(pdu)
if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
prev_pdu = pdu_gr
d, sw = await self.xceive_apdu_raw(pdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
pdu_gr = prev_pdu[0:8] + sw[2:4]
data, sw = await self.xceive_apdu_raw(pdu_gr)
return data, sw
async def xceive_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""like xceive_apdu, but checking the status word matches the expected pattern"""
rv = await self.xceive_apdu(pdu)
last_sw = rv[1]
if not sw_match(rv[1], sw):
raise SwMatchError(rv[1], sw.lower())
return rv
async def card_reset(self):
"""reset the card"""
rx = await self.xceive_json('reset_req', exp_msg_type='reset_resp')
self.identity['ATR'] = rx['atr']
async def notify_state(self):
"""notify the card client of a state [change]"""
await self.tx_json('state_notification', {'new_state': self.state})
async def get_iccid(self):
"""high-level method to obtain the ICCID of the card"""
await self.xceive_apdu_checksw('00a40000023f00') # SELECT MF
await self.xceive_apdu_checksw('00a40000022fe2') # SELECT EF.ICCID
res, sw = await self.xceive_apdu_checksw('00b0000000') # READ BINARY
return dec_iccid(res)
async def get_eid_sgp22(self):
"""high-level method to obtain the EID of a SGP.22 consumer eUICC"""
await self.xceive_apdu_checksw('00a4040410a0000005591010ffffffff8900000100')
res, sw = await self.xceive_apdu_checksw('80e2910006bf3e035c015a')
return res[-32:]
async def set_state(self, new_state:str):
self.logger.info("Card now in '%s' state" % new_state)
self.state = new_state
await self.notify_state()
async def identify(self):
# identify the card by asking for its EID and/or ICCID
try:
eid = await self.get_eid_sgp22()
self.logger.debug("EID: %s", eid)
self.identity['EID'] = eid
except SwMatchError:
pass
try:
iccid = await self.get_iccid()
self.logger.debug("ICCID: %s", iccid)
self.identity['ICCID'] = iccid
except SwMatchError:
pass
await self.set_state('ready')
async def associate_user(self, user):
assert self.state == 'ready'
self.user = user
await self.set_state('associated')
async def disassociate_user(self):
assert self.user
assert self.state == 'associated'
await self.set_state('ready')
@staticmethod
def find_client_for_id(id_type: str, id_str: str) -> Optional['CardClient']:
for c in card_clients:
if c.state != 'ready':
continue
c_id = c.identity.get(id_type.upper(), None)
if c_id and c_id.lower() == id_str.lower():
return c
return None
class UserClient(WsClient):
"""A websocket client representing a user application like pySim-shell."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.state = 'init'
self.card = None
async def associate_card(self, card: CardClient):
assert self.state == 'init'
self.card = card
self.state = 'associated'
async def disassociate_card(self):
assert self.state == 'associated'
self.card = None
self.state = 'init'
async def state_init(self):
"""Wait for incoming 'select_card' and process it."""
while True:
rx = await self.rx_json()
if rx['msg_type'] == 'select_card':
# look-up if the card can be found
card = CardClient.find_client_for_id(rx['id_type'], rx['id_str'])
if not card:
await self.tx_error('No CardClient found for %s == %s' % (rx['id_type'], rx['id_str']))
continue
# transition to next statee
await card.associate_user(self)
await self.associate_card(card)
await self.tx_json('select_card_ack', {'identities': card.identity})
break
elif rx['msg_type'] == 'list_cards':
res = [x.listing_entry() for x in card_clients]
await self.tx_json('list_cards_ack', {'cards': res})
else:
self.tx_error('Unknown message type %s' % rx['msg_type'])
async def state_selected(self):
while True:
rx = await self.rx_json()
if rx['msg_type'] == 'c_apdu':
rsp, sw = await self.card.xceive_apdu_raw(rx['command'])
await self.tx_json('r_apdu', {'response': rsp, 'sw': sw})
elif rx['msg_type'] == 'reset_req':
await self.card.card_reset()
await self.tx_json('reset_resp')
else:
self.logger.warning("Unknown/unsupported command '%s' received" % rx['msg_type'])
async def card_conn_hdlr(websocket):
"""Handler for incoming connection to 'card' port."""
# receive first message, which should be a 'hello'
rx_raw = await websocket.recv()
rx = json.loads(rx_raw)
assert rx['msg_type'] == 'hello'
client_type = rx['client_type']
if client_type != 'card':
logger.error("Rejecting client (unknown type %s) connection", client_type)
raise ValueError("client_type '%s' != expected 'card'" % client_type)
card = CardClient(websocket, rx)
card.logger.info("connection accepted")
# first go through identity phase
async with asyncio.timeout(30):
await card.tx_hello_ack()
card.logger.info("hello-handshake completed")
card_clients.add(card)
# first obtain the identity of the card
await card.identify()
# then go into the "main loop"
try:
# wait 'indefinitely'. We cannot call websocket.recv() here, as we will call another
# recv() while waiting for the R-APDU after forwarding one from the user client.
await websocket.wait_closed()
finally:
card.logger.info("connection closed")
card_clients.remove(card)
async def user_conn_hdlr(websocket):
"""Handler for incoming connection to 'user' port."""
# receive first message, which should be a 'hello'
rx_raw = await websocket.recv()
rx = json.loads(rx_raw)
assert rx['msg_type'] == 'hello'
client_type = rx['client_type']
if client_type != 'user':
logger.error("Rejecting client (unknown type %s) connection", client_type)
raise ValueError("client_type '%s' != expected 'card'" % client_type)
user = UserClient(websocket, rx)
user.logger.info("connection accepted")
# first go through hello phase
async with asyncio.timeout(10):
await user.tx_hello_ack()
user.logger.info("hello-handshake completed")
user_clients.add(user)
# first wait for the user to specify the select the card
try:
await user.state_init()
except ConnectionClosedError:
user.logger.info("connection closed")
user_clients.remove(user)
return
except asyncio.exceptions.CancelledError: # direct cause of TimeoutError
user.logger.error("User failed to transition to 'associated' state within 10s")
user_clients.remove(user)
return
except Exception as e:
user.logger.error("Unknown exception %s", e)
raise e
user_clients.remove(user)
return
# then perform APDU exchanges without any time limit
try:
await user.state_selected()
except ConnectionClosedError:
pass
finally:
user.logger.info("connection closed")
await user.card.disassociate_user()
await user.disassociate_card()
user_clients.remove(user)
parser = argparse.ArgumentParser(description="""Osmocom WebSocket Remote Card server""")
parser.add_argument('--user-bind-port', type=int, default=WSRC_DEFAULT_PORT_USER,
help="port number to bind for connections from users")
parser.add_argument('--user-bind-host', type=str, default='localhost',
help="local Host/IP to bind for connections from users")
parser.add_argument('--card-bind-port', type=int, default=WSRC_DEFAULT_PORT_CARD,
help="port number to bind for connections from cards")
parser.add_argument('--card-bind-host', type=str, default='localhost',
help="local Host/IP to bind for connections from cards")
if __name__ == '__main__':
opts = parser.parse_args()
async def main():
# we use different ports for user + card connections to ensure they can
# have different packet filter rules apply to them
async with serve(card_conn_hdlr, opts.card_bind_host, opts.card_bind_port), serve(user_conn_hdlr, opts.user_bind_host, opts.user_bind_port):
await asyncio.get_running_loop().create_future() # run forever
asyncio.run(main())

View File

@@ -4,7 +4,7 @@
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= python3 -m sphinx.cmd.build
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build

View File

@@ -1,126 +0,0 @@
Retrieving card-individual keys via CardKeyProvider
==================================================
When working with a batch of cards, or more than one card in general, it
is a lot of effort to manually retrieve the card-specific PIN (like
ADM1) or key material (like SCP02/SCP03 keys).
To increase productivity in that regard, pySim has a concept called the
`CardKeyProvider`. This is a generic mechanism by which different parts
of the pySim[-shell] code can programmatically request card-specific key material
from some data source (*provider*).
For example, when you want to verify the ADM1 PIN using the `verify_adm`
command without providing an ADM1 value yourself, pySim-shell will
request the ADM1 value for the ICCID of the card via the
CardKeyProvider.
There can in theory be multiple different CardKeyProviders. You can for
example develop your own CardKeyProvider that queries some kind of
database for the key material, or that uses a key derivation function to
derive card-specific key material from a global master key.
The only actual CardKeyProvider implementation included in pySim is the
`CardKeyProviderCsv` which retrieves the key material from a
[potentially encrypted] CSV file.
The CardKeyProviderCsv
----------------------
The `CardKeyProviderCsv` allows you to retrieve card-individual key
material from a CSV (comma separated value) file that is accessible to pySim.
The CSV file must have the expected column names, for example `ICCID`
and `ADM1` in case you would like to use that CSV to obtain the
card-specific ADM1 PIN when using the `verify_adm` command.
You can specify the CSV file to use via the `--csv` command-line option
of pySim-shell. If you do not specify a CSV file, pySim will attempt to
open a CSV file from the default location at
`~/.osmocom/pysim/card_data.csv`, and use that, if it exists.
Column-Level CSV encryption
~~~~~~~~~~~~~~~~~~~~~~~~~~~
pySim supports column-level CSV encryption. This feature will make sure
that your key material is not stored in plaintext in the CSV file.
The encryption mechanism uses AES in CBC mode. You can use any key
length permitted by AES (128/192/256 bit).
Following GSMA FS.28, the encryption works on column level. This means
different columns can be decrypted using different key material. This
means that leakage of a column encryption key for one column or set of
columns (like a specific security domain) does not compromise various
other keys that might be stored in other columns.
You can specify column-level decryption keys using the
`--csv-column-key` command line argument. The syntax is
`FIELD:AES_KEY_HEX`, for example:
`pySim-shell.py --csv-column-key SCP03_ENC_ISDR:000102030405060708090a0b0c0d0e0f`
In order to avoid having to repeat the column key for each and every
column of a group of keys within a keyset, there are pre-defined column
group aliases, which will make sure that the specified key will be used
by all columns of the set:
* `UICC_SCP02` is a group alias for `UICC_SCP02_KIC1`, `UICC_SCP02_KID1`, `UICC_SCP02_KIK1`
* `UICC_SCP03` is a group alias for `UICC_SCP03_KIC1`, `UICC_SCP03_KID1`, `UICC_SCP03_KIK1`
* `SCP03_ECASD` is a group alias for `SCP03_ENC_ECASD`, `SCP03_MAC_ECASD`, `SCP03_DEK_ECASD`
* `SCP03_ISDA` is a group alias for `SCP03_ENC_ISDA`, `SCP03_MAC_ISDA`, `SCP03_DEK_ISDA`
* `SCP03_ISDR` is a group alias for `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR`, `SCP03_DEK_ISDR`
Field naming
------------
* For look-up of UICC/SIM/USIM/ISIM or eSIM profile specific key
material, pySim uses the `ICCID` field as lookup key.
* For look-up of eUICC specific key material (like SCP03 keys for the
ISD-R, ECASD), pySim uses the `EID` field as lookup key.
As soon as the CardKeyProviderCsv finds a line (row) in your CSV where
the ICCID or EID match, it looks for the column containing the requested
data.
ADM PIN
~~~~~~~
The `verify_adm` command will attempt to look up the `ADM1` column
indexed by the ICCID of the SIM/UICC.
SCP02 / SCP03
~~~~~~~~~~~~~
SCP02 and SCP03 each use key triplets consisting if ENC, MAC and DEK
keys. For more details, see the applicable GlobalPlatform
specifications.
If you do not want to manually enter the key material for each specific
card as arguments to the `establish_scp02` or `establish_scp03`
commands, you can make use of the `--key-provider-suffix` option. pySim
uses this suffix to compose the column names for the CardKeyProvider as
follows.
* `SCP02_ENC_` + suffix for the SCP02 ciphering key
* `SCP02_MAC_` + suffix for the SCP02 MAC key
* `SCP02_DEK_` + suffix for the SCP02 DEK key
* `SCP03_ENC_` + suffix for the SCP03 ciphering key
* `SCP03_MAC_` + suffix for the SCP03 MAC key
* `SCP03_DEK_` + suffix for the SCP03 DEK key
So for example, if you are using a command like `establish_scp03
--key-provider-suffix ISDR`, then the column names for the key material
look-up are `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR` and `SCP03_DEK_ISDR`,
respectively.
The identifier used for look-up is determined by the definition of the
Security Domain. For example, the eUICC ISD-R and ECASD will use the EID
of the eUICC. On the other hand, the ISD-P of an eSIM or the ISD of an
UICC will use the ICCID.

View File

@@ -43,7 +43,6 @@ pySim consists of several parts:
legacy
library
osmo-smdpp
wsrc
Indices and tables

View File

@@ -1,20 +1,20 @@
Legacy tools
Legacy tools
============
*legacy tools* are the classic ``pySim-prog`` and ``pySim-read`` programs that
existed long before ``pySim-shell``.
These days, it is highly recommended to use ``pySim-shell`` instead of these
These days, you should primarily use ``pySim-shell`` instead of these
legacy tools.
pySim-prog
----------
``pySim-prog`` was the first part of the pySim software suite. It started as a
tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and was
later extended to a variety of other cards. As the number of features supported
became no longer bearable to express with command-line arguments, `pySim-shell`
was created.
``pySim-prog`` was the first part of the pySim software suite. It started as
a tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and
was later extended to a variety of other cards. As the number of features supported
became no longer bearable to express with command-line arguments, `pySim-shell` was
created.
Basic use cases can still use `pySim-prog`.
@@ -22,180 +22,36 @@ Program customizable SIMs
~~~~~~~~~~~~~~~~~~~~~~~~~
Two modes are possible:
- one where the user specifies every parameter manually:
- one where you specify every parameter manually :
This is the most common way to use ``pySim-prog``. The user will specify all relevant parameters directly via the
commandline. A typical commandline would look like this:
``pySim-prog.py -p <pcsc_reader> --ki <ki_value> --opc <opc_value> --mcc <mcc_value> --mnc <mnc_value>
--country <country_code> --imsi <imsi_value> --iccid <iccid_value> --pin-adm <adm_pin>``
Please note, that this already lengthy commandline still only contains the most common card parameters. For a full
list of all possible parameters, use the ``--help`` option of ``pySim-prog``. It is also important to mention
that not all parameters are supported by all card types. In particular, very simple programmable SIM cards will only
support a very basic set of parameters, such as MCC, MNC, IMSI and KI values.
- one where the parameters are generated from a minimal set:
It is also possible to leave the generation of certain parameters to ``pySim-prog``. This is in particular helpful
when a large number of cards should be initialized with randomly generated key material.
``pySim-prog.py -p <pcsc_reader> --mcc <mcc_value> --mnc <mnc_value> --secret <random_secret> --num <card_number> --pin-adm <adm_pin>``
The parameter ``--secret`` specifies a random seed that is used to generate the card individual parameters. (IMSI).
The secret should contain enough randomness to avoid conflicts. It is also recommended to store the secret safely,
in case cards have to be re-generated or the current card batch has to be extended later. For security reasons, the
key material, which is also card individual, will not be derived from the random seed. Instead a new random set of
Ki and OPc will be generated during each programming cycle. This means fresh keys are generated, even when the
``--num`` remains unchanged.
The parameter ``--num`` specifies a card individual number. This number will be manged into the random seed so that
it serves as an identifier for a particular set of randomly generated parameters.
In the example above the parameters ``--mcc``, and ``--mnc`` are specified as well, since they identify the GSM
network where the cards should operate in, it is absolutely required to keep them static. ``pySim-prog`` will use
those parameters to generate a valid IMSI that thas the specified MCC/MNC at the beginning and a random tail.
Specifying the card type:
``pySim-prog`` usually autodetects the card type. In case auto detection does not work, it is possible to specify
the parameter ``--type``. The following card types are supported:
* Fairwaves-SIM
* fakemagicsim
* gialersim
* grcardsim
* magicsim
* OpenCells-SIM
* supersim
* sysmoISIM-SJA2
* sysmoISIM-SJA5
* sysmosim-gr1
* sysmoSIM-GR2
* sysmoUSIM-SJS1
* Wavemobile-SIM
Specifying the card reader:
It is most common to use ``pySim-prog`` together whith a PCSC reader. The PCSC reader number is specified via the
``--pcsc-device`` or ``-p`` option. However, other reader types (such as serial readers and modems) are supported. Use
the ``--help`` option of ``pySim-prog`` for more information.
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -i <IMSI> -s <ICCID>``
Card programming using CSV files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- one where they are generated from some minimal set :
To simplify the card programming process, ``pySim-prog`` also allows to read
the card parameters from a CSV file. When a CSV file is used as input, the
user does not have to craft an individual commandline for each card. Instead
all card related parameters are automatically drawn from the CSV file.
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -z <random_string_of_choice> -j <card_num>``
A CSV files may hold rows for multiple (hundreds or even thousands) of
cards. ``pySim-prog`` is able to identify the rows either by ICCID
(recommended as ICCIDs are normally not changed) or IMSI.
With <random_string_of_choice> and <card_num>, the soft will generate
'predictable' IMSI and ICCID, so make sure you choose them so as not to
conflict with anyone. (for eg. your name as <random_string_of_choice> and
0 1 2 ... for <card num>).
The CSV file format is a flexible format with mandatory and optional columns,
here the same rules as for the commandline parameters apply. The column names
match the command line options. The CSV file may also contain columns that are
unknown to pySim-prog, such as inventory numbers, nicknames or parameters that
are unrelated to the card programming process. ``pySim-prog`` will silently
ignore all unknown columns.
A CSV file may contain the following columns:
* name
* iccid (typically used as key)
* mcc
* mnc
* imsi (may be used as key, but not recommended)
* smsp
* ki
* opc
* acc
* pin_adm, adm1 or pin_adm_hex (must be present)
* msisdn
* epdgid
* epdgSelection
* pcscf
* ims_hdomain
* impi
* impu
* opmode
* fplmn
Due to historical reasons, and to maintain the compatibility between multiple different CSV file formats, the ADM pin
may be stored in three different columns. Only one of the three columns must be available.
* adm1: This column contains the ADM pin in numeric ASCII digit format. This format is the most common.
* pin_adm: Same as adm1, only the column name is different
* pin_adm_hex: If the ADM pin consists of raw HEX digits, rather then of numerical ASCII digits, then the ADM pin
can also be provided as HEX string using this column.
The following example shows a typical minimal example
::
"imsi","iccid","acc","ki","opc","adm1"
"999700000053010","8988211000000530108","0001","51ACE8BD6313C230F0BFE1A458928DF0","E5A00E8DE427E21B206526B5D1B902DF","65942330"
"999700000053011","8988211000000530116","0002","746AAFD7F13CFED3AE626B770E53E860","38F7CE8322D2A7417E0BBD1D7B1190EC","13445792"
"999700123053012","8988211000000530124","0004","D0DA4B7B150026ADC966DC637B26429C","144FD3AEAC208DFFF4E2140859BAE8EC","53540383"
"999700000053013","8988211000000530132","0008","52E59240ABAC6F53FF5778715C5CE70E","D9C988550DC70B95F40342298EB84C5E","26151368"
"999700000053014","8988211000000530140","0010","3B4B83CB9C5F3A0B41EBD17E7D96F324","D61DCC160E3B91F284979552CC5B4D9F","64088605"
"999700000053015","8988211000000530157","0020","D673DAB320D81039B025263610C2BBB3","4BCE1458936B338067989A06E5327139","94108841"
"999700000053016","8988211000000530165","0040","89DE5ACB76E06D14B0F5D5CD3594E2B1","411C4B8273FD7607E1885E59F0831906","55184287"
"999700000053017","8988211000000530173","0080","977852F7CEE83233F02E69E211626DE1","2EC35D48DBF2A99C07D4361F19EF338F","70284674"
The following commandline will instruct ``pySim-prog`` to use the provided CSV file as parameter source and the
ICCID (read from the card before programming) as a key to identify the card. To use the IMSI as a key, the parameter
``--read-imsi`` can be used instead of ``--read-iccid``. However, this option is only recommended to be used in very
specific corner cases.
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --read-iccid``
It is also possible to pick a row from the CSV file by manually providing an ICCID (option ``--iccid``) or an IMSI
(option ``--imsi``) that is then used as a key to find the matching row in the CSV file.
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --iccid <iccid_value>``
Writing CSV files
~~~~~~~~~~~~~~~~~
``pySim-prog`` is also able to generate CSV files that contain a subset of the parameters it has generated or received
from some other source (commandline, CSV-File). The generated file will be header-less and contain the following
columns:
* name
* iccid
* mcc
* mnc
* imsi
* smsp
* ki
* opc
A commandline that makes use of the CSV write feature would look like this:
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_input_csv_file> --read-iccid --source csv --write-csv <path_to_output_csv_file>``
Batch programming
~~~~~~~~~~~~~~~~~
In case larger card batches need to be programmed, it is possible to use the ``--batch`` parameter to run ``pySim-prog`` in batch mode.
The batch mode will prompt the user to insert a card. Once a card is detected in the reader, the programming is carried out. The user may then remove the card again and the process starts over. This allows for a quick and efficient card programming without permanent commandline interaction.
You also need to enter some parameters to select the device :
-t TYPE : type of card (supersim, magicsim, fakemagicsim or try 'auto')
-d DEV : Serial port device (default /dev/ttyUSB0)
-b BAUD : Baudrate (default 9600)
pySim-read
----------
``pySim-read`` allows to read some of the most important data items from a SIM
card. This means it will only read some files of the card, and will only read
files accessible to a normal user (without any special authentication)
``pySim-read`` allows you to read some data from a SIM card. It will only some files
of the card, and will only read files accessible to a normal user (without any special authentication)
These days, it is recommended to use the ``export`` command of ``pySim-shell``
instead. It performs a much more comprehensive export of all of the [standard]
files that can be found on the card. To get a human-readable decode instead of
the raw hex export, you can use ``export --json``.
These days, you should use the ``export`` command of ``pySim-shell``
instead. It performs a much more comprehensive export of all of the
[standard] files that can be found on the card. To get a human-readable
decode instead of the raw hex export, you can use ``export --json``.
Specifically, pySim-read will dump the following:

View File

@@ -74,6 +74,18 @@ at 9600 bps. These readers are sometimes called `Phoenix`.
:members:
pySim construct utilities
-------------------------
.. automodule:: pySim.construct
:members:
pySim TLV utilities
-------------------
.. automodule:: pySim.tlv
:members:
pySim utility functions
-----------------------

View File

@@ -19,21 +19,13 @@ support for profile personalization yet.
osmo-smdpp currently
* [by default] uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your
osmo-smdpp would be running at the host name `testsmdpplus1.example.com`. You can of course replace those
certificates with your own, whether SGP.26 derived or part of a *private root CA* setup with mathcing eUICCs.
* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
of the EID or whether it was donwloaded before. This is actually very useful for R&D and testing, as it
doesn't require you to generate new profiles all the time. This logic of course is unsuitable for
production usage.
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical (the ones that are stored in
the respective UPP `.der` files)
* 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 all of the mandatory certificate verification (it checks the certificate chain, but not
the expiration dates nor any CRL)
* does not evaluate/consider any *Confirmation Code*
* stores the sessions in an unencrypted *python shelve* and is hence leaking one-time key materials
* does not perform any certificate verification
* does not evaluate/consider any *Matching ID* or *Confirmation Code*
* stores the sessions in an unencrypted _python shelve_ and is hence leaking one-time key materials
used for profile encryption and signing.
@@ -79,22 +71,17 @@ If you use `nginx` as web server, you can use the following configuration snippe
You can of course achieve a similar functionality with apache, lighttpd or many other web server
software.
supplementary files
~~~~~~~~~~~~~~~~~~~
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
used; they are copied from GSMA SGP.26 v2. You can of course replace them with custom certificates
if you're operating eSIM with a *private root CA*.
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
commandline options
~~~~~~~~~~~~~~~~~~~
osmo-smdpp
~~~~~~~~~~
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
used; they are copied from GSMA SGP.26 v2.
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used.
DNS setup for your LPA
@@ -104,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.

View File

@@ -1,46 +0,0 @@
Remote access to an UICC/eUICC
==============================
To access a card with pySim-shell, it is not strictly necessary to have physical
access to it. There are solutions that allow remote access to UICC/eUICC cards.
In this section we will give a brief overview.
osmo-remsim
-----------
osmo-remsim is a suite of software programs enabling physical/geographic
separation of a cellular phone (or modem) on the one hand side and the
UICC/eUICC card on the other side.
Using osmo-remsim, you can operate an entire fleet of modems/phones, as well as
banks of SIM cards and dynamically establish or remove the connections between
modems/phones and cards.
To access remote cards with pySim-shell via osmo-remseim (RSPRO), the
provided libifd_remsim_client would be used to provide a virtual PC/SC reader
on the local machine. pySim-shell can then access this reader like any other
PC/SC reader.
More information on osmo-remsim can be found under:
* https://osmocom.org/projects/osmo-remsim/wiki
* https://ftp.osmocom.org/docs/osmo-remsim/master/osmo-remsim-usermanual.pdf
Android APDU proxy
------------------
Android APDU proxy is an Android app that provides a bridge between a host
computer and the UICC/eUICC slot of an Android smartphone.
The APDU proxy connects to VPCD server that runs on the remote host (in this
case the local machine where pySim-shell is running). The VPCD server then
provides a virtual PC/SC reader, that pySim-shell can access like any other
PC/SC reader.
On the Android side the UICC/eUICC is accessed via OMAPI (Open Mobile API),
which is available in Android since API level Android 8 (API level 29).
More information Android APDU proxy can be found under:
* https://gitea.osmocom.org/sim-card/android-apdu-proxy

View File

@@ -20,9 +20,6 @@ The pySim-shell interactive shell provides commands for
* if your card supports it, and you have the related privileges: resizing, creating, enabling and disabling of
files
* performing GlobalPlatform operations, including establishment of Secure Channel Protocol (SCP), Installing
applications, installing key material, etc.
* listing/enabling/disabling/deleting eSIM profiles on Consumer eUICC
By means of using the python ``cmd2`` module, various useful features improve usability:
@@ -69,15 +66,6 @@ Usage Examples
suci-tutorial
Advanced Topics
---------------
.. toctree::
:maxdepth: 1
:caption: Advanced pySIM-shell topics
card-key-provider
remote-access
cmd2 basics
-----------
@@ -145,32 +133,6 @@ optional files in some later 3GPP release) were not found on the card, or were i
trying to SELECT them.
fsdump
~~~~~~
.. argparse::
:module: pySim-shell
:func: PySimCommands.fsdump_parser
Please note that `fsdump` works relative to the current working
directory, so if you are in `MF`, then the dump will contain all known
files on the card. However, if you are in `ADF.ISIM`, only files below
that ADF will be part of the dump.
Furthermore, it is strongly advised to first enter the ADM1 pin
(`verify_adm`) to maximize the chance of having permission to read
all/most files.
One use case for this is to systematically analyze the differences between the contents of two
cards. To do this, you can create fsdumps of the two cards, and then use some general-purpose JSON
diffing tool like `jycm --show` (see https://github.com/eggachecat/jycm).
Example:
::
pySIM-shell (00:MF)> fsdump > /tmp/fsdump.json
pySIM-shell (00:MF)>
tree
~~~~
Display a tree of the card filesystem. It is important to note that this displays a tree
@@ -438,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
@@ -511,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
@@ -543,9 +482,6 @@ read_record_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.read_rec_dec_parser
If this command fails, it means that the record is not decodable, and you should use the :ref:`read_record`
command and proceed with manual decoding of the contents.
read_records
~~~~~~~~~~~~
@@ -560,9 +496,6 @@ read_records_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.read_recs_dec_parser
If this command fails, it means that the record[s] are not decodable, and you should use the :ref:`read_records`
command and proceed with manual decoding of the contents.
update_record
~~~~~~~~~~~~~
@@ -577,9 +510,6 @@ update_record_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.upd_rec_dec_parser
If this command fails, it means that the record is not encodable; please check your input and/or use the raw
:ref:`update_record` command.
edit_record_decoded
~~~~~~~~~~~~~~~~~~~
@@ -598,12 +528,6 @@ back to the record on the SIM card.
This allows for easy interactive modification of records.
If this command fails before the editor is spawned, it means that the current record contents is not decodable,
and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
encodable; please check your input and/or us the raw :ref:`update_record` comamdn.
decode_hex
~~~~~~~~~~
@@ -632,8 +556,6 @@ read_binary_decoded
:module: pySim.filesystem
:func: TransparentEF.ShellCommands.read_bin_dec_parser
If this command fails, it means that the file is not decodable, and you should use the :ref:`read_binary`
command and proceed with manual decoding of the contents.
update_binary
~~~~~~~~~~~~~
@@ -687,10 +609,6 @@ The below example demonstrates this by modifying the ciphering indicator field w
"extensions": "ff"
}
If this command fails, it means that the file is not encodable; please check your input and/or use the raw
:ref:`update_binary` command.
edit_binary_decoded
~~~~~~~~~~~~~~~~~~~
This command will read the selected binary EF, decode it to its JSON representation, save
@@ -704,12 +622,6 @@ to the SIM card.
This allows for easy interactive modification of file contents.
If this command fails before the editor is spawned, it means that the current file contents is not decodable,
and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
encodable; please check your input and/or us the raw :ref:`update_binary` comamdn.
decode_hex
~~~~~~~~~~
@@ -1002,7 +914,7 @@ aram_delete_all
~~~~~~~~~~~~~~~
This command will request deletion of all access rules stored within the
ARA-M applet. Use it with caution, there is no undo. Any rules later
intended must be manually inserted again using :ref:`aram_store_ref_ar_do`
intended must be manually inserted again using `aram_store_ref_ar_do`
GlobalPlatform commands
@@ -1017,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
--------------------

View File

@@ -1,56 +1,40 @@
Guide: Enabling 5G SUCI
=======================
========================
SUPI/SUCI Concealment is a feature of 5G-Standalone (SA) to encrypt the
IMSI/SUPI with a network operator public key. 3GPP Specifies two different
variants for this:
* SUCI calculation *in the UE*, using key data from the SIM
* SUCI calculation *in the UE*, using data from the SIM
* SUCI calculation *on the card itself*
pySim supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming
that your cards contain the required files, and you have the privileges/credentials to write to them.
This is the case using sysmocom sysmoISIM-SJA2 or any flavor of sysmoISIM-SJA5.
pySIM supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming that
your cards contain the required files, and you have the privileges/credentials to write to them. This is
the case using sysmocom sysmoISIM-SJA2 cards (or successor products).
There is no 3GPP/ETSI standard method for configuring *SUCI calculation on the card*; pySim currently
supports the vendor-specific method for the sysmoISIM-SJA5-S17).
In short, you can enable SUCI with these steps:
This document describes both methods.
* activate USIM **Service 124**
* make sure USIM **Service 125** is disabled
* store the public keys in **SUCI_Calc_Info**
* set the **Routing Indicator** (required)
If you want to disable the feature, you can just disable USIM Service 124 (and 125).
Technical References
~~~~~~~~~~~~~~~~~~~~
This guide covers the basic workflow of provisioning SIM cards with the 5G SUCI feature. For detailed information on the SUCI feature and file contents, the following documents are helpful:
* USIM files and structure: `3GPP TS 31.102 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131102/16.06.00_60/ts_131102v160600p.pdf>`__
* USIM tests (incl. file content examples): `3GPP TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
* Test keys for SUCI calculation: `3GPP TS 33.501 <https://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__
* USIM files and structure: `TS 31.102 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131102/16.06.00_60/ts_131102v160600p.pdf>`__
* USIM tests (incl. file content examples) `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
For specific information on sysmocom SIM cards, refer to
* the `sysmoISIM-SJA5 User Manual <https://sysmocom.de/manuals/sysmoisim-sja5-manual.pdf>`__ for the curent
sysmoISIM-SJA5 product
* the `sysmoISIM-SJA2 User Manual <https://sysmocom.de/manuals/sysmousim-manual.pdf>`__ for the older
sysmoISIM-SJA2 product
For specific information on sysmocom SIM cards, refer to Section 9.1 of the `sysmoUSIM User
Manual <https://www.sysmocom.de/manuals/sysmousim-manual.pdf>`__.
--------------
Enabling 5G SUCI *calculated in the UE*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In short, you can enable *SUCI calculation in the UE* with these steps:
* activate USIM **Service 124**
* make sure USIM **Service 125** is disabled
* store the public keys in **EF.SUCI_Calc_Info**
* set the **Routing Indicator** (required)
If you want to disable the feature, you can just disable USIM Service 124 (and 125) in `EF.UST`.
Admin PIN
---------
@@ -99,8 +83,8 @@ By default, the file is present but empty:
missing Protection Scheme Identifier List data object tag
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
The following JSON config defines the testfile from 3GPP TS 31.121, Section 4.9.4 with
test keys from 3GPP TS 33.501, Annex C.4. Highest priority (``0``) has a
The following JSON config defines the testfile from `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__ Section 4.9.4 with
test keys from `TS 33.501 <hhttps://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__ Annex C.4. Highest priority (``0``) has a
Profile-B (``identifier: 2``) key in key slot ``1``, which means the key
with ``hnet_pubkey_identifier: 27``.
@@ -113,7 +97,7 @@ with ``hnet_pubkey_identifier: 27``.
{"priority": 2, "identifier": 0, "key_index": 0}],
"hnet_pubkey_list": [
{"hnet_pubkey_identifier": 27,
"hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
"hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"},
{"hnet_pubkey_identifier": 30,
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
}
@@ -122,7 +106,7 @@ Write the config to file (must be single-line input as for now):
::
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
WARNING: These are TEST KEYS with publicly known/specified private keys, and hence unsafe for live/secure
deployments! For use in production networks, you need to generate your own set[s] of keys.
@@ -166,7 +150,7 @@ First, check out the USIM Service Table (UST):
pySIM-shell (00:MF/ADF.USIM/EF.UST)> read_binary_decoded
9000: beff9f9de73e0408400170730000002e00000000 -> [2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 25, 27, 28, 29, 33, 34, 35, 38, 39, 42, 43, 44, 45, 46, 51, 60, 71, 73, 85, 86, 87, 89, 90, 93, 94, 95, 122, 123, 124, 126]
.. list-table:: From 3GPP TS 31.102
.. list-table:: From TS31.102
:widths: 15 40
:header-rows: 1
@@ -200,7 +184,7 @@ be disabled.
USIM Error with 5G and sysmoISIM
--------------------------------
sysmoISIM-SJA2 come 5GS-enabled. By default however, the configuration stored
sysmoISIMs come 5GS-enabled. By default however, the configuration stored
in the card file-system is **not valid** for 5G networks: Service 124 is enabled,
but EF.SUCI_Calc_Info and EF.Routing_Indicator are empty files (hence
do not contain valid data).
@@ -209,62 +193,3 @@ At least for Qualcomms X55 modem, this results in an USIM error and the
whole modem shutting 5G down. If you dont need SUCI concealment but the
smartphone refuses to connect to any 5G network, try to disable the UST
service 124.
sysmoISIM-SJA5 are shipped with a more forgiving default, with valid EF.Routing_Indicator
contents and disabled Service 124
SUCI calculation by the USIM
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The SUCI calculation can also be performed by the USIM application on the UICC
directly. The UE then uses the GET IDENTITY command (see also 3GPP TS 31.102,
section 7.5) to retrieve a SUCI value.
The sysmoISIM-SJA5-S17 supports *SUCI calculation by the USIM*. The configuration
is not much different to the above described configuration of *SUCI calculation
in the UE*.
The main difference is how the key provisioning is done. When the SUCI
calculation is done by the USIM, then the key material is not accessed by the
UE. The specification (see also 3GPP TS 31.102, section 7.5.1.1), also does not
specify any file or file format to store the key material. This means the exact
way to perform the key provisioning is an implementation detail of the USIM
card application.
In the case of sysmoISIM-SJA5-S17, the key material for *SUCI calculation by the USIM* is stored in
`ADF.USIM/DF.SAIP/EF.SUCI_Calc_Info` (**not** in `ADF.USIM/DF.5GS/EF.SUCI_Calc_Info`!).
::
pySIM-shell (00:MF)> select MF
pySIM-shell (00:MF)> select ADF.USIM
pySIM-shell (00:MF/ADF.USIM)> select DF.SAIP
pySIM-shell (00:MF/ADF.USIM/DF.SAIP)> select EF.SUCI_Calc_Info
The file format is exactly the same as specified in 3GPP TS 31.102, section
4.4.11.8. This means the above described key provisioning procedure can be
applied without any changes, except that the file location is different.
To signal to the UE that the USIM is setup up for SUCI calculation, service
125 must be enabled in addition to service 124 (see also 3GPP TS 31.102,
section 5.3.48)
::
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 124
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 125
To verify that the SUCI calculation works as expected, it is possible to issue
a GET IDENTITY command using pySim-shell:
::
select ADF.USIM
get_identity
The USIM should then return a SUCI TLV Data object that looks like this:
::
SUCI TLV Data Object: 0199f90717ff021b027a2c58ce1c6b89df088a9eb4d242596dd75746bb5f3503d2cf58a7461e4fd106e205c86f76544e9d732226a4e1

View File

@@ -1,57 +0,0 @@
WebSocket Remote Card (WSRC)
============================
WSRC (*Web Socket Remote Card*) is a mechanism by which card readers can be made remotely available
via a computer network. The transport mechanism is (as the name implies) a WebSocket. This transport
method was chosen to be as firewall/NAT friendly as possible.
WSRC Network Architecture
-------------------------
In a WSRC network, there are three major elements:
* The **WSRC Card Client** which exposes a locally attached smart card (usually via a Smart Card Reader)
to a remote *WSRC Server*
* The **WSRC Server** manges incoming connections from both *WSRC Card Clients* as well as *WSRC User Clients*
* The **WSRC User Client** is a user application, like for example pySim-shell, which is accessing a remote
card by connecting to the *WSRC Server* which relays the information to the selected *WSRC Card Client*
WSRC Protocol
-------------
The WSRC protocl consits of JSON objects being sent over a websocket. The websocket communication itself
is based on HTTP and should usually operate via TLS for security reasons.
The detailed protocol is currently still WIP. The plan is to document it here.
pySim implementations
---------------------
wsrc_server
~~~~~~~~~~~~~~~~
.. argparse::
:filename: ../contrib/wsrc_server.py
:func: parser
:prog: contrib/wsrc_server.py
wsrc_card_client
~~~~~~~~~~~~~~~~
.. argparse::
:filename: ../contrib/wsrc_card_client.py
:func: parser
:prog: contrib/wsrc_card_client.py
pySim-shell
~~~~~~~~~~~
pySim-shell can talk to a remote card via WSRC if you use the *wsrc transport*, for example like this:
::
./pySim-shell.py --wsrc-eid 89882119900000000000000000007280 --wsrc-serer-url ws://localhost:4220/
You can specify `--wsrc-eid` or `--wsrc-iccid` to identify the remote eUICC or UICC you would like to select.

View File

@@ -32,13 +32,10 @@ from klein import Klein
from twisted.web.iweb import IRequest
import asn1tools
from osmocom.utils import h2b, b2h, swap_nibbles
from pySim.utils import h2b, b2h, swap_nibbles
import pySim.esim.rsp as rsp
from pySim.esim import saip, PMO
from pySim.esim.es8p import *
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
# HACK: make this configurable
DATA_DIR = './smdpp-data'
@@ -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,20 +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)
if output == None:
return ''
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
@@ -204,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']
@@ -219,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:
@@ -241,7 +325,7 @@ class SmDppHttpServer:
# Generate a TransactionID which is used to identify the ongoing RSP session. The TransactionID
# SHALL be unique within the scope and lifetime of each SM-DP+.
transactionId = uuid.uuid4().hex.upper()
transactionId = uuid.uuid4().hex
assert not transactionId in self.rss
# Generate a serverChallenge for eUICC authentication attached to the ongoing RSP session.
@@ -258,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]
@@ -294,7 +391,8 @@ class SmDppHttpServer:
r_ok = authenticateServerResp[1]
euiccSigned1 = r_ok['euiccSigned1']
euiccSigned1_bin = rsp.extract_euiccSigned1(authenticateServerResp_bin)
# TODO: use original data, don't re-encode?
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
euiccSignature1_bin = r_ok['euiccSignature1']
euiccCertificate_dec = r_ok['euiccCertificate']
# TODO: use original data, don't re-encode?
@@ -307,35 +405,29 @@ class SmDppHttpServer:
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
# TODO: Verify the validity of the eUICC certificate chain
# raise ApiError('8.1.3', '6.1', 'Verification failed')
# raise ApiError('8.1.3', '6.3', 'Expired')
# TODO: Verify that the Root Certificate of the eUICC certificate chain corresponds to the
# euiccCiPKIdToBeUsed or euiccCiPKIdToBeUsedV3
# raise ApiError('8.11.1', '3.9', 'Unknown')
# Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
# Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
raise ApiError('8.1', '6.1', 'Verification failed')
# Verify that the transactionId is known and relates to an ongoing RSP session. Otherwise, the SM-DP+
# SHALL return a status code "TransactionId - Unknown"
ss = self.rss.get(transactionId, None)
if ss is None:
raise ApiError('8.10.1', '3.9', 'Unknown')
ss.euicc_cert = euicc_cert
ss.eum_cert = eum_cert # TODO: do we need this in the state?
# Verify that the Root Certificate of the eUICC certificate chain corresponds to the
# euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3
if cert_get_auth_key_id(eum_cert) != ss.ci_cert_id:
raise ApiError('8.11.1', '3.9', 'Unknown')
# Verify the validity of the eUICC certificate chain
cs = CertificateSet(self.ci_get_cert_for_pkid(ss.ci_cert_id))
cs.add_intermediate_cert(eum_cert)
# TODO v3: otherCertsInChain
try:
cs.verify_cert_chain(euicc_cert)
except VerifyError:
raise ApiError('8.1.3', '6.1', 'Verification failed (certificate chain)')
# raise ApiError('8.1.3', '6.3', 'Expired')
# Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
# Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
raise ApiError('8.1', '6.1', 'Verification failed (euiccSignature1 over euiccSigned1)')
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
@@ -345,40 +437,10 @@ class SmDppHttpServer:
# serverChallenge returned by the eUICC. Otherwise, the SM-DP+ SHALL return a status code "eUICC -
# Verification failed".
if euiccSigned1['serverChallenge'] != ss.serverChallenge:
raise ApiError('8.1', '6.1', 'Verification failed (serverChallenge)')
# If ctxParams1 contains a ctxParamsForCommonAuthentication data object, the SM-DP+ Shall [...]
# TODO: We really do a very simplistic job here, this needs to be properly implemented later,
# considering all the various cases, profile state, etc.
if euiccSigned1['ctxParams1'][0] == 'ctxParamsForCommonAuthentication':
cpca = euiccSigned1['ctxParams1'][1]
matchingId = cpca.get('matchingId', None)
if not matchingId:
# TODO: check if any pending profile downloads for the EID
raise ApiError('8.2.6', '3.8', 'Refused')
if matchingId:
# look up profile based on matchingID. We simply check if a given file exists for now..
path = os.path.join(self.upp_dir, matchingId) + '.der'
# prevent directory traversal attack
if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
raise ApiError('8.2.6', '3.8', 'Refused')
if not os.path.isfile(path) or not os.access(path, os.R_OK):
raise ApiError('8.2.6', '3.8', 'Refused')
ss.matchingId = matchingId
with open(path, 'rb') as f:
pes = saip.ProfileElementSequence.from_der(f.read())
iccid_str = b2h(pes.get_pe_for_type('header').decoded['iccid'])
else:
# there's currently no other option in the ctxParams1 choice, so this cannot happen
raise ApiError('1.3.1', '2.2', 'ctxParams1 missing mandatory ctxParamsForCommonAuthentication')
# FIXME: we actually want to perform the profile binding herr, and read the profile metadat from the profile
raise ApiError('8.1', '6.1', 'Verification failed')
# Put together profileMetadata + _bin
ss.profileMetadata = ProfileMetadata(iccid_bin=h2b(swap_nibbles(iccid_str)), spn="OsmocomSPN", profile_name=matchingId)
# enable notifications for all operations
for event in ['enable', 'disable', 'delete']:
ss.profileMetadata.add_notification(event, self.server_hostname)
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
@@ -395,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']
@@ -412,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':
@@ -426,7 +499,8 @@ class SmDppHttpServer:
# Verify the euiccSignature2 computed over euiccSigned2 and smdpSignature2 using the PK.EUICC.SIG attached to the ongoing RSP session
euiccSigned2 = r_ok['euiccSigned2']
euiccSigned2_bin = rsp.extract_euiccSigned2(prepDownloadResp_bin)
# TODO: use original data, don't re-encode?
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
if not self._ecdsa_verify(ss.euicc_cert, r_ok['euiccSignature2'], euiccSigned2_bin + ss.smdpSignature2_do):
raise ApiError('8.1', '6.1', 'eUICC signature is invalid')
@@ -449,7 +523,7 @@ class SmDppHttpServer:
ss.host_id = b'mahlzeit'
# Generate Session Keys using the CRT, otPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
# Generate Session Keys using the CRT, opPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
euicc_public_key = ec.EllipticCurvePublicKey.from_encoded_point(ss.smdp_ot.curve, ss.euicc_otpk)
ss.shared_secret = ss.smdp_ot.exchange(ec.ECDH(), euicc_public_key)
print("shared_secret: %s" % b2h(ss.shared_secret))
@@ -457,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
@@ -474,18 +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"""
# SGP.22 Section 6.3: "A normal notification function execution status (MEP Notification)
# SHALL be indicated by the HTTP status code '204' (No Content) with an empty HTTP response body"
request.setResponseCode(204)
pendingNotification_bin = b64decode(content['pendingNotification'])
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
pendingNotification = content['pendingNotification']
print("Rx %s: %s" % pendingNotification)
if pendingNotification[0] == 'profileInstallationResult':
profileInstallRes = pendingNotification[1]
@@ -500,46 +582,36 @@ class SmDppHttpServer:
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
# verify eUICC signature
if not self._ecdsa_verify(ss.euicc_cert, profileInstallRes['euiccSignPIR'], pird_bin):
raise Exception('ECDSA signature verification failed on notification')
print("Unable to verify eUICC signature")
print("Profile Installation Final Result: ", pird['finalResult'])
# remove session state
del self.rss[transactionId]
elif pendingNotification[0] == 'otherSignedNotification':
otherSignedNotif = pendingNotification[1]
# TODO: use some kind of partially-parsed original data, don't re-encode?
euiccCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['euiccCertificate'])
eumCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['eumCertificate'])
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
ci_cert_id = cert_get_auth_key_id(eum_cert)
# Verify the validity of the eUICC certificate chain
cs = CertificateSet(self.ci_get_cert_for_pkid(ci_cert_id))
cs.add_intermediate_cert(eum_cert)
# TODO v3: otherCertsInChain
cs.verify_cert_chain(euicc_cert)
tbs_bin = rsp.asn1.encode('NotificationMetadata', otherSignedNotif['tbsOtherNotification'])
if not self._ecdsa_verify(euicc_cert, otherSignedNotif['euiccNotificationSignature'], tbs_bin):
raise Exception('ECDSA signature verification failed on notification')
other_notif = otherSignedNotif['tbsOtherNotification']
pmo = PMO.from_bitstring(other_notif['profileManagementOperation'])
eid = euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
iccid = other_notif.get('iccid', None)
if iccid:
iccid = swap_nibbles(b2h(iccid))
print("handleNotification: EID %s: %s of %s" % (eid, pmo, iccid))
# TODO
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
@@ -547,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':
@@ -578,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()
@@ -587,7 +706,7 @@ def main(argv):
args = parser.parse_args()
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=False)
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=True)
#hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
hs.app.run("localhost", 8000)

View File

@@ -25,7 +25,7 @@
#
import hashlib
import argparse
from optparse import OptionParser
import os
import random
import re
@@ -33,13 +33,12 @@ import sys
import traceback
import json
import csv
from osmocom.utils import h2b, swap_nibbles, rpad
from pySim.commands import SimCardCommands
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.transport import init_reader
from pySim.legacy.cards import _cards_classes, card_detect
from pySim.utils import derive_milenage_opc, calculate_luhn, dec_iccid
from pySim.profile.ts_51_011 import EF_AD
from pySim.utils import h2b, swap_nibbles, rpad, derive_milenage_opc, calculate_luhn, dec_iccid
from pySim.ts_51_011 import EF_AD
from pySim.legacy.ts_51_011 import EF
from pySim.card_handler import *
from pySim.utils import *
@@ -47,146 +46,169 @@ from pySim.utils import *
def parse_options():
parser = argparse.ArgumentParser()
argparse_add_reader_args(parser)
parser = OptionParser(usage="usage: %prog [options]")
parser.add_argument("-t", "--type", dest="type",
help="Card type (user -t list to view) [default: %(default)s]",
parser.add_option("-d", "--device", dest="device", metavar="DEV",
help="Serial Device for SIM access [default: %default]",
default="/dev/ttyUSB0",
)
parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
help="Baudrate used for SIM access [default: %default]",
default=9600,
)
parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
help="Which PC/SC reader number for SIM access",
default=None,
)
parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
default=None,
)
parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
help="Baudrate used for modem's port [default: %default]",
default=115200,
)
parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
default=None,
)
parser.add_option("-t", "--type", dest="type",
help="Card type (user -t list to view) [default: %default]",
default="auto",
)
parser.add_argument("-T", "--probe", dest="probe",
parser.add_option("-T", "--probe", dest="probe",
help="Determine card type",
default=False, action="store_true"
)
parser.add_argument("-a", "--pin-adm", dest="pin_adm",
parser.add_option("-a", "--pin-adm", dest="pin_adm",
help="ADM PIN used for provisioning (overwrites default)",
)
parser.add_argument("-A", "--pin-adm-hex", dest="pin_adm_hex",
parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
help="ADM PIN used for provisioning, as hex string (16 characters long",
)
parser.add_argument("-e", "--erase", dest="erase", action='store_true',
help="Erase beforehand [default: %(default)s]",
parser.add_option("-e", "--erase", dest="erase", action='store_true',
help="Erase beforehand [default: %default]",
default=False,
)
parser.add_argument("-S", "--source", dest="source",
help="Data Source[default: %(default)s]",
parser.add_option("-S", "--source", dest="source",
help="Data Source[default: %default]",
default="cmdline",
)
# if mode is "cmdline"
parser.add_argument("-n", "--name", dest="name",
help="Operator name [default: %(default)s]",
parser.add_option("-n", "--name", dest="name",
help="Operator name [default: %default]",
default="Magic",
)
parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
help="Country code [default: %(default)s]",
parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
help="Country code [default: %default]",
default=1,
)
parser.add_argument("-x", "--mcc", dest="mcc",
help="Mobile Country Code [default: %(default)s]",
parser.add_option("-x", "--mcc", dest="mcc", type="string",
help="Mobile Country Code [default: %default]",
default="901",
)
parser.add_argument("-y", "--mnc", dest="mnc",
help="Mobile Network Code [default: %(default)s]",
parser.add_option("-y", "--mnc", dest="mnc", type="string",
help="Mobile Network Code [default: %default]",
default="55",
)
parser.add_argument("--mnclen", dest="mnclen",
help="Length of Mobile Network Code [default: %(default)s]",
parser.add_option("--mnclen", dest="mnclen", type="choice",
help="Length of Mobile Network Code [default: %default]",
default="auto",
choices=["2", "3", "auto"],
)
parser.add_argument("-m", "--smsc", dest="smsc",
parser.add_option("-m", "--smsc", dest="smsc",
help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
)
parser.add_argument("-M", "--smsp", dest="smsp",
parser.add_option("-M", "--smsp", dest="smsp",
help="Raw SMSP content in hex [default: auto from SMSC]",
)
parser.add_argument("-s", "--iccid", dest="iccid", metavar="ID",
parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
help="Integrated Circuit Card ID",
)
parser.add_argument("-i", "--imsi", dest="imsi",
parser.add_option("-i", "--imsi", dest="imsi",
help="International Mobile Subscriber Identity",
)
parser.add_argument("--msisdn", dest="msisdn",
parser.add_option("--msisdn", dest="msisdn",
help="Mobile Subscriber Integrated Services Digital Number",
)
parser.add_argument("-k", "--ki", dest="ki",
parser.add_option("-k", "--ki", dest="ki",
help="Ki (default is to randomize)",
)
parser.add_argument("-o", "--opc", dest="opc",
parser.add_option("-o", "--opc", dest="opc",
help="OPC (default is to randomize)",
)
parser.add_argument("--op", dest="op",
parser.add_option("--op", dest="op",
help="Set OP to derive OPC from OP and KI",
)
parser.add_argument("--acc", dest="acc",
parser.add_option("--acc", dest="acc",
help="Set ACC bits (Access Control Code). not all card types are supported",
)
parser.add_argument("--opmode", dest="opmode",
parser.add_option("--opmode", dest="opmode", type="choice",
help="Set UE Operation Mode in EF.AD (Administrative Data)",
default=None,
choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
)
parser.add_argument("-f", "--fplmn", dest="fplmn", action="append",
parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
)
parser.add_argument("--epdgid", dest="epdgid",
parser.add_option("--epdgid", dest="epdgid",
help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
)
parser.add_argument("--epdgSelection", dest="epdgSelection",
parser.add_option("--epdgSelection", dest="epdgSelection",
help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
)
parser.add_argument("--pcscf", dest="pcscf",
parser.add_option("--pcscf", dest="pcscf",
help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
)
parser.add_argument("--ims-hdomain", dest="ims_hdomain",
parser.add_option("--ims-hdomain", dest="ims_hdomain",
help="Set IMS Home Network Domain Name in FQDN format",
)
parser.add_argument("--impi", dest="impi",
parser.add_option("--impi", dest="impi",
help="Set IMS private user identity",
)
parser.add_argument("--impu", dest="impu",
parser.add_option("--impu", dest="impu",
help="Set IMS public user identity",
)
parser.add_argument("--read-imsi", dest="read_imsi", action="store_true",
parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
help="Read the IMSI from the CARD", default=False
)
parser.add_argument("--read-iccid", dest="read_iccid", action="store_true",
parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
help="Read the ICCID from the CARD", default=False
)
parser.add_argument("-z", "--secret", dest="secret", metavar="STR",
parser.add_option("-z", "--secret", dest="secret", metavar="STR",
help="Secret used for ICCID/IMSI autogen",
)
parser.add_argument("-j", "--num", dest="num", type=int,
parser.add_option("-j", "--num", dest="num", type=int,
help="Card # used for ICCID/IMSI autogen",
)
parser.add_argument("--batch", dest="batch_mode",
help="Enable batch mode [default: %(default)s]",
parser.add_option("--batch", dest="batch_mode",
help="Enable batch mode [default: %default]",
default=False, action='store_true',
)
parser.add_argument("--batch-state", dest="batch_state", metavar="FILE",
parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
help="Optional batch state file",
)
# if mode is "csv"
parser.add_argument("--read-csv", dest="read_csv", metavar="FILE",
parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
help="Read parameters from CSV file rather than command line")
parser.add_argument("--write-csv", dest="write_csv", metavar="FILE",
parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
help="Append generated parameters in CSV file",
)
parser.add_argument("--write-hlr", dest="write_hlr", metavar="FILE",
parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
help="Append generated parameters to OpenBSC HLR sqlite3",
)
parser.add_argument("--dry-run", dest="dry_run",
parser.add_option("--dry-run", dest="dry_run",
help="Perform a 'dry run', don't actually program the card",
default=False, action="store_true")
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine")
options = parser.parse_args()
(options, args) = parser.parse_args()
if options.type == 'list':
for kls in _cards_classes:
@@ -197,13 +219,15 @@ def parse_options():
return options
if options.source == 'csv':
if (options.imsi is None) and (options.iccid is None) and (options.read_imsi is False) and (options.read_iccid is False):
parser.error("CSV mode requires one additional parameter: --read-iccid, --read-imsi, --iccid or --imsi")
if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
parser.error(
"CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
if options.read_csv is None:
parser.error("CSV mode requires a CSV input file")
elif options.source == 'cmdline':
if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
parser.error("If either IMSI or ICCID isn't specified, num is required")
parser.error(
"If either IMSI or ICCID isn't specified, num is required")
else:
parser.error("Only `cmdline' and `csv' sources supported")
@@ -218,6 +242,9 @@ def parse_options():
parser.error(
"Can't give ICCID/IMSI for batch mode, need to use automatic parameters ! see --num and --secret for more information")
if args:
parser.error("Extraneous arguments")
return options
@@ -622,9 +649,6 @@ def read_params_csv(opts, imsi=None, iccid=None):
def write_params_hlr(opts, params):
# SQLite3 OpenBSC HLR
# FIXME: The format of the osmo-hlr database has evolved, so that the code below will no longer work.
print("Warning: the database format of recent OsmoHLR versions is not compatible with pySim-prog!")
if opts.write_hlr:
import sqlite3
conn = sqlite3.connect(opts.write_hlr)
@@ -725,18 +749,16 @@ def process_card(scc, opts, first, ch):
card.erase()
card.reset()
cp = None
# Generate parameters
if opts.source == 'cmdline':
cp = gen_parameters(opts)
elif opts.source == 'csv':
imsi = None
iccid = None
if opts.read_iccid:
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
iccid = dec_iccid(res)
else:
iccid = opts.iccid
if opts.read_imsi:
elif opts.read_imsi:
(res, _) = scc.read_binary(EF['IMSI'])
imsi = swap_nibbles(res)[3:]
else:

View File

@@ -29,9 +29,7 @@ import random
import re
import sys
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
from pySim.profile.ts_51_011 import EF_SST_map, EF_AD
from pySim.ts_51_011 import EF_SST_map, EF_AD
from pySim.legacy.ts_51_011 import EF, DF
from pySim.ts_31_102 import EF_UST_map
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
@@ -42,8 +40,8 @@ from pySim.commands import SimCardCommands
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.exceptions import SwMatchError
from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
from pySim.utils import dec_imsi, dec_iccid
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
from pySim.utils import h2b, h2s, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
from pySim.legacy.utils import format_xplmn_w_act, dec_st
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@@ -88,7 +86,7 @@ if __name__ == '__main__':
scc.sel_ctrl = "0004"
# Testing for Classic SIM or UICC
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00" + "00")
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00")
if sw == '6e00':
# Just a Classic SIM
scc.cla_byte = "a0"

View File

@@ -21,7 +21,6 @@ from typing import List, Optional
import json
import traceback
import re
import cmd2
from packaging import version
@@ -48,18 +47,16 @@ from io import StringIO
from pprint import pprint as pp
from osmocom.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, is_hexstr, is_decimal
from osmocom.utils import is_hexstr_or_decimal, Hexstr
from osmocom.tlv import bertlv_parse_one
from pySim.exceptions import *
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, dec_iccid, sw_match
from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, bertlv_parse_one, sw_match
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr, dec_iccid
from pySim.utils import is_hexstr_or_decimal, is_hexstr, is_decimal
from pySim.card_handler import CardHandler, CardHandlerAuto
from pySim.filesystem import CardMF, CardEF, CardDF, CardADF, LinFixedEF, TransparentEF, BerTlvEF
from pySim.ts_102_221 import pin_names
from pySim.filesystem import CardMF, CardDF, CardADF
from pySim.ts_102_222 import Ts102222Commands
from pySim.gsm_r import DF_EIRENE
from pySim.cat import ProactiveCommand
from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
@@ -113,7 +110,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
self.conserve_write = True
self.json_pretty_print = True
self.apdu_trace = False
self.apdu_strict = False
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
onchange_cb=self._onchange_numeric_path))
@@ -122,9 +118,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
onchange_cb=self._onchange_apdu_trace))
self.add_settable(Settable2Compat('apdu_strict', bool,
'Enforce APDU responses according to ISO/IEC 7816-3, table 12', self,
onchange_cb=self._onchange_apdu_strict))
self.equip(card, rs)
def equip(self, card, rs):
@@ -167,9 +160,9 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
try:
self.lchan.select('MF/EF.ICCID', self)
rs.identity['ICCID'] = dec_iccid(self.lchan.read_binary()[0])
self.iccid = dec_iccid(self.lchan.read_binary()[0])
except:
rs.identity['ICCID'] = None
self.iccid = None
self.lchan.select('MF', self)
rc = True
@@ -201,13 +194,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
else:
self.card._scc._tp.apdu_tracer = None
def _onchange_apdu_strict(self, param_name, old, new):
if self.card:
if new == True:
self.card._scc._tp.apdu_strict = True
else:
self.card._scc._tp.apdu_strict = False
class Cmd2ApduTracer(ApduTracer):
def __init__(self, cmd2_app):
self.cmd2 = cmd2_app
@@ -219,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)> '
@@ -249,10 +231,8 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
self.equip(card, rs)
apdu_cmd_parser = argparse.ArgumentParser()
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
apdu_cmd_parser.add_argument('--expect-response-regex', help='match response against regex', type=str, default=None)
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
@cmd2.with_argparser(apdu_cmd_parser)
def do_apdu(self, opts):
@@ -265,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, apply_lchan = False)
else:
data, sw = self.lchan.scc.send_apdu(opts.APDU, apply_lchan = False)
data, sw = self.card._scc._tp.send_apdu(opts.APDU)
if data:
self.poutput("SW: %s, RESP: %s" % (sw, data))
else:
@@ -276,22 +253,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
if opts.expect_sw:
if not sw_match(sw, opts.expect_sw):
raise SwMatchError(sw, opts.expect_sw)
if opts.expect_response_regex:
response_regex_compiled = re.compile(opts.expect_response_regex)
if re.match(response_regex_compiled, data) is None:
raise ValueError("RESP does not match regex \'%s\'" % opts.expect_response_regex)
@cmd2.with_category(CUSTOM_CATEGORY)
def do_reset(self, opts):
"""Reset the Card."""
if self.rs is None:
# In case no runtime state is available we go the direct route
self.card._scc.reset_card()
atr = b2h(self.card._scc.get_atr())
else:
atr = self.rs.reset(self)
self.poutput('Card ATR: %s' % atr)
self.update_prompt()
class InterceptStderr(list):
def __init__(self):
@@ -369,7 +330,8 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
return -1
bulk_script_parser = argparse.ArgumentParser()
bulk_script_parser.add_argument('SCRIPT_PATH', help="path to the script file")
bulk_script_parser.add_argument(
'script_path', help="path to the script file")
bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
action='store_true')
bulk_script_parser.add_argument('--tries', type=int, default=2,
@@ -385,7 +347,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
"""Run script on multiple cards (bulk provisioning)"""
# Make sure that the script file exists and that it is readable.
if not os.access(opts.SCRIPT_PATH, os.R_OK):
if not os.access(opts.script_path, os.R_OK):
self.poutput("Invalid script file!")
return
@@ -395,7 +357,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
first = True
while 1:
# TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
# The rationale is: There may be a problem with the device, we do want to prevent that
# The ratinale is: There may be a problem with the device, we do want to prevent that
# all remaining cards are fired to the error bin. This is only relevant for situations
# with large stacks, probably we do not need this feature right now.
@@ -410,7 +372,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
os.system(opts.pre_card_action)
# process the card
rc = self._process_card(first, opts.SCRIPT_PATH)
rc = self._process_card(first, opts.script_path)
if rc == 0:
success_count = success_count + 1
self._show_success_sign()
@@ -462,13 +424,13 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
first = False
echo_parser = argparse.ArgumentParser()
echo_parser.add_argument('STRING', help="string to echo on the shell", nargs='+')
echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
@cmd2.with_argparser(echo_parser)
@cmd2.with_category(CUSTOM_CATEGORY)
def do_echo(self, opts):
"""Echo (print) a string on the console"""
self.poutput(' '.join(opts.STRING))
self.poutput(' '.join(opts.string))
@cmd2.with_category(CUSTOM_CATEGORY)
def do_version(self, opts):
@@ -517,25 +479,12 @@ class PySimCommands(CommandSet):
self._cmd.poutput(directory_str)
self._cmd.poutput("%d files" % len(selectables))
def __walk_action(self, action, filename, context, **kwargs):
# Changing the currently selected file while walking over the filesystem tree would disturb the
# walk, so we memorize the currently selected file here so that we can select it again after
# we have executed the action callback.
selected_file_before_action = self._cmd.lchan.selected_file
# Perform action
action(filename, context, **kwargs)
# When the action callback is done, make sure the file that was selected before is selected again.
if selected_file_before_action != self._cmd.lchan.selected_file:
self._cmd.lchan.select_file(selected_file_before_action, self._cmd)
def __walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
"""Recursively walk through the file system, starting at the currently selected DF"""
if isinstance(self._cmd.lchan.selected_file, CardDF):
if action_df:
self.__walk_action(action_df, self._cmd.lchan.selected_file.name, context, **kwargs)
action_df(context, **kwargs)
files = self._cmd.lchan.selected_file.get_selectables(
flags=['FNAMES', 'ANAMES'])
@@ -568,45 +517,143 @@ class PySimCommands(CommandSet):
# If the DF was skipped, we never have entered the directory
# below, so we must not move up.
if skip_df == False:
self.__walk(indent + 1, action_ef, action_df, context, **kwargs)
self._cmd.lchan.select_file(self._cmd.lchan.selected_file.parent, self._cmd)
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
parent = self._cmd.lchan.selected_file.parent
df = self._cmd.lchan.selected_file
adf = self._cmd.lchan.selected_adf
if isinstance(parent, CardMF) and (adf and adf.has_fs == False):
# Not every application that may be present on a GlobalPlatform card will support the SELECT
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
# "select by name" method, which means we can only select an application and not a file.
# The consequence of this is that we may get trapped in an application that does not have
# ISIM/USIM like file system support and the only way to leave that application is to select
# an ISIM/USIM application in order to get the file system access back.
#
# To automate this escape-route while traversing the file system we will check whether
# the parent file is the MF. When this is the case and the selected ADF has no file system
# support, we will select an arbitrary ADF that has file system support first and from there
# we will then select the MF.
for selectable in parent.get_selectables().items():
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
self._cmd.lchan.select(selectable[1].name, self._cmd)
break
self._cmd.lchan.select(df.get_mf().name, self._cmd)
else:
# Normal DF/ADF selection
fcp_dec = self._cmd.lchan.select("..", self._cmd)
elif action_ef:
self.__walk_action(action_ef, f, context, **kwargs)
df_before_action = self._cmd.lchan.selected_file
action_ef(f, context, **kwargs)
# When walking through the file system tree the action must not
# always restore the currently selected file to the file that
# was selected before executing the action() callback.
if df_before_action != self._cmd.lchan.selected_file:
raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
% (str(self._cmd.lchan.selected_file), str(df_before_action)))
def do_tree(self, opts):
"""Display a filesystem-tree with all selectable files"""
self.__walk()
self.walk()
def __export_file(self, filename, context, as_json):
""" Select and export a single file (EF, DF or ADF) """
def export_ef(self, filename, context, as_json):
""" Select and export a single elementary file (EF) """
context['COUNT'] += 1
df = self._cmd.lchan.selected_file
file = self._cmd.lchan.get_file_by_name(filename)
if file:
self._cmd.poutput(boxed_heading_str(file.fully_qualified_path_str(True)))
self._cmd.poutput("# directory: %s (%s)" % (file.fully_qualified_path_str(True),
file.fully_qualified_path_str(False)))
else:
# If this is called from self.__walk(), then it is ensured that the file exists.
raise RuntimeError("cannot export, file %s does not exist in the file system tree" % filename)
# The currently selected file (not the file we are going to export)
# must always be an ADF or DF. From this starting point we select
# the EF we want to export. To maintain consistency we will then
# select the current DF again (see comment below).
if not isinstance(df, CardDF):
raise RuntimeError(
"currently selected file %s is not a DF or ADF" % str(df))
df_path_list = df.fully_qualified_path(True)
df_path = df.fully_qualified_path_str(True)
df_path_fid = df.fully_qualified_path_str(False)
file_str = df_path + "/" + str(filename)
self._cmd.poutput(boxed_heading_str(file_str))
self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
try:
fcp_dec = self._cmd.lchan.select_file(file, self._cmd)
self._cmd.poutput("# file: %s (%s)" %
(self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
if isinstance(self._cmd.lchan.selected_file, CardEF):
self._cmd.poutput("# structure: %s" % str(self._cmd.lchan.selected_file_structure()))
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
self._cmd.poutput("# file: %s (%s)" % (
self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
structure = self._cmd.lchan.selected_file_structure()
self._cmd.poutput("# structure: %s" % str(structure))
self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
self._cmd.poutput("select " + self._cmd.lchan.selected_file.fully_qualified_path_str())
self._cmd.poutput(self._cmd.lchan.selected_file.export(as_json, self._cmd.lchan))
for f in df_path_list:
self._cmd.poutput("select " + str(f))
self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
if structure == 'transparent':
if as_json:
result = self._cmd.lchan.read_binary_dec()
self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
else:
result = self._cmd.lchan.read_binary()
self._cmd.poutput("update_binary " + str(result[0]))
elif structure == 'cyclic' or structure == 'linear_fixed':
# Use number of records specified in select response
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
if num_of_rec:
for r in range(1, num_of_rec + 1):
if as_json:
result = self._cmd.lchan.read_record_dec(r)
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
else:
result = self._cmd.lchan.read_record(r)
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
# When the select response does not return the number of records, read until we hit the
# first record that cannot be read.
else:
r = 1
while True:
try:
if as_json:
result = self._cmd.lchan.read_record_dec(r)
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
else:
result = self._cmd.lchan.read_record(r)
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
except SwMatchError as e:
# We are past the last valid record - stop
if e.sw_actual == "9402":
break
# Some other problem occurred
else:
raise e
r = r + 1
elif structure == 'ber_tlv':
tags = self._cmd.lchan.retrieve_tags()
for t in tags:
result = self._cmd.lchan.retrieve_data(t)
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
else:
raise RuntimeError(
'Unsupported structure "%s" of file "%s"' % (structure, filename))
except Exception as e:
bad_file_str = file.fully_qualified_path_str(True) + "/" + str(file.name) + ", " + str(e)
bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
self._cmd.poutput("# bad file: %s" % bad_file_str)
context['ERR'] += 1
context['BAD'].append(bad_file_str)
# When reading the file is done, make sure the parent file is
# selected again. This will be the usual case, however we need
# to check before since we must not select the same DF twice
if df != self._cmd.lchan.selected_file:
self._cmd.lchan.select(df.fid or df.aid, self._cmd)
self._cmd.poutput("#")
export_parser = argparse.ArgumentParser()
@@ -624,10 +671,10 @@ class PySimCommands(CommandSet):
exception_str_add = ""
if opts.filename:
self.__walk_action(self.__export_file, opts.filename, context, **kwargs_export)
self.export_ef(opts.filename, context, **kwargs_export)
else:
try:
self.__walk(0, self.__export_file, self.__export_file, context, **kwargs_export)
self.walk(0, self.export_ef, None, context, **kwargs_export)
except Exception as e:
print("# Stopping early here due to exception: " + str(e))
print("#")
@@ -655,202 +702,47 @@ class PySimCommands(CommandSet):
raise RuntimeError(
"unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
def __dump_file(self, filename, context, as_json):
""" Select and dump a single file (EF, DF or ADF) """
file = self._cmd.lchan.get_file_by_name(filename)
if file:
res = {
'path': file.fully_qualified_path(True)
}
else:
# If this is called from self.__walk(), then it is ensured that the file exists.
raise RuntimeError("cannot dump, file %s does not exist in the file system tree" % filename)
try:
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
# File control parameters (common for EF, DF and ADF files)
if not self._cmd.lchan.selected_file_fcp_hex:
# An application without a real ADF (like ADF.ARA-M) / filesystem
return
res['fcp_raw'] = str(self._cmd.lchan.selected_file_fcp_hex)
res['fcp'] = fcp_dec
# File structure and contents (EF only)
if isinstance(self._cmd.lchan.selected_file, CardEF):
structure = self._cmd.lchan.selected_file_structure()
if structure == 'transparent':
if as_json:
result = self._cmd.lchan.read_binary_dec()
body = result[0]
else:
result = self._cmd.lchan.read_binary()
body = str(result[0])
elif structure == 'cyclic' or structure == 'linear_fixed':
body = []
# Use number of records specified in select response
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
if num_of_rec:
for r in range(1, num_of_rec + 1):
if as_json:
result = self._cmd.lchan.read_record_dec(r)
body.append(result[0])
else:
result = self._cmd.lchan.read_record(r)
body.append(str(result[0]))
# When the select response does not return the number of records, read until we hit the
# first record that cannot be read.
else:
r = 1
while True:
try:
if as_json:
result = self._cmd.lchan.read_record_dec(r)
body.append(result[0])
else:
result = self._cmd.lchan.read_record(r)
body.append(str(result[0]))
except SwMatchError as e:
# We are past the last valid record - stop
if e.sw_actual == "9402":
break
# Some other problem occurred
raise e
r = r + 1
elif structure == 'ber_tlv':
tags = self._cmd.lchan.retrieve_tags()
body = {}
for t in tags:
result = self._cmd.lchan.retrieve_data(t)
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
body[t] = b2h(val)
else:
raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
res['body'] = body
except SwMatchError as e:
res['error'] = {
'sw_actual': e.sw_actual,
'sw_expected': e.sw_expected,
'message': e.description,
}
except Exception as e:
raise(e)
res['error'] = {
'message': str(e)
}
context['result']['files'][file.fully_qualified_path_str(True)] = res
fsdump_parser = argparse.ArgumentParser()
fsdump_parser.add_argument(
'--filename', type=str, default=None, help='only export specific (named) file')
fsdump_parser.add_argument(
'--json', action='store_true', help='export file contents as JSON (less reliable)')
@cmd2.with_argparser(fsdump_parser)
def do_fsdump(self, opts):
"""Export filesystem metadata and file contents of all files below current DF in
machine-readable json format. This is similar to "export", but much easier to parse by
downstream processing tools. You usually may want to call this from the MF and verify
the ADM1 PIN (if available) to maximize the amount of readable files."""
result = {
'name': self._cmd.card.name,
'atr': self._cmd.rs.identity['ATR'],
'eid': self._cmd.rs.identity.get('EID', None),
'iccid': self._cmd.rs.identity.get('ICCID', None),
'aids': {x.aid:{} for x in self._cmd.rs.mf.applications.values()},
'files': {},
}
context = {'result': result, 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
kwargs_export = {'as_json': opts.json}
exception_str_add = ""
if opts.filename:
self.__walk_action(self.__dump_file, opts.filename, context, **kwargs_export)
else:
# export an entire subtree
try:
self.__walk(0, self.__dump_file, self.__dump_file, context, **kwargs_export)
except Exception as e:
print("# Stopping early here due to exception: " + str(e))
print("#")
exception_str_add = ", also had to stop early due to exception:" + str(e)
#raise e
self._cmd.poutput_json(context['result'])
def do_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
if desc:
self._cmd.poutput("%s: %s" % (self._cmd.lchan.selected_file, desc))
self._cmd.poutput(desc)
else:
self._cmd.poutput("%s: no description available" % self._cmd.lchan.selected_file)
self._cmd.poutput(" file structure: %s" % self._cmd.lchan.selected_file_structure())
if isinstance(self._cmd.lchan.selected_file, LinFixedEF):
self._cmd.poutput(" record length:")
self._cmd.poutput(" minimum_length: %s" % str(self._cmd.lchan.selected_file.rec_len[0]))
self._cmd.poutput(" recommended_length: %s" % str(self._cmd.lchan.selected_file.rec_len[1]))
self._cmd.poutput(" actual_length: %s" % str(self._cmd.lchan.selected_file_record_len()))
self._cmd.poutput(" number of records: %s" % str(self._cmd.lchan.selected_file_num_of_rec()))
elif isinstance(self._cmd.lchan.selected_file, TransparentEF):
self._cmd.poutput(" file size:")
self._cmd.poutput(" minimum_size: %s" % str(self._cmd.lchan.selected_file.size[0]))
self._cmd.poutput(" recommended_size: %s" % str(self._cmd.lchan.selected_file.size[1]))
self._cmd.poutput(" actual_size: %s" % str(self._cmd.lchan.selected_file_size()))
elif isinstance(self._cmd.lchan.selected_file, BerTlvEF):
self._cmd.poutput(" file size:")
self._cmd.poutput(" minimum_size: %s" % str(self._cmd.lchan.selected_file.size[0]))
self._cmd.poutput(" recommended_size: %s" % str(self._cmd.lchan.selected_file.size[1]))
self._cmd.poutput(" actual_size: %s" % str(self._cmd.lchan.selected_file_size()))
self._cmd.poutput(" reserved_file_size: %s" % str(self._cmd.lchan.selected_file_reserved_file_size()))
self._cmd.poutput(" maximum_file_size: %s" % str(self._cmd.lchan.selected_file_maximum_file_size()))
self._cmd.poutput("no description available")
verify_adm_parser = argparse.ArgumentParser()
verify_adm_parser.add_argument('--pin-is-hex', action='store_true',
help='ADM pin value is specified as hex-string (not decimal)')
verify_adm_parser.add_argument('--adm-type',
choices=[x for x in pin_names.values() if x.startswith('ADM')],
help='Override ADM number. Default is card-model-specific, usually 1')
verify_adm_parser.add_argument('ADM', nargs='?', type=is_hexstr_or_decimal,
help='ADM pin value. If none given, CSV file will be queried')
verify_adm_parser.add_argument('ADM1', nargs='?', type=is_hexstr_or_decimal,
help='ADM1 pin value. If none given, CSV file will be queried')
@cmd2.with_argparser(verify_adm_parser)
def do_verify_adm(self, opts):
"""Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
to get write/update permissions to most of the files on SIM cards.
"""
if opts.adm_type:
# pylint: disable=unsubscriptable-object
adm_chv_num = pin_names.inverse[opts.adm_type]
else:
adm_chv_num = self._cmd.card._adm_chv_num
if opts.ADM:
to get write/update permissions to most of the files on SIM cards.
Currently only ADM1 is supported."""
if opts.ADM1:
# use specified ADM-PIN
if opts.pin_is_hex:
pin_adm = sanitize_pin_adm(None, opts.ADM)
else:
pin_adm = sanitize_pin_adm(opts.ADM)
pin_adm = sanitize_pin_adm(opts.ADM1)
else:
iccid = self._cmd.rs.identity['ICCID']
adm_type = opts.adm_type or 'ADM1'
# try to find an ADM-PIN if none is specified
result = card_key_provider_get_field(adm_type, key='ICCID', value=iccid)
if opts.pin_is_hex or (result and len(result) > 8):
pin_adm = sanitize_pin_adm(None, result)
else:
pin_adm = sanitize_pin_adm(result)
result = card_key_provider_get_field(
'ADM1', key='ICCID', value=self._cmd.iccid)
pin_adm = sanitize_pin_adm(result)
if pin_adm:
self._cmd.poutput("found %s '%s' for ICCID '%s'" % (adm_type, result, iccid))
self._cmd.poutput(
"found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
else:
raise ValueError("cannot find %s for ICCID '%s'" % (adm_type, iccid))
raise ValueError(
"cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
if pin_adm:
self._cmd.lchan.scc.verify_chv(adm_chv_num, h2b(pin_adm))
self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
else:
raise ValueError("error: cannot authenticate, no adm-pin!")
@@ -858,17 +750,13 @@ class PySimCommands(CommandSet):
"""Display information about the currently inserted card"""
self._cmd.poutput("Card info:")
self._cmd.poutput(" Name: %s" % self._cmd.card.name)
self._cmd.poutput(" ATR: %s" % self._cmd.rs.identity['ATR'].lower())
eid = self._cmd.rs.identity.get('EID', None)
if eid:
self._cmd.poutput(" EID: %s" % eid.lower())
self._cmd.poutput(" ICCID: %s" % self._cmd.rs.identity['ICCID'].lower())
self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte.lower())
self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl.lower())
if len(self._cmd.rs.mf.applications) > 0:
self._cmd.poutput(" AIDs:")
for a in self._cmd.rs.mf.applications:
self._cmd.poutput(" %s" % a.lower())
self._cmd.poutput(" ATR: %s" % b2h(self._cmd.lchan.scc.get_atr()))
self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte)
self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl)
self._cmd.poutput(" AIDs:")
for a in self._cmd.rs.mf.applications:
self._cmd.poutput(" %s" % a)
@with_default_category('ISO7816 Commands')
class Iso7816Commands(CommandSet):
@@ -893,64 +781,69 @@ class Iso7816Commands(CommandSet):
index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
def get_code(self, code, field):
"""Use code either directly or try to get it from external data source using the provided field name"""
if code is not None:
def get_code(self, code):
"""Use code either directly or try to get it from external data source"""
auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
if str(code).upper() not in auto:
return sanitize_pin_adm(code)
iccid = self._cmd.rs.identity['ICCID']
result = card_key_provider_get_field(field, key='ICCID', value=iccid)
result = card_key_provider_get_field(
str(code), key='ICCID', value=self._cmd.iccid)
result = sanitize_pin_adm(result)
if result:
self._cmd.poutput("found %s '%s' for ICCID '%s'" % (field, result, iccid))
self._cmd.poutput("found %s '%s' for ICCID '%s'" %
(code.upper(), result, self._cmd.iccid))
else:
raise RuntimeError("cannot find %s for ICCID '%s'" % (field, iccid))
self._cmd.poutput("cannot find %s for ICCID '%s'" %
(code.upper(), self._cmd.iccid))
return result
verify_chv_parser = argparse.ArgumentParser()
verify_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
verify_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
help='PIN code value. If none given, CSV file will be queried')
verify_chv_parser.add_argument(
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(verify_chv_parser)
def do_verify_chv(self, opts):
"""Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
PIN2."""
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.lchan.scc.verify_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV verification successful")
unblock_chv_parser = argparse.ArgumentParser()
unblock_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
unblock_chv_parser.add_argument('PUK', nargs='?', type=is_decimal,
help='PUK code value. If none given, CSV file will be queried')
unblock_chv_parser.add_argument('NEWPIN', nargs='?', type=is_decimal,
help='PIN code value. If none given, CSV file will be queried')
unblock_chv_parser.add_argument(
'puk_code', type=is_decimal, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
unblock_chv_parser.add_argument(
'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(unblock_chv_parser)
def do_unblock_chv(self, opts):
"""Unblock PIN code using specified PUK code"""
new_pin = self.get_code(opts.NEWPIN, "PIN" + str(opts.pin_nr))
puk = self.get_code(opts.PUK, "PUK" + str(opts.pin_nr))
new_pin = self.get_code(opts.new_pin_code)
puk = self.get_code(opts.puk_code)
(data, sw) = self._cmd.lchan.scc.unblock_chv(
opts.pin_nr, h2b(puk), h2b(new_pin))
self._cmd.poutput("CHV unblock successful")
change_chv_parser = argparse.ArgumentParser()
change_chv_parser.add_argument('NEWPIN', nargs='?', type=is_decimal,
help='PIN code value. If none given, CSV file will be queried')
change_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
help='PIN code value. If none given, CSV file will be queried')
change_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
change_chv_parser.add_argument(
'pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
change_chv_parser.add_argument(
'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(change_chv_parser)
def do_change_chv(self, opts):
"""Change PIN code to a new PIN code"""
new_pin = self.get_code(opts.NEWPIN, "PIN" + str(opts.pin_nr))
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
new_pin = self.get_code(opts.new_pin_code)
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.lchan.scc.change_chv(
opts.pin_nr, h2b(pin), h2b(new_pin))
self._cmd.poutput("CHV change successful")
@@ -958,26 +851,26 @@ class Iso7816Commands(CommandSet):
disable_chv_parser = argparse.ArgumentParser()
disable_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
disable_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
help='PIN code value. If none given, CSV file will be queried')
disable_chv_parser.add_argument(
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(disable_chv_parser)
def do_disable_chv(self, opts):
"""Disable PIN code using specified PIN code"""
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.lchan.scc.disable_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV disable successful")
enable_chv_parser = argparse.ArgumentParser()
enable_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
enable_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
help='PIN code value. If none given, CSV file will be queried')
enable_chv_parser.add_argument(
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(enable_chv_parser)
def do_enable_chv(self, opts):
"""Enable PIN code using specified PIN code"""
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
pin = self.get_code(opts.pin_code)
(data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV enable successful")
@@ -989,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]:
@@ -1007,7 +893,7 @@ class Iso7816Commands(CommandSet):
open_chan_parser = argparse.ArgumentParser()
open_chan_parser.add_argument(
'chan_nr', type=int, default=1, choices=range(1,16), help='Channel Number')
'chan_nr', type=int, default=0, help='Channel Number')
@cmd2.with_argparser(open_chan_parser)
def do_open_channel(self, opts):
@@ -1019,7 +905,7 @@ class Iso7816Commands(CommandSet):
close_chan_parser = argparse.ArgumentParser()
close_chan_parser.add_argument(
'chan_nr', type=int, default=1, choices=range(1,16), help='Channel Number')
'chan_nr', type=int, default=0, help='Channel Number')
@cmd2.with_argparser(close_chan_parser)
def do_close_channel(self, opts):
@@ -1031,14 +917,14 @@ class Iso7816Commands(CommandSet):
switch_chan_parser = argparse.ArgumentParser()
switch_chan_parser.add_argument(
'chan_nr', type=int, default=0, choices=range(0,16), help='Channel Number')
'chan_nr', type=int, default=0, help='Channel Number')
@cmd2.with_argparser(switch_chan_parser)
def do_switch_channel(self, opts):
"""Switch currently active logical channel."""
self._cmd.lchan.unregister_cmds(self._cmd)
self._cmd.lchan._select_pre(self._cmd)
self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
self._cmd.lchan.register_cmds(self._cmd)
self._cmd.lchan._select_post(self._cmd)
self._cmd.update_prompt()
def do_status(self, opts):
@@ -1064,14 +950,8 @@ global_group.add_argument('--script', metavar='PATH', default=None,
help='script with pySim-shell commands to be executed automatically at start-up')
global_group.add_argument('--csv', metavar='FILE',
default=None, help='Read card data from CSV file')
global_group.add_argument('--csv-column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
help='per-CSV-column AES transport key')
global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine")
global_group.add_argument("--noprompt", help="Run in non interactive mode",
action='store_true', default=False)
global_group.add_argument("--skip-card-init", help="Skip all card/profile initialization",
action='store_true', default=False)
adm_group = global_group.add_mutually_exclusive_group()
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
@@ -1079,8 +959,6 @@ adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', de
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
help='ADM PIN used for provisioning, as hex string (16 characters long)')
option_parser.add_argument('-e', '--execute-command', action='append', default=[],
help='A pySim-shell command that will be executed at startup')
option_parser.add_argument("command", nargs='?',
help="A pySim-shell command that would optionally be executed at startup")
option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
@@ -1089,20 +967,22 @@ option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
if __name__ == '__main__':
startup_errors = False
# Parse options
opts = option_parser.parse_args()
# If a script file is specified, be sure that it actually exists
if opts.script:
if not os.access(opts.script, os.R_OK):
print("Invalid script file!")
sys.exit(2)
# Register csv-file as card data provider, either from specified CSV
# or from CSV file in home directory
csv_column_keys = {}
for par in opts.csv_column_key:
name, key = par.split(':')
csv_column_keys[name] = key
csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
if opts.csv:
card_key_provider_register(CardKeyProviderCsv(opts.csv, csv_column_keys))
card_key_provider_register(CardKeyProviderCsv(opts.csv))
if os.path.isfile(csv_default):
card_key_provider_register(CardKeyProviderCsv(csv_default, csv_column_keys))
card_key_provider_register(CardKeyProviderCsv(csv_default))
# Init card reader driver
sl = init_reader(opts, proactive_handler = Proact())
@@ -1117,19 +997,20 @@ if __name__ == '__main__':
# is no card in the reader or the card is unresponsive. PysimApp is
# able to tolerate and recover from that.
try:
rs, card = init_card(sl, opts.skip_card_init)
app = PysimApp(card, rs, sl, ch)
rs, card = init_card(sl)
app = PysimApp(card, rs, sl, ch, opts.script)
except:
startup_errors = True
print("Card initialization (%s) failed with an exception:" % str(sl))
print("---------------------8<---------------------")
traceback.print_exc()
print("---------------------8<---------------------")
if not opts.noprompt:
print("(you may still try to recover from this manually by using the 'equip' command.)")
print(" it should also be noted that some readers may behave strangely when no card")
print(" is inserted.)")
print("")
print("(you may still try to recover from this manually by using the 'equip' command.)")
print(
" it should also be noted that some readers may behave strangely when no card")
print(" is inserted.)")
print("")
if opts.script:
print("will not execute startup script due to card initialization errors!")
app = PysimApp(None, None, sl, ch)
# If the user supplies an ADM PIN at via commandline args authenticate
@@ -1141,38 +1022,9 @@ if __name__ == '__main__':
try:
card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
except Exception as e:
startup_errors = True
print("ADM verification (%s) failed with an exception:" % str(pin_adm))
print("---------------------8<---------------------")
print(e)
print("---------------------8<---------------------")
# Run optional commands
for c in opts.execute_command:
if not startup_errors:
app.onecmd_plus_hooks(c)
else:
print("Errors during startup, refusing to execute command (%s)" % c)
# Run optional command
if opts.command:
if not startup_errors:
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
else:
print("Errors during startup, refusing to execute command (%s)" % opts.command)
# Run optional script file
if opts.script:
if not startup_errors:
if not os.access(opts.script, os.R_OK):
print("Error: script file (%s) not readable!" % opts.script)
startup_errors = True
else:
app.onecmd_plus_hooks('{} {}'.format('run_script', opts.script), add_to_history = False)
else:
print("Errors during startup, refusing to execute script (%s)" % opts.script)
if not opts.noprompt:
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
else:
app.cmdloop()
elif startup_errors:
sys.exit(2)

View File

@@ -8,21 +8,17 @@ from pprint import pprint as pp
from pySim.apdu import *
from pySim.runtime import RuntimeState
from osmocom.utils import JsonEncoder
from pySim.cards import UiccCardBase
from pySim.commands import SimCardCommands
from pySim.profile import CardProfile
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.ts_102_221 import CardProfileUICC
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
from pySim.euicc import CardApplicationISDR, CardApplicationECASD
from pySim.transport import LinkBase
from pySim.apdu_source.gsmtap import GsmtapApduSource
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
from pySim.apdu_source.tca_loader_log import TcaLoaderLogApduSource
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
@@ -55,7 +51,7 @@ class DummySimLink(LinkBase):
def __str__(self):
return "dummy"
def _send_apdu(self, pdu):
def _send_apdu_raw(self, pdu):
#print("DummySimLink-apdu: %s" % pdu)
return [], '9000'
@@ -65,7 +61,7 @@ class DummySimLink(LinkBase):
def disconnect(self):
pass
def _reset_card(self):
def reset_card(self):
return 1
def get_atr(self):
@@ -82,8 +78,6 @@ class Tracer:
profile = CardProfileUICC()
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
profile.add_application(CardApplicationISDR())
profile.add_application(CardApplicationECASD())
scc = SimCardCommands(transport=DummySimLink())
card = UiccCardBase(scc)
self.rs = RuntimeState(card, profile)
@@ -99,8 +93,7 @@ class Tracer:
"""Output a single decoded + processed ApduCommand."""
if self.show_raw_apdu:
print(apdu)
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id,
inst.col_sw, json.dumps(inst.processed, cls=JsonEncoder)))
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id, inst.col_sw, inst.processed))
print("===============================")
def format_reset(self, apdu: CardReset):
@@ -185,11 +178,6 @@ parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
help='Name of the network interface to capture on')
parser_tcaloader_log = subparsers.add_parser('tca-loader-log', help="""
Read APDUs from a TCA Loader log file.""")
parser_tcaloader_log.add_argument('-f', '--log-file', required=True,
help='Name of te log file to be read')
if __name__ == '__main__':
opts = option_parser.parse_args()
@@ -203,10 +191,6 @@ if __name__ == '__main__':
s = PysharkRsproLive(opts.interface)
elif opts.source == 'gsmtap-pyshark-pcap':
s = PysharkGsmtapPcap(opts.pcap_file)
elif opts.source == 'tca-loader-log':
s = TcaLoaderLogApduSource(opts.log_file)
else:
raise ValueError("unsupported source %s", opts.source)
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
show_raw_apdu=opts.show_raw_apdu)

View File

@@ -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
@@ -9,7 +10,7 @@ is far too simplistic, while this decoder can utilize all of the information
we already know in pySim about the filesystem structure, file encoding, etc.
"""
# (C) 2022-2024 by Harald Welte <laforge@osmocom.org>
# (C) 2022 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -26,14 +27,14 @@ we already know in pySim about the filesystem structure, file encoding, etc.
import abc
from termcolor import colored
import typing
from typing import List, Dict, Optional
from termcolor import colored
from construct import Byte, GreedyBytes
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.utils import *
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
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
@@ -51,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))
@@ -149,10 +150,8 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
# fall-back constructs if the derived class provides no override
_construct_p1 = Byte
_construct_p2 = Byte
_construct = GreedyBytes
_construct_rsp = GreedyBytes
_tlv = None
_tlv_rsp = None
_construct = HexAdapter(GreedyBytes)
_construct_rsp = HexAdapter(GreedyBytes)
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
"""Instantiate a new ApduCommand from give cmd + resp."""
@@ -188,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())
@@ -232,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']
@@ -271,7 +275,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
"""Does the given CLA match the CLA list of the command?."""
if not isinstance(cla, str):
cla = '%02X' % cla
cla = cla.upper()
cla = cla.lower()
# see https://github.com/PyCQA/pylint/issues/7219
# pylint: disable=no-member
for cla_match in cls._cla:
@@ -281,7 +285,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
cla_masked += 'X'
else:
cla_masked += cla[i]
if cla_masked == cla_match.upper():
if cla_masked == cla_match:
return True
return False
@@ -291,26 +295,17 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
if callable(method):
return method()
else:
return self._cmd_to_dict()
def _cmd_to_dict(self) -> Dict:
"""back-end function performing automatic decoding using _construct / _tlv."""
r = {}
method = getattr(self, '_decode_p1p2', None)
if callable(method):
r = self._decode_p1p2()
else:
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
r['p3'] = self.p3
if self.cmd_data:
if self._tlv:
ie = self._tlv()
ie.from_tlv(self.cmd_data)
r['body'] = ie.to_dict()
r = {}
method = getattr(self, '_decode_p1p2', None)
if callable(method):
r = self._decode_p1p2()
else:
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
r['p3'] = self.p3
if self.cmd_data:
r['body'] = parse_construct(self._construct, self.cmd_data)
return r
return r
def rsp_to_dict(self) -> Dict:
"""Convert the Response part of the APDU to a dict."""
@@ -320,12 +315,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
else:
r = {}
if self.rsp_data:
if self._tlv_rsp:
ie = self._tlv_rsp()
ie.from_tlv(self.rsp_data)
r['body'] = ie.to_dict()
else:
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
r['sw'] = b2h(self.sw)
return r
@@ -438,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):
@@ -461,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__)

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""APDU definition/decoder of GlobalPLatform Card Spec (currently 2.1.1)
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
(C) 2022 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,11 +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 FlagsEnum, Struct
from osmocom.tlv import flatten_dict_lists
from osmocom.construct import *
from pySim.apdu import ApduCommand, ApduCommandSet
from pySim.global_platform import InstallParameters
class GpDelete(ApduCommand, n='DELETE', ins=0xE4, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
@@ -44,29 +40,8 @@ class GpGetDataCB(ApduCommand, n='GET DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
class GpGetStatus(ApduCommand, n='GET STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
# GPCS Section 11.5.2
class GpInstall(ApduCommand, n='INSTALL', ins=0xE6, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
_construct_p1 = FlagsEnum(Byte, more_commands=0x80, for_registry_update=0x40,
for_personalization=0x20, for_extradition=0x10,
for_make_selectable=0x08, for_install=0x04, for_load=0x02)
_construct_p2 = Enum(Byte, no_info_provided=0x00, beginning_of_combined=0x01,
end_of_combined=0x03)
_construct = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
'module_aid'/Prefixed(Int8ub, GreedyBytes),
'application_aid'/Prefixed(Int8ub, GreedyBytes),
'privileges'/Prefixed(Int8ub, GreedyBytes),
'install_parameters'/Prefixed(Int8ub, GreedyBytes), # TODO: InstallParameters
'install_token'/Prefixed(Int8ub, GreedyBytes))
def _decode_cmd(self):
# first use _construct* above
res = self._cmd_to_dict()
# then do TLV decode of install_parameters
ip = InstallParameters()
ip.from_tlv(res['body']['install_parameters'])
res['body']['install_parameters'] = flatten_dict_lists(ip.to_dict())
return res
class GpLoad(ApduCommand, n='LOAD', ins=0xE8, cla=['8X', 'CX', 'EX']):
_apdu_case = 4

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
(C) 2022 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,18 +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 osmocom.utils import i2h
from osmocom.construct import *
from pySim.construct import *
from pySim.filesystem import *
from pySim.runtime import RuntimeLchan
from pySim.apdu import ApduCommand, ApduCommandSet
from pySim import cat
from typing import Optional, Dict, Tuple
logger = logging.getLogger(__name__)
@@ -100,7 +94,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
logger.warning('SELECT UNKNOWN FID %s', file_hex)
elif mode == 'df_name':
# Select by AID (can be sub-string!)
aid = b2h(self.cmd_dict['body'])
aid = self.cmd_dict['body']
sels = lchan.rs.mf.get_app_selectables(['AIDS'])
adf = self._find_aid_substr(sels, aid)
if adf:
@@ -109,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
@@ -116,7 +111,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
self.file = lchan.selected_file
if 'body' in self.rsp_dict:
# not every SELECT is asking for the FCP in response...
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
return None
@@ -129,7 +124,7 @@ class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
def process_on_lchan(self, lchan):
if self.cmd_dict['p2'] == 'response_like_select':
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
def _decode_binary_p1p2(p1, p2) -> Dict:
ret = {}
@@ -295,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):
@@ -309,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):
@@ -321,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):
@@ -332,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):
@@ -344,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):
@@ -397,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']):
@@ -420,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)))
@@ -437,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']):
@@ -460,17 +460,14 @@ class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
# TS 102 221 Section 11.2.2 / TS 102 223
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
_apdu_case = 4
_tlv = cat.EventCollection
# TS 102 221 Section 11.2.3 / TS 102 223
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
_apdu_case = 2
_tlv_rsp = cat.ProactiveCommand
# TS 102 221 Section 11.2.3 / TS 102 223
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
_apdu_case = 3
_tlv = cat.TerminalResponse
# TS 102 221 Section 11.3.1
class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):

View File

@@ -1,60 +0,0 @@
# coding=utf-8
"""APDU definitions/decoders of ETSI TS 102 222.
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
from construct import Struct
from osmocom.construct import *
from pySim.apdu import ApduCommand, ApduCommandSet
from pySim.ts_102_221 import FcpTemplate
logger = logging.getLogger(__name__)
# TS 102 222 Section 6.3
class CreateFile(ApduCommand, n='CREATE FILE', ins=0xE0, cla=['0X', '4X', 'EX']):
_apdu_case = 3
_tlv = FcpTemplate
# TS 102 222 Section 6.4
class DeleteFile(ApduCommand, n='DELETE FILE', ins=0xE4, cla=['0X', '4X']):
_apdu_case = 3
_construct = Struct('file_id'/Bytes(2))
# TS 102 222 Section 6.7
class TerminateDF(ApduCommand, n='TERMINATE DF', ins=0xE6, cla=['0X', '4X']):
_apdu_case = 1
# TS 102 222 Section 6.8
class TerminateEF(ApduCommand, n='TERMINATE EF', ins=0xE8, cla=['0X', '4X']):
_apdu_case = 1
# TS 102 222 Section 6.9
class TerminateCardUsage(ApduCommand, n='TERMINATE CARD USAGE', ins=0xFE, cla=['0X', '4X']):
_apdu_case = 1
# TS 102 222 Section 6.10
class ResizeFile(ApduCommand, n='RESIZE FILE', ins=0xD4, cla=['8X', 'CX', 'EX']):
_apdu_case = 3
_construct_p1 = Enum(Byte, mode_0=0, mode_1=1)
_tlv = FcpTemplate
ApduCommands = ApduCommandSet('TS 102 222', cmds=[CreateFile, DeleteFile, TerminateDF,
TerminateEF, TerminateCardUsage, ResizeFile])

View File

@@ -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 osmocom.construct import *
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
@@ -42,28 +44,28 @@ class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '
BitsInteger(4),
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
vgcs_vbs=2, gba=4))
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, Bytes(this._autn_len)))
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/Bytes(this._vsid_len),
'_vkid_len'/Int8ub, 'vk_id'/Bytes(this._vkid_len),
'_vstk_rand_len'/Int8ub, 'vstk_rand'/Bytes(this._vstk_rand_len))
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
'_autn_len'/Int8ub, 'autn'/Bytes(this._autn_len))
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/Bytes(this._naf_id_len),
'_impi_len'/Int8ub, 'impi'/Bytes(this._impi_len))
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, HexAdapter(Bytes(this._autn_len))))
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/HexAdapter(Bytes(this._vsid_len)),
'_vkid_len'/Int8ub, 'vk_id'/HexAdapter(Bytes(this._vkid_len)),
'_vstk_rand_len'/Int8ub, 'vstk_rand'/HexAdapter(Bytes(this._vstk_rand_len)))
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
'_autn_len'/Int8ub, 'autn'/HexAdapter(Bytes(this._autn_len)))
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/HexAdapter(Bytes(this._naf_id_len)),
'_impi_len'/Int8ub, 'impi'/HexAdapter(Bytes(this._impi_len)))
_cs_cmd_gba = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDD: 'bootstrap'/_cmd_gba_bs,
0xDE: 'naf_derivation'/_cmd_gba_naf }))
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/Bytes(this._len_sres),
'_len_kc'/Int8ub, 'kc'/Bytes(this._len_kc))
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/Bytes(this._len_res),
'_len_ck'/Int8ub, 'ck'/Bytes(this._len_ck),
'_len_ik'/Int8ub, 'ik'/Bytes(this._len_ik),
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, Bytes(this._len_kc)))
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/Bytes(this._len_auts))
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/HexAdapter(Bytes(this._len_sres)),
'_len_kc'/Int8ub, 'kc'/HexAdapter(Bytes(this._len_kc)))
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/HexAdapter(Bytes(this._len_res)),
'_len_ck'/Int8ub, 'ck'/HexAdapter(Bytes(this._len_ck)),
'_len_ik'/Int8ub, 'ik'/HexAdapter(Bytes(this._len_ik)),
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, HexAdapter(Bytes(this._len_kc))))
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/HexAdapter(Bytes(this._len_auts)))
_cs_rsp_3g = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDB: 'success'/_rsp_3g_ok,
0xDC: 'sync_fail'/_rsp_3g_sync}))
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/Bytes(this._vstk_len))
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/Bytes(this._ks_ext_naf_len))
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/HexAdapter(Bytes(this._vstk_len)))
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/HexAdapter(Bytes(this._ks_ext_naf_len)))
def _decode_cmd(self) -> Dict:
r = {}
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))

View File

@@ -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

View File

@@ -16,16 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from osmocom.gsmtap import GsmtapReceiver
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
from pySim.gsmtap import GsmtapMessage, GsmtapSource
from . import ApduSource, PacketType, CardReset
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
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
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
class GsmtapApduSource(ApduSource):
"""ApduSource for handling GSMTAP-SIM messages received via UDP, such as
@@ -41,19 +38,19 @@ class GsmtapApduSource(ApduSource):
bind_port: UDP port number to which the socket should be bound (default: 4729)
"""
super().__init__()
self.gsmtap = GsmtapReceiver(bind_ip, bind_port)
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:

View File

@@ -16,20 +16,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import logging
from pprint import pprint as pp
from typing import Tuple
import pyshark
from osmocom.gsmtap import GsmtapMessage
from pySim.utils import h2b
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
from pySim.utils import h2b, b2h
from pySim.apdu import Tpdu
from pySim.gsmtap import GsmtapMessage
from . import ApduSource, PacketType, CardReset
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
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
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)

View File

@@ -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

View File

@@ -1,48 +0,0 @@
# coding=utf-8
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pySim.utils import h2b
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
from . import ApduSource, PacketType, CardReset
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
class TcaLoaderLogApduSource(ApduSource):
"""ApduSource for reading log files created by TCALoader."""
def __init__(self, filename:str):
super().__init__()
self.logfile = open(filename, 'r')
def read_packet(self) -> PacketType:
command = None
response = None
for line in self.logfile:
if line.startswith('Command'):
command = line.split()[1]
print("Command: '%s'" % command)
pass
elif command and line.startswith('Response'):
response = line.split()[1]
print("Response: '%s'" % response)
return ApduCommands.parse_cmd_bytes(h2b(command) + h2b(response))
raise StopIteration

View File

@@ -19,13 +19,13 @@ from typing import Tuple
from pySim.transport import LinkBase
from pySim.commands import SimCardCommands
from pySim.filesystem import CardModel, CardApplication
from pySim.cards import card_detect, SimCardBase, UiccCardBase, CardBase
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.profile.cdma_ruim import CardProfileRUIM
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.cdma_ruim import CardProfileRUIM
from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import all_subclasses
from pySim.exceptions import SwMatchError
# we need to import this module so that the SysmocomSJA2 sub-class of
# CardModel is created, which will add the ATR-based matching and
@@ -40,9 +40,9 @@ import pySim.ts_31_103
import pySim.ts_31_104
import pySim.ara_m
import pySim.global_platform
import pySim.profile.euicc
import pySim.euicc
def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState, SimCardBase]:
def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
"""
Detect card in reader and setup card profile and runtime state. This
function must be called at least once on startup. The card and runtime
@@ -57,12 +57,6 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
print("Waiting for card...")
sl.wait_for_card(3)
# The user may opt to skip all card initialization. In this case only the
# most basic card profile is selected. This mode is suitable for blank
# cards that need card O/S initialization using APDU scripts first.
if skip_card_init:
return None, CardBase(scc)
generic_card = False
card = card_detect(scc)
if card is None:
@@ -113,16 +107,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
# inform the transport that we can do context-specific SW interpretation
sl.set_sw_interpreter(rs)
# try to obtain the EID, if any
isd_r = rs.mf.applications.get(pySim.euicc.AID_ISD_R.lower(), None)
if isd_r:
rs.lchan[0].select_file(isd_r)
try:
rs.identity['EID'] = pySim.euicc.CardApplicationISDR.get_eid(scc)
except SwMatchError:
# has ISD-R but not a SGP.22/SGP.32 eUICC - maybe SGP.02?
pass
finally:
rs.reset()
return rs, card

View File

@@ -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 osmocom.construct import *
from osmocom.tlv import *
from osmocom.utils import Hexstr
from pySim.construct import *
from pySim.filesystem import *
import pySim.global_platform
from pySim.tlv import *
# various BER-TLV encoded Data Objects (DOs)
@@ -69,28 +67,30 @@ 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))
self.decoded = {'apdu_filter': []}
self.decoded['apdu_filter'] = []
offset = 0
while offset < len(do):
self.decoded['apdu_filter'] += [{'header': b2h(do[offset:offset+4]),
'mask': b2h(do[offset+4:offset+8])}]
offset += 8 # Move offset to the beginning of the next apdu_filter object
return self.decoded
self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
'mask': b2h(do[offset+4:offset+8])}
self.decoded = res
return res
def _to_bytes(self):
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)))
@@ -228,14 +227,16 @@ class BlockDO(BER_TLV_IE, tag=0xe7):
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
pass
# SEID v1.1 Table 4-2
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
ResponseRefreshTagDO, ResponseAramConfigDO]):
pass
# SEID v1.1 Table 5-1
class StoreCommandDoCollection(TLV_IE_Collection,
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
@@ -258,11 +259,8 @@ 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(scc, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
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
and decoding the response data tlv."""
if cmd_do:
@@ -274,43 +272,47 @@ 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) = scc.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
@staticmethod
def store_data(scc, do) -> bytes:
def store_data(tp, do) -> bytes:
"""Build the Command APDU for STORE DATA."""
return ADF_ARAM.xceive_apdu_tlv(scc, '80e29000', do, StoreResponseDoCollection)
return ADF_ARAM.xceive_apdu_tlv(tp, '80e29000', do, StoreResponseDoCollection)
@staticmethod
def get_all(scc):
return ADF_ARAM.xceive_apdu_tlv(scc, '80caff40', None, GetResponseDoCollection)
def get_all(tp):
return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
@staticmethod
def get_config(scc, v_major=0, v_minor=0, v_patch=1):
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
cmd_do = DeviceConfigDO()
cmd_do.from_val_dict([{'device_interface_version_do': {
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
return ADF_ARAM.xceive_apdu_tlv(scc, '80cadf21', cmd_do, ResponseAramConfigDO)
cmd_do.from_dict([{'device_interface_version_do': {
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
def 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)
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)
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -332,7 +334,7 @@ class ADF_ARAM(CardADF):
apdu_grp.add_argument(
'--apdu-always', action='store_true', help='APDU access is allowed')
apdu_grp.add_argument(
'--apdu-filter', help='APDU filter: multiple groups of 8 hex bytes (4 byte CLA/INS/P1/P2 followed by 4 byte mask)')
'--apdu-filter', help='APDU filter: 4 byte CLA/INS/P1/P2 followed by 4 byte mask (8 hex bytes)')
nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
nfc_grp.add_argument('--nfc-always', action='store_true',
help='NFC event access is allowed')
@@ -346,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}]
@@ -356,19 +358,12 @@ class ADF_ARAM(CardADF):
# AR
ar_do_content = []
if opts.apdu_never:
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
ar_do_content += [{'apdu_ar_od': {'generic_access_rule': 'never'}}]
elif opts.apdu_always:
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
elif opts.apdu_filter:
if len(opts.apdu_filter) % 16:
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
offset = 0
apdu_filter = []
while offset < len(opts.apdu_filter):
apdu_filter += [{'header': opts.apdu_filter[offset:offset+8],
'mask': opts.apdu_filter[offset+8:offset+16]}]
offset += 16 # Move offset to the beginning of the next apdu_filter object
ar_do_content += [{'apdu_ar_do': {'apdu_filter': apdu_filter}}]
# TODO: multiple filters
ar_do_content += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
if opts.nfc_always:
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
elif opts.nfc_never:
@@ -377,15 +372,15 @@ class ADF_ARAM(CardADF):
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
csrado = CommandStoreRefArDO()
csrado.from_val_dict(d)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, csrado)
csrado.from_dict(d)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, csrado)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
def do_aram_delete_all(self, _opts):
def do_aram_delete_all(self, opts):
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
deldo = CommandDelete()
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, deldo)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, deldo)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -415,78 +410,3 @@ sw_aram = {
class CardApplicationARAM(CardApplication):
def __init__(self):
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
@staticmethod
def __export_get_from_dictlist(key, dictlist):
# Data objects are organized in lists that contain dictionaries, usually there is only one dictionary per
# list item. This function goes through that list and gets the value of the first dictionary that has the
# matching key.
if dictlist is None:
return None
obj = None
for d in dictlist:
obj = d.get(key, obj)
return obj
@staticmethod
def __export_ref_ar_do_list(ref_ar_do_list):
export_str = ""
ref_do_list = CardApplicationARAM.__export_get_from_dictlist('ref_do', ref_ar_do_list.get('ref_ar_do'))
ar_do_list = CardApplicationARAM.__export_get_from_dictlist('ar_do', ref_ar_do_list.get('ref_ar_do'))
if ref_do_list and ar_do_list:
# Get ref_do parameters
aid_ref_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_do', ref_do_list)
dev_app_id_ref_do = CardApplicationARAM.__export_get_from_dictlist('dev_app_id_ref_do', ref_do_list)
pkg_ref_do = CardApplicationARAM.__export_get_from_dictlist('pkg_ref_do', ref_do_list)
# Get ar_do parameters
apdu_ar_do = CardApplicationARAM.__export_get_from_dictlist('apdu_ar_do', ar_do_list)
nfc_ar_do = CardApplicationARAM.__export_get_from_dictlist('nfc_ar_do', ar_do_list)
perm_ar_do = CardApplicationARAM.__export_get_from_dictlist('perm_ar_do', ar_do_list)
# Write command-line
export_str += "aram_store_ref_ar_do"
if aid_ref_do:
export_str += (" --aid %s" % aid_ref_do)
else:
export_str += " --aid-empty"
if dev_app_id_ref_do:
export_str += (" --device-app-id %s" % dev_app_id_ref_do)
if apdu_ar_do and 'generic_access_rule' in apdu_ar_do:
export_str += (" --apdu-%s" % apdu_ar_do['generic_access_rule'])
elif apdu_ar_do and 'apdu_filter' in apdu_ar_do:
export_str += (" --apdu-filter ")
for apdu_filter in apdu_ar_do['apdu_filter']:
export_str += apdu_filter['header']
export_str += apdu_filter['mask']
if nfc_ar_do and 'nfc_event_access_rule' in nfc_ar_do:
export_str += (" --nfc-%s" % nfc_ar_do['nfc_event_access_rule'])
if perm_ar_do:
export_str += (" --android-permissions %s" % perm_ar_do['permissions'])
if pkg_ref_do:
export_str += (" --pkg-ref %s" % pkg_ref_do['package_name_string'])
export_str += "\n"
return export_str
@staticmethod
def export(as_json: bool, lchan):
# TODO: Add JSON output as soon as aram_store_ref_ar_do is able to process input in JSON format.
if as_json:
raise NotImplementedError("res_do encoder not yet implemented. Patches welcome.")
export_str = ""
export_str += "aram_delete_all\n"
res_do = ADF_ARAM.get_all(lchan.scc)
if not res_do:
return export_str.strip()
for res_do_dict in res_do.to_dict():
if not res_do_dict.get('response_all_ref_ar_do', False):
continue
for ref_ar_do_list in res_do_dict['response_all_ref_ar_do']:
export_str += CardApplicationARAM.__export_ref_ar_do_list(ref_ar_do_list)
return export_str.strip()

View File

@@ -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("")

View File

@@ -10,10 +10,10 @@ the need of manually entering the related card-individual data on every
operation with pySim-shell.
"""
# (C) 2021-2024 by Sysmocom s.f.m.c. GmbH
# (C) 2021 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier, Harald Welte
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -29,29 +29,18 @@ operation with pySim-shell.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List, Dict, Optional
from Cryptodome.Cipher import AES
from osmocom.utils import h2b, b2h
import abc
import csv
card_key_providers = [] # type: List['CardKeyProvider']
# well-known groups of columns relate to a given functionality. This avoids having
# to specify the same transport key N number of times, if the same key is used for multiple
# fields of one group, like KIC+KID+KID of one SD.
CRYPT_GROUPS = {
'UICC_SCP02': ['UICC_SCP02_KIC1', 'UICC_SCP02_KID1', 'UICC_SCP02_KIK1'],
'UICC_SCP03': ['UICC_SCP03_KIC1', 'UICC_SCP03_KID1', 'UICC_SCP03_KIK1'],
'SCP03_ISDR': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDR', 'SCP03_DEK_ISDR'],
'SCP03_ISDA': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDA', 'SCP03_DEK_ISDA'],
'SCP03_ECASD': ['SCP03_ENC_ECASD', 'SCP03_MAC_ECASD', 'SCP03_DEK_ECASD'],
}
class CardKeyProvider(abc.ABC):
"""Base class, not containing any concrete implementation."""
VALID_KEY_FIELD_NAMES = ['ICCID', 'EID', 'IMSI' ]
VALID_FIELD_NAMES = ['ICCID', 'ADM1',
'IMSI', 'PIN1', 'PIN2', 'PUK1', 'PUK2']
# check input parameters, but do nothing concrete yet
def _verify_get_data(self, fields: List[str] = [], key: str = 'ICCID', value: str = "") -> Dict[str, str]:
@@ -64,10 +53,14 @@ class CardKeyProvider(abc.ABC):
Returns:
dictionary of {field, value} strings for each requested field from 'fields'
"""
for f in fields:
if (f not in self.VALID_FIELD_NAMES):
raise ValueError("Requested field name '%s' is not a valid field name, valid field names are: %s" %
(f, str(self.VALID_FIELD_NAMES)))
if key not in self.VALID_KEY_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_KEY_FIELD_NAMES)))
(key, str(self.VALID_FIELD_NAMES)))
return {}
@@ -91,47 +84,19 @@ class CardKeyProvider(abc.ABC):
class CardKeyProviderCsv(CardKeyProvider):
"""Card key provider implementation that allows to query against a specified CSV file.
Supports column-based encryption as it is generally a bad idea to store cryptographic key material in
plaintext. Instead, the key material should be encrypted by a "key-encryption key", occasionally also
known as "transport key" (see GSMA FS.28)."""
IV = b'\x23' * 16
"""Card key provider implementation that allows to query against a specified CSV file"""
csv_file = None
filename = None
def __init__(self, filename: str, transport_keys: dict):
def __init__(self, filename: str):
"""
Args:
filename : file name (path) of CSV file containing card-individual key/data
transport_keys : a dict indexed by field name, whose values are hex-encoded AES keys for the
respective field (column) of the CSV. This is done so that different fields
(columns) can use different transport keys, which is strongly recommended by
GSMA FS.28
"""
self.csv_file = open(filename, 'r')
if not self.csv_file:
raise RuntimeError("Could not open CSV file '%s'" % filename)
self.filename = filename
self.transport_keys = self.process_transport_keys(transport_keys)
@staticmethod
def process_transport_keys(transport_keys: dict):
"""Apply a single transport key to multiple fields/columns, if the name is a group."""
new_dict = {}
for name, key in transport_keys.items():
if name in CRYPT_GROUPS:
for field in CRYPT_GROUPS[name]:
new_dict[field] = key
else:
new_dict[name] = key
return new_dict
def _decrypt_field(self, field_name: str, encrypted_val: str) -> str:
"""decrypt a single field, if we have a transport key for the field of that name."""
if not field_name in self.transport_keys:
return encrypted_val
cipher = AES.new(h2b(self.transport_keys[field_name]), AES.MODE_CBC, self.IV)
return b2h(cipher.decrypt(h2b(encrypted_val)))
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
super()._verify_get_data(fields, key, value)
@@ -148,7 +113,7 @@ class CardKeyProviderCsv(CardKeyProvider):
if row[key] == value:
for f in fields:
if f in row:
rc.update({f: self._decrypt_field(f, row[f])})
rc.update({f: row[f]})
else:
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
(self.filename, f))

View File

@@ -22,12 +22,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Optional, Tuple
from osmocom.utils import *
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.profile.ts_102_221 import EF_DIR, CardProfileUICC
from pySim.profile.ts_51_011 import DF_GSM
from pySim.utils import SwHexstr
from pySim.utils import *
from pySim.commands import Path, SimCardCommands
class CardBase:
@@ -40,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)"""
@@ -53,19 +54,13 @@ class CardBase:
def erase(self):
print("warning: erasing is not supported for specified card type!")
return
def file_exists(self, fid: Path) -> bool:
"""Determine if the file exists (and is not deactivated)."""
res_arr = self._scc.try_select_path(fid)
for res in res_arr:
if res[1] != '9000':
return False
try:
d = CardProfileUICC.decode_select_response(res_arr[-1][0])
if d.get('life_cycle_status_integer', 'operational_activated') != 'operational_activated':
return False
except:
pass
return True
def read_aids(self) -> List[Hexstr]:
@@ -80,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"
@@ -93,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
@@ -163,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]:

View File

@@ -18,58 +18,43 @@ 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 typing import List
from pySim.utils import b2h, h2b, dec_xplmn_w_act
from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from pySim.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger
from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi, GsmString
from osmocom.utils import b2h
from pySim.utils import dec_xplmn_w_act
# Tag values as per TS 101 220 Table 7.23
# TS 102 223 Section 8.1
class Address(COMPR_TLV_IE, tag=0x86):
class Address(COMPR_TLV_IE, tag=0x06):
_construct = Struct('ton_npi'/Int8ub,
'call_number'/BcdAdapter(GreedyBytes))
'call_number'/BcdAdapter(Bytes(this._.total_len-1)))
# TS 102 223 Section 8.2
class AlphaIdentifier(COMPR_TLV_IE, tag=0x85):
class AlphaIdentifier(COMPR_TLV_IE, tag=0x05):
# FIXME: like EF.ADN
pass
# TS 102 223 Section 8.3
class Subaddress(COMPR_TLV_IE, tag=0x88):
class Subaddress(COMPR_TLV_IE, tag=0x08):
pass
# TS 102 223 Section 8.4 + TS 31.111 Section 8.4
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x87):
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x07):
pass
# TS 31.111 Section 8.5
class CBSPage(COMPR_TLV_IE, tag=0x8C):
class CBSPage(COMPR_TLV_IE, tag=0x0C):
pass
# TS 102 223 V15.3.0 Section 9.4
TypeOfCommand = Enum(Int8ub, refresh=0x01, more_time=0x02, poll_interval=0x03, polling_off=0x04,
set_up_event_list=0x05, set_up_call=0x10, send_ss=0x11, send_ussd=0x12,
send_short_message=0x13, send_dtmf=0x14, launch_browser=0x15, geo_location_req=0x16,
play_tone=0x20, display_text=0x21, get_inkey=0x22, get_input=0x23, select_item=0x24,
set_up_menu=0x25, provide_local_info=0x26, timer_management=0x27,
set_up_idle_mode_text=0x28, perform_card_apdu=0x30, power_on_card=0x31,
power_off_card=0x32, get_reader_status=0x33, run_at_command=0x34,
language_notification=0x35, open_channel=0x40, close_channel=0x41, receive_data=0x42,
send_data=0x43, get_channel_status=0x44, service_search=0x45, get_service_info=0x46,
declare_service=0x47, set_frames=0x50, get_frames_status=0x51, retrieve_mms=0x60,
submit_mms=0x61, display_mms=0x62, activate=0x70, contactless_state_changed=0x71,
command_container=0x72, encapsulated_session_control=0x73)
# TS 102 223 Section 8.6 + TS 31.111 Section 8.6
class CommandDetails(COMPR_TLV_IE, tag=0x81):
_construct = Struct('command_number'/Int8ub,
'type_of_command'/TypeOfCommand,
'type_of_command'/Int8ub,
'command_qualifier'/Int8ub)
# TS 102 223 Section 8.7
@@ -122,26 +107,26 @@ class DeviceIdentities(COMPR_TLV_IE, tag=0x82):
return bytes([src, dst])
# TS 102 223 Section 8.8
class Duration(COMPR_TLV_IE, tag=0x84):
class Duration(COMPR_TLV_IE, tag=0x04):
_construct = Struct('time_unit'/Enum(Int8ub, minutes=0, seconds=1, tenths_of_seconds=2),
'time_interval'/Int8ub)
# TS 102 223 Section 8.9
class Item(COMPR_TLV_IE, tag=0x8f):
class Item(COMPR_TLV_IE, tag=0x0f):
_construct = Struct('identifier'/Int8ub,
'text_string'/GsmStringAdapter(GreedyBytes))
# TS 102 223 Section 8.10
class ItemIdentifier(COMPR_TLV_IE, tag=0x90):
class ItemIdentifier(COMPR_TLV_IE, tag=0x10):
_construct = Struct('identifier'/Int8ub)
# TS 102 223 Section 8.11
class ResponseLength(COMPR_TLV_IE, tag=0x91):
class ResponseLength(COMPR_TLV_IE, tag=0x11):
_construct = Struct('minimum_length'/Int8ub,
'maximum_length'/Int8ub)
# TS 102 223 Section 8.12
class Result(COMPR_TLV_IE, tag=0x83):
class Result(COMPR_TLV_IE, tag=0x03):
GeneralResult = Enum(Int8ub,
# '0X' and '1X' indicate that the command has been performed
performed_successfully=0,
@@ -267,15 +252,12 @@ class SsString(COMPR_TLV_IE, tag=0x89):
# TS 102 223 Section 8.15
class TextString(COMPR_TLV_IE, tag=0x8D):
_test_de_encode = [
( '8d090470617373776f7264', {'dcs': 4, 'text_string': '70617373776f7264'} ),
]
class TextString(COMPR_TLV_IE, tag=0x0d):
_construct = Struct('dcs'/Int8ub, # TS 03.38
'text_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.16
class Tone(COMPR_TLV_IE, tag=0x8E):
class Tone(COMPR_TLV_IE, tag=0x0e):
_construct = Struct('tone'/Enum(Int8ub, dial_tone=0x01,
called_subscriber_busy=0x02,
congestion=0x03,
@@ -306,12 +288,12 @@ class Tone(COMPR_TLV_IE, tag=0x8E):
melody_8=0x47))
# TS 31 111 Section 8.17
class USSDString(COMPR_TLV_IE, tag=0x8A):
class USSDString(COMPR_TLV_IE, tag=0x0a):
_construct = Struct('dcs'/Int8ub,
'ussd_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.18
class FileList(COMPR_TLV_IE, tag=0x92):
class FileList(COMPR_TLV_IE, tag=0x12):
FileId=HexAdapter(Bytes(2))
_construct = Struct('number_of_files'/Int8ub,
'files'/GreedyRange(FileId))
@@ -338,7 +320,7 @@ class DefaultText(COMPR_TLV_IE, tag=0x97):
'text_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.24
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x98):
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x18):
_construct = GreedyRange(Int8ub)
class EventList(COMPR_TLV_IE, tag=0x99):
@@ -383,7 +365,7 @@ class LocationStatus(COMPR_TLV_IE, tag=0x9b):
_construct = Enum(Int8ub, normal_service=0, limited_service=1, no_service=2)
# TS 102 223 Section 8.31
class IconIdentifier(COMPR_TLV_IE, tag=0x9e):
class IconIdentifier(COMPR_TLV_IE, tag=0x1e):
_construct = Struct('icon_qualifier'/FlagsEnum(Int8ub, not_self_explanatory=1),
'icon_identifier'/Int8ub)
@@ -409,48 +391,25 @@ class AtCommand(COMPR_TLV_IE, tag=0xA8):
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.43
class ImmediateResponse(COMPR_TLV_IE, tag=0xAB):
class ImmediateResponse(COMPR_TLV_IE, tag=0x2b):
pass
# TS 102 223 Section 8.44
class DtmfString(COMPR_TLV_IE, tag=0xAC):
_construct = BcdAdapter(GreedyBytes)
# TS 102 223 Section 8.45
class Language(COMPR_TLV_IE, tag=0xAD):
_construct = HexAdapter(GreedyBytes)
# TS 31.111 Section 8.46
class TimingAdvance(COMPR_TLV_IE, tag=0xC6):
class TimingAdvance(COMPR_TLV_IE, tag=0x46):
_construct = Struct('me_status'/Enum(Int8ub, in_idle_state=0, not_in_idle_state=1),
'timing_advance'/Int8ub)
# TS 31.111 Section 8.47
class BrowserIdentity(COMPR_TLV_IE, tag=0xB0):
_construct = Enum(Int8ub, default=0, wml=1, html=2, xhtml=3, chtml=4)
# TS 31.111 Section 8.48
class Url(COMPR_TLV_IE, tag=0xB1):
_construct = GsmString(GreedyBytes)
# TS 31.111 Section 8.49
class Bearer(COMPR_TLV_IE, tag=0xB2):
SingleBearer = Enum(Int8ub, sms=0, csd=1, ussd=2, packet_Service=3)
_construct = GreedyRange(SingleBearer)
# TS 102 223 Section 8.50
class ProvisioningFileReference(COMPR_TLV_IE, tag=0xB3):
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.51
class BrowserTerminationCause(COMPR_TLV_IE, tag=0xB4):
_construct = Enum(Int8ub, user_termination=0, error_termination=1)
# TS 102 223 Section 8.52
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
_test_de_encode = [
( 'b50103', {'bearer_parameters': '', 'bearer_type': 'default'} ),
]
# TS 31.111 Section 8.52.1
BearerParsCs = Struct('data_rate'/Int8ub,
'bearer_service'/Int8ub,
@@ -506,32 +465,25 @@ class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
class BufferSize(COMPR_TLV_IE, tag = 0xB9):
_construct = Int16ub
# TS 102 223 Section 8.56 + TS 31.111 Section 8.56
# TS 31.111 Section 8.56
class ChannelStatus(COMPR_TLV_IE, tag = 0xB8):
# complex decoding, depends on out-of-band context/knowledge :(
# for default / TCP Client mode: bit 8 of first byte indicates connected, 3 LSB indicate channel nr
_construct = HexAdapter(GreedyBytes)
pass
# TS 102 223 Section 8.58
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
_test_de_encode = [
( 'be052101020304', {'address': '01020304', 'type_of_address': 'ipv4'} ),
]
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
'address'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.59
class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
_test_de_encode = [
( 'bc03028000', {'port_number': 32768, 'protocol_type': 'tcp_uicc_client_remote'} ),
]
_construct = Struct('protocol_type'/Enum(Int8ub, udp_uicc_client_remote=1, tcp_uicc_client_remote=2,
tcp_uicc_server=3, udp_uicc_client_local=4,
tcp_uicc_client_local=5, direct_channel=6),
'port_number'/Int16ub)
# TS 102 223 Section 8.60
class Aid(COMPR_TLV_IE, tag=0xAF):
class Aid(COMPR_TLV_IE, tag=0x2f):
_construct = Struct('aid'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.61
@@ -571,13 +523,10 @@ class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
# TS 102 223 Section 8.70
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
_test_de_encode = [
( 'c704036e6161', '036e6161' ),
]
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.72
class TextAttribute(COMPR_TLV_IE, tag=0xD0):
class TextAttribute(COMPR_TLV_IE, tag=0x50):
pass
# TS 31.111 Section 8.72
@@ -613,7 +562,7 @@ class ItemTextAttributeList(COMPR_TLV_IE, tag=0xD1):
_construct = GreedyRange(Int8ub)
# TS 102 223 Section 8.80
class FrameIdentifier(COMPR_TLV_IE, tag=0xE8):
class FrameIdentifier(COMPR_TLV_IE, tag=0x68):
_construct = Struct('identifier'/Int8ub)
# TS 102 223 Section 8.82
@@ -634,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
@@ -720,7 +669,7 @@ class SaTemplate(COMPR_TLV_IE, tag=0xA3):
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.103
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0xBA):
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0x3A):
_construct = FlagsEnum(Byte, even_if_navigating_menus=0, even_if_data_call=1, even_if_voice_call=2)
# TS 102 223 Section 8.104
@@ -734,7 +683,7 @@ class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
_construct = GreedyRange(AccessTechTuple)
# TS 102 223 Section 8.107
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0xBB):
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0x3B):
pass
# TS 31.111 Section 8.108
@@ -768,194 +717,19 @@ class SMSCBDownload(BER_TLV_IE, tag=0xD2,
nested=[DeviceIdentities, CBSPage]):
pass
# TS 101 220 Table 7.17
class MenuSelection(BER_TLV_IE, tag=0xD3,
nested=[DeviceIdentities, ItemIdentifier, HelpRequest]):
pass
class BcRepeatIndicator(BER_TLV_IE, tag=0x2A):
pass
# TS 101 220 Table 7.17
class CallControl(BER_TLV_IE, tag=0xD4,
nested=[DeviceIdentities, Address, CapabilityConfigParams, Subaddress,
LocationInformation, BcRepeatIndicator]):
pass
# TS 101 220 Table 7.17
class MoShortMessageControl(BER_TLV_IE, tag=0xD5):
pass
# TS 101 220 Table 7.23
class TransactionIdentifier(BER_TLV_IE, tag=0x1C):
pass
# TS 101 220 Table 7.23
class ImsURI(BER_TLV_IE, tag=0x31):
pass
# TS 101 220 Table 7.23
class UriTruncated(BER_TLV_IE, tag=0x73):
pass
# TS 101 220 Table 7.23
class TrackingAreaIdentification(BER_TLV_IE, tag=0x7D):
pass
# TS 101 220 Table 7.23
class ExtendedRejectionCauseCode(BER_TLV_IE, tag=0x57):
pass
# TS 101 220 Table 7.23
class CsgCellSelectionStatus(BER_TLV_IE, tag=0x55):
pass
# TS 101 220 Table 7.23
class CsgId(BER_TLV_IE, tag=0x56):
pass
# TS 101 220 Table 7.23
class HnbName(BER_TLV_IE, tag=0x57):
pass
# TS 101 220 Table 7.23
class PlmnId(BER_TLV_IE, tag=0x09):
pass
# TS 101 220 Table 7.23
class ImsCallDisconnectionStatus(BER_TLV_IE, tag=0x55):
pass
# TS 101 220 Table 7.23
class Iari(BER_TLV_IE, tag=0x76):
pass
# TS 101 220 Table 7.23
class ImpuList(BER_TLV_IE, tag=0x77):
pass
# TS 101 220 Table 7.23
class ImsStatusCode(BER_TLV_IE, tag=0x77):
pass
# TS 101 220 Table 7.23
class DateTimeAndTimezone(BER_TLV_IE, tag=0x26):
pass
# TS 101 220 Table 7.23
class PdpPdnPduType(BER_TLV_IE, tag=0x0B):
pass
# TS 101 220 Table 7.23
class GadShape(BER_TLV_IE, tag=0x77):
pass
# TS 101 220 Table 7.23
class NmeaSentence(BER_TLV_IE, tag=0x78):
pass
# TS 101 220 Table 7.23
class WlanAccessStatus(BER_TLV_IE, tag=0x4B):
pass
# TS 101 220 Table 7.17
class EventDownload(BER_TLV_IE, tag=0xD6,
nested=[EventList, DeviceIdentities,
# 7.5.1.2 (I-)WLAN Access Status
WlanAccessStatus,
# 7.5.1A.2 MT Call
TransactionIdentifier, Address,
Subaddress, ImsURI, MediaType, UriTruncated,
# 7.5.2.2 Network Rejection
LocationInformation, RoutingAreaIdentification, TrackingAreaIdentification,
AccessTechnology, UpdateAttachRegistrationType, RejectionCauseCode,
ExtendedRejectionCauseCode,
# 7.5.2A.2 Call Connected
# TransactionIdentifier, MediaType
# 7.5.3.2 CSG Cell Selection
# AccessTechnology
CsgCellSelectionStatus, CsgId, HnbName, PlmnId,
# 7.5.3A.2 CAll Disconnected
# TransactionIdentifier, MediaType,
ImsCallDisconnectionStatus,
# TS 102 223 7.5.4 LocationStatusEvent
# TS 102 223 7.5.5 UserActivityEvent
# TS 102 223 7.5.6 IdleScreenAvailableEvent
# TS 102 223 7.5.7 CardReaderStatusEvent
# TS 102 223 7.5.8 LanguageSelectionEvent
# TS 102 223 7.5.9 BrowserTerminationEvent
# TS 102 223 7.5.10 DataAvailableEvent
ChannelStatus, ChannelDataLength,
# TS 102 223 7.5.11 ChannelStatusEvent
# TS 102 223 7.5.12 AccessTechnologyChangeEvent
# TS 102 223 7.5.13 DisplayParametersChangedEvent
# TS 102 223 7.5.14 LocalConnectionEvent
# TS 102 223 7.5.15 NetworkSearchModeChangeEvent
# TS 102 223 7.5.16 BrowsingStatusEvent
# TS 102 223 7.5.17 FramesInformationChangedEvent
# 7.5.20 Incoming IMS Data
Iari,
# 7.5.21 MS Registration Event
ImpuList, ImsStatusCode,
# 7.5.24 / TS 102 223 7.5.22 PollIntervalNegotiation
# 7.5.25 DataConnectionStatusChangeEvent
DataConnectionStatus, DataConnectionType, SmCause,
# TransactionIdentifier, LocationInformation, AccessTechnology
DateTimeAndTimezone, LocationStatus, NetworkAccessName, PdpPdnPduType,
# 7.7 / TS 102 223 7.6 MMS Transfer Status
# 7.8 / TS 102 223 MMS Notification Download
# 7.9 / TS 102 223 8.8 Terminal Applications
]):
pass
# TS 101 220 Table 7.17
class TimerExpiration(BER_TLV_IE, tag=0xD7):
pass
# TS 101 220 Table 7.17 + TS 31.111 7.6.2
class USSDDownload(BER_TLV_IE, tag=0xD9,
nested=[DeviceIdentities, USSDString]):
pass
# TS 101 220 Table 7.17 + TS 102 223 7.6
class MmsTransferStatus(BER_TLV_IE, tag=0xDA):
pass
# TS 101 220 Table 7.17 + 102 223
class MmsNotificationDownload(BER_TLV_IE, tag=0xDB):
pass
# TS 101 220 Table 7.17 + 102 223 7.8
class TerminalApplication(BER_TLV_IE, tag=0xDC):
pass
# TS 101 220 Table 7.17 + TS 31.111 7.10.2
class GeographicalLocation(BER_TLV_IE, tag=0xDD,
nested=[DeviceIdentities, GadShape, NmeaSentence]):
pass
# TS 101 220 Table 7.17
class EnvelopeContainer(BER_TLV_IE, tag=0xDE):
pass
# TS 101 220 Table 7.17
class ProSeReport(BER_TLV_IE, tag=0xDF):
pass
# TS 101 220 Table 7.17
class ProactiveCmd(BER_TLV_IE):
def _compute_tag(self) -> int:
return 0xD0
class EventCollection(TLV_IE_Collection,
nested=[SMSPPDownload, SMSCBDownload,
EventDownload, CallControl, MoShortMessageControl,
USSDDownload, GeographicalLocation, ProSeReport]):
pass
# TS 101 220 Table 7.17 + 102 223 6.6.13/9.4 + TS 31.111 6.6.13
class Refresh(ProactiveCmd, tag=0x01,
nested=[CommandDetails, DeviceIdentities, FileList, Aid, AlphaIdentifier,
@@ -963,24 +737,20 @@ class Refresh(ProactiveCmd, tag=0x01,
ApplicationSpecificRefreshData, PlmnWactList, PlmnList]):
pass
# TS 102 223 Section 6.6.4
class MoreTime(ProactiveCmd, tag=0x02,
nested=[CommandDetails, DeviceIdentities]):
nested=[CommandDetails]):
pass
# TS 102 223 Section 6.6.5
class PollInterval(ProactiveCmd, tag=0x03,
nested=[CommandDetails, DeviceIdentities, Duration]):
nested=[CommandDetails]):
pass
# TS 102 223 Section 6.6.14
class PollingOff(ProactiveCmd, tag=0x04,
nested=[CommandDetails, DeviceIdentities]):
nested=[CommandDetails]):
pass
# TS 102 223 Section 6.6.16
class SetUpEventList(ProactiveCmd, tag=0x05,
nested=[CommandDetails, DeviceIdentities, EventList]):
nested=[CommandDetails]):
pass
# TS 31.111 Section 6.6.12
@@ -1008,27 +778,20 @@ class SendShortMessage(ProactiveCmd, tag=0x13,
SMS_TPDU, IconIdentifier, TextAttribute, FrameIdentifier]):
pass
# TS 102 223 6.6.24
class SendDTMF(ProactiveCmd, tag=0x14,
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
DtmfString, IconIdentifier, TextAttribute, FrameIdentifier]):
nested=[CommandDetails]):
pass
# TS 102 223 6.6.26
class LaunchBrowser(ProactiveCmd, tag=0x15,
nested=[CommandDetails, DeviceIdentities, BrowserIdentity, Url, Bearer, ProvisioningFileReference,
TextString, AlphaIdentifier, IconIdentifier, TextAttribute, FrameIdentifier,
NetworkAccessName]):
nested=[CommandDetails]):
pass
class GeographicalLocationRequest(ProactiveCmd, tag=0x16,
nested=[CommandDetails]):
pass
# TS 102 223 6.6.5
class PlayTone(ProactiveCmd, tag=0x20,
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
Tone, Duration, IconIdentifier, TextAttribute, FrameIdentifier]):
nested=[CommandDetails]):
pass
# TS 101 220 Table 7.17 + 102 223 6.6.1/9.4 CMD=0x21
@@ -1215,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,
@@ -1234,17 +997,17 @@ class ProactiveCommand(TLV_IE_Collection,
more difficult than any normal TLV IE Collection, because the content of one of the IEs defines the
definitions of all the other IEs. So we first need to find the CommandDetails, and then parse according
to the command type indicated in that IE data."""
def from_bytes(self, binary: bytes, 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)
cmd_details = pcmd.find_cmd_details()
# then do a second decode stage for the specific
cmd_type = TypeOfCommand.encmapping[cmd_details.decoded['type_of_command']]
cmd_type = cmd_details.decoded['type_of_command']
if cmd_type in self.members_by_tag:
cls = self.members_by_tag[cmd_type]
inst = cls()
_dec, remainder = inst.from_tlv(binary)
dec, remainder = inst.from_tlv(binary)
self.decoded = inst
else:
self.decoded = pcmd
@@ -1256,18 +1019,9 @@ 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()
# TS 101 223 Section 6.8.0
class TerminalResponse(TLV_IE_Collection,
nested=[CommandDetails, DeviceIdentities, Result,
Duration, TextString, ItemIdentifier,
#TODO: LocalInformation and other optional/conditional IEs
ChannelData, ChannelDataLength,
ChannelStatus, BufferSize, BearerDescription,
]):
pass
# reasonable default for playing with OTA
# 010203040506070809101112131415161718192021222324252627282930313233

View File

@@ -19,15 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import enum
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
from osmocom.utils import *
from osmocom.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile import match_ruim
from pySim.profile import CardProfile, CardProfileAddon
from pySim.profile.ts_51_011 import CardProfileSIM
from pySim.profile.ts_51_011 import DF_TELECOM, DF_GSM
from pySim.profile.ts_51_011 import EF_ServiceTable
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
@@ -178,23 +178,20 @@ class DF_CDMA(CardDF):
class CardProfileRUIM(CardProfile):
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
ORDER = 20
ORDER = 2
def __init__(self):
super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
@staticmethod
def decode_select_response(data_hex: str) -> object:
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)
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
""" Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
the card is considered an R-UIM card for CDMA."""
cls._mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
return CardProfileSIM.decode_select_response(resp_hex)
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_ruim(scc)
class AddonRUIM(CardProfileAddon):
"""An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""

View File

@@ -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,18 +21,15 @@
# 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 osmocom.construct import LV, filter_dict
from osmocom.utils import rpad, lpad, b2h, h2b, h2i, i2h, str_sanitize, Hexstr
from osmocom.tlv import bertlv_encode_len
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, 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
from pySim.ts_102_221 import decode_select_response
# A path can be either just a FID or a list of FID
Path = typing.Union[Hexstr, List[Hexstr]]
@@ -67,115 +64,73 @@ class SimCardCommands:
byte by the respective instance. """
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
self._tp = transport
self._cla_byte = None
self.sel_ctrl = "0000"
self.lchan_nr = lchan_nr
# invokes the setter below
self.cla_byte = "a0"
self.scp = None # Secure Channel Protocol
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
ret = SimCardCommands(transport = self._tp, lchan_nr = lchan_nr)
ret.cla_byte = self.cla_byte
ret.cla_byte = self._cla_byte
ret.sel_ctrl = self.sel_ctrl
return ret
@property
def 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
def cla_byte(self) -> Hexstr:
"""Return the (cached) patched default CLA byte for this card."""
return self._cla4lchan
@cla_byte.setter
def cla_byte(self, new_val: Hexstr):
"""Set the (raw, without lchan) default CLA value for this card."""
self._cla_byte = new_val
# compute cached result
self._cla4lchan = cla_with_lchan(self._cla_byte, self.lchan_nr)
def cla4lchan(self, cla: Hexstr) -> Hexstr:
"""Compute the lchan-patched value of the given CLA value. If no CLA
value is provided as argument, the lchan-patched version of the SimCardCommands._cla_byte
value is used. Most commands will use the latter, while some wish to override it and
can pass it as argument here."""
if not cla:
# return cached result to avoid re-computing this over and over again
return self._cla4lchan
else:
return 255
return cla_with_lchan(cla, self.lchan_nr)
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
"""Sends an APDU and auto fetch response data
# Extract a single FCP item from TLV
def __parse_fcp(self, fcp: Hexstr):
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
# DF or ADF
from pytlv.TLV import TLV
tlvparser = TLV(['82', '83', '84', 'a5', '8a', '8b',
'8c', '80', 'ab', 'c6', '81', '88'])
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if apply_lchan:
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
# pytlv is case sensitive!
fcp = fcp.lower()
if fcp[0:2] != '62':
raise ValueError(
'Tag of the FCP template does not match, expected 62 but got %s' % fcp[0:2])
# Unfortunately the spec is not very clear if the FCP length is
# coded as one or two byte vale, so we have to try it out by
# checking if the length of the remaining TLV string matches
# what we get in the length field.
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
exp_tlv_len = int(fcp[2:4], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 4
else:
return self._tp.send_apdu(pdu)
exp_tlv_len = int(fcp[2:6], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 6
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000", apply_lchan:bool = True) -> ResTuple:
"""Sends an APDU and check returned SW
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
digits using a '?' to add some ambiguity if needed.
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if apply_lchan:
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
else:
return self._tp.send_apdu_checksw(pdu, sw)
def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,
cmd_data: Hexstr, resp_constr: Construct, apply_lchan:bool = True) -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
cla : string (in hex) ISO 7816 class byte
ins : string (in hex) ISO 7816 instruction byte
p1 : string (in hex) ISO 7116 Parameter 1 byte
p2 : string (in hex) ISO 7116 Parameter 2 byte
cmd_cosntr : defining how to generate binary APDU command data
cmd_data : command data passed to cmd_constr
resp_cosntr : defining how to decode binary APDU response data
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
Tuple of (decoded_data, sw)
"""
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
lc = i2h([len(cmd)]) if cmd_data else ''
le = '00' if resp_constr else ''
pdu = ''.join([cla, ins, p1, p2, lc, b2h(cmd), le])
(data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan)
if data:
# filter the resulting dict to avoid '_io' members inside
rsp = filter_dict(resp_constr.parse(h2b(data)))
else:
rsp = None
return (rsp, sw)
def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
sw_exp: SwMatchstr="9000", apply_lchan:bool = True) -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
cla : string (in hex) ISO 7816 class byte
ins : string (in hex) ISO 7816 instruction byte
p1 : string (in hex) ISO 7116 Parameter 1 byte
p2 : string (in hex) ISO 7116 Parameter 2 byte
cmd_cosntr : defining how to generate binary APDU command data
cmd_data : command data passed to cmd_constr
resp_cosntr : defining how to decode binary APDU response data
exp_sw : string (in hex) of status word (ex. "9000")
Returns:
Tuple of (decoded_data, sw)
"""
(rsp, sw) = self.send_apdu_constr(cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr,
apply_lchan = apply_lchan)
if not sw_match(sw, sw_exp):
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
return (rsp, sw)
# Skip FCP tag and length
tlv = fcp[skip:]
return tlvparser.parse(tlv)
# Tell the length of a record by the card response
# USIMs respond with an FCP template, which is different
@@ -184,8 +139,10 @@ class SimCardCommands:
# SIM: GSM 11.11, chapter 9.2.1 SELECT
def __record_len(self, r) -> int:
if self.sel_ctrl == "0004":
fcp_parsed = decode_select_response(r[-1])
return fcp_parsed['file_descriptor']['record_len']
tlv_parsed = self.__parse_fcp(r[-1])
file_descriptor = tlv_parsed['82']
# See also ETSI TS 102 221, chapter 11.1.1.4.3 File Descriptor
return int(file_descriptor[4:8], 16)
else:
return int(r[-1][28:30], 16)
@@ -193,8 +150,8 @@ class SimCardCommands:
# above.
def __len(self, r) -> int:
if self.sel_ctrl == "0004":
fcp_parsed = decode_select_response(r[-1])
return fcp_parsed['file_size']
tlv_parsed = self.__parse_fcp(r[-1])
return int(tlv_parsed['80'], 16)
else:
return int(r[-1][4:8], 16)
@@ -210,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 + "00")
data, sw = self._tp.send_apdu(
self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
rv.append((data, sw))
if sw != '9000':
return rv
@@ -229,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
@@ -243,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 + "00")
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 + "a40304")
return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
def select_adf(self, aid: Hexstr) -> ResTuple:
"""Execute SELECT a given Applicaiton ADF.
@@ -257,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 + "00")
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.
@@ -278,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:
e.add_note('failed to read (offset %d)' % offset)
raise e
raise ValueError('%s, failed to read (offset %d)' %
(str_sanitize(str(e)), offset))
total_data += data
chunk_offset += chunk_len
return total_data, sw
@@ -336,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:
e.add_note('failed to write chunk (chunk_offset %d, chunk_len %d)' % (chunk_offset, chunk_len))
raise e
raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' %
(str_sanitize(str(e)), chunk_offset, chunk_len))
total_data += data
chunk_offset += chunk_len
if verify:
@@ -362,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
@@ -401,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:
@@ -425,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
@@ -460,10 +418,10 @@ class SimCardCommands:
# TS 102 221 Section 11.3.1 low-level helper
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
if first:
pdu = '80cb008001%02x00' % (tag)
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
else:
pdu = '80cb0000'
return self.send_apdu_checksw(pdu)
pdu = self.cla4lchan('80') + 'cb000000'
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.
@@ -479,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
@@ -490,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 = '80db00%02x%02x%s' % (p1, len(data)//2, data)
return self.send_apdu_checksw(pdu)
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
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.
@@ -520,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:
@@ -535,7 +493,7 @@ class SimCardCommands:
if len(rand) != 32:
raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20'])
return self.send_apdu_checksw('a088000010' + rand + '00', 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).
@@ -546,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}
@@ -556,9 +515,7 @@ class SimCardCommands:
p2 = '81'
elif context == 'gsm':
p2 = '80'
else:
raise ValueError("Unsupported context '%s'" % context)
(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}
@@ -568,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('80F20000')
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.
@@ -580,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 CREATE FILE command as per TS 102 222 Section 6.3"""
return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
"""Execute CREEATE FILE command as per TS 102 222 Section 6.3"""
return self._tp.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
def resize_file(self, payload: Hexstr) -> ResTuple:
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
return self.send_apdu_checksw('80d40000%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.
@@ -617,8 +574,8 @@ class SimCardCommands:
p1 = 0x80
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
return self.send_apdu_checksw(pdu)
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
return self._tp.send_apdu_checksw(pdu)
def reset_card(self) -> Hexstr:
"""Physically reset the card"""
@@ -628,8 +585,8 @@ class SimCardCommands:
if sw_match(sw, '63cx'):
raise RuntimeError('Failed to %s chv_no 0x%02X with code 0x%s, %i tries left.' %
(op_name, chv_no, b2h(pin_code).upper(), int(sw[3])))
if sw != '9000':
raise SwMatchError(sw, '9000', self._tp.sw_interpreter)
elif (sw != '9000'):
raise SwMatchError(sw, '9000')
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
"""Verify a given CHV (Card Holder Verification == PIN)
@@ -639,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)
@@ -652,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)
@@ -665,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)
@@ -678,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)
@@ -690,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)
@@ -700,7 +662,7 @@ class SimCardCommands:
Args:
payload : payload as hex string
"""
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload), apply_lchan = False)
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
@@ -709,7 +671,7 @@ class SimCardCommands:
payload : payload as hex string
"""
data_length = len(payload) // 2
data, sw = self.send_apdu_checksw(('80100000%02x' % data_length) + payload, apply_lchan = False)
data, sw = self._tp.send_apdu(('80100000%02x' % data_length) + payload)
return (data, sw)
# ETSI TS 102 221 11.1.22
@@ -723,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, apply_lchan = False)
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)
@@ -757,15 +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, apply_lchan = False)
data, sw = self._tp.send_apdu_checksw('8076010008' + token)
return (data, sw)
# GPC_SPE_034 11.3
def get_data(self, tag: int, cla: int = 0x00):
data, sw = self.send_apdu_checksw('%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)

552
pySim/construct.py Normal file
View File

@@ -0,0 +1,552 @@
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
import typing
from construct import *
from construct.core import evaluate, BitwisableString
from construct.lib import integertypes
from pySim.utils import b2h, h2b, swap_nibbles
import gsm0338
import codecs
import ipaddress
"""Utility code related to the integration of the 'construct' declarative parser."""
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class HexAdapter(Adapter):
"""convert a bytes() type to a string of hex nibbles."""
def _decode(self, obj, context, path):
return b2h(obj)
def _encode(self, obj, context, path):
return h2b(obj)
class Utf8Adapter(Adapter):
"""convert a bytes() type that contains utf8 encoded text to human readable text."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
return codecs.decode(obj, "utf-8")
def _encode(self, obj, context, path):
return codecs.encode(obj, "utf-8")
class GsmOrUcs2Adapter(Adapter):
"""Try to encode into a GSM 03.38 string; if that fails, fall back to UCS-2 as described
in TS 102 221 Annex A."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
# one of the magic bytes of TS 102 221 Annex A
if obj[0] in [0x80, 0x81, 0x82]:
ad = Ucs2Adapter(GreedyBytes)
else:
ad = GsmString(GreedyBytes)
return ad._decode(obj, context, path)
def _encode(self, obj, context, path):
# first try GSM 03.38; then fall back to TS 102 221 Annex A UCS-2
try:
ad = GsmString(GreedyBytes)
return ad._encode(obj, context, path)
except:
ad = Ucs2Adapter(GreedyBytes)
return ad._encode(obj, context, path)
class Ucs2Adapter(Adapter):
"""convert a bytes() type that contains UCS2 encoded characters encoded as defined in TS 102 221
Annex A to normal python string representation (and back)."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
if obj[0] == 0x80:
# TS 102 221 Annex A Variant 1
return codecs.decode(obj[1:], 'utf_16_be')
elif obj[0] == 0x81:
# TS 102 221 Annex A Variant 2
out = ""
# second byte contains a value indicating the number of characters
num_of_chars = obj[1]
# the third byte contains an 8 bit number which defines bits 15 to 8 of a 16 bit base
# pointer, where bit 16 is set to zero, and bits 7 to 1 are also set to zero. These
# sixteen bits constitute a base pointer to a "half-page" in the UCS2 code space
base_ptr = obj[2] << 7
for ch in obj[3:3+num_of_chars]:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
# GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, then
# the remaining seven bits are an offset value added to the 16 bit base pointer
# defined earlier, and the resultant 16 bit value is a UCS2 code point
if ch & 0x80:
codepoint = (ch & 0x7f) + base_ptr
out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
else:
out += codecs.decode(bytes([ch]), 'gsm03.38')
return out
elif obj[0] == 0x82:
# TS 102 221 Annex A Variant 3
out = ""
# second byte contains a value indicating the number of characters
num_of_chars = obj[1]
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space, for use with some or all of the
# remaining bytes in the string
base_ptr = obj[2] << 8 | obj[3]
for ch in obj[4:4+num_of_chars]:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
# GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, the
# remaining seven bits are an offset value added to the base pointer defined in
# bytes three and four, and the resultant 16 bit value is a UCS2 code point, else: #
# GSM default alphabet
if ch & 0x80:
codepoint = (ch & 0x7f) + base_ptr
out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
else:
out += codecs.decode(bytes([ch]), 'gsm03.38')
return out
else:
raise ValueError('First byte of TS 102 221 UCS-2 must be 0x80, 0x81 or 0x82')
def _encode(self, obj, context, path):
def encodable_in_gsm338(instr: str) -> bool:
"""Determine if given input string is encode-ale in gsm03.38."""
try:
# TODO: figure out if/how we can constrain to default alphabet. The gsm0338
# library seems to include the spanish lock/shift table
codecs.encode(instr, 'gsm03.38')
except ValueError:
return False
return True
def codepoints_not_in_gsm338(instr: str) -> typing.List[int]:
"""Return an integer list of UCS2 codepoints for all characters of 'inster'
which are not representable in the GSM 03.38 default alphabet."""
codepoint_list = []
for c in instr:
if encodable_in_gsm338(c):
continue
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
codepoint_list.append(c_codepoint)
return codepoint_list
def diff_between_min_and_max_of_list(inlst: typing.List) -> int:
return max(inlst) - min(inlst)
def encodable_in_variant2(instr: str) -> bool:
codepoint_prefix = None
for c in instr:
if encodable_in_gsm338(c):
continue
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
if c_codepoint >= 0x8000:
return False
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
else:
if c_prefix != codepoint_prefix:
return False
return True
def encodable_in_variant3(instr: str) -> bool:
codepoint_list = codepoints_not_in_gsm338(instr)
# compute delta between max and min; check if it's encodable in 7 bits
if diff_between_min_and_max_of_list(codepoint_list) >= 0x80:
return False
return True
def _encode_variant1(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 1"""
return b'\x80' + codecs.encode(obj, 'utf_16_be')
def _encode_variant2(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 2"""
codepoint_prefix = None
# second byte contains a value indicating the number of characters
hdr = b'\x81' + len(instr).to_bytes(1, byteorder='big')
chars = b''
for c in instr:
try:
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
assert codepoint_prefix == c_prefix
enc = (0x80 + (c_codepoint & 0x7f)).to_bytes(1, byteorder='big')
chars += enc
if codepoint_prefix == None:
codepoint_prefix = 0
return hdr + codepoint_prefix.to_bytes(1, byteorder='big') + chars
def _encode_variant3(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 3"""
# second byte contains a value indicating the number of characters
hdr = b'\x82' + len(instr).to_bytes(1, byteorder='big')
chars = b''
codepoint_list = codepoints_not_in_gsm338(instr)
codepoint_base = min(codepoint_list)
for c in instr:
try:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a GSM
# Default # Alphabet character
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
# if bit 8 of the byte is set to one, the remaining seven bits are an offset
# value added to the base pointer defined in bytes three and four, and the
# resultant 16 bit value is a UCS2 code point
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_codepoint_delta = c_codepoint - codepoint_base
assert c_codepoint_delta < 0x80
enc = (0x80 + c_codepoint_delta).to_bytes(1, byteorder='big')
chars += enc
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space
return hdr + codepoint_base.to_bytes(2, byteorder='big') + chars
if encodable_in_variant2(obj):
return _encode_variant2(obj)
elif encodable_in_variant3(obj):
return _encode_variant3(obj)
else:
return _encode_variant1(obj)
class BcdAdapter(Adapter):
"""convert a bytes() type to a string of BCD nibbles."""
def _decode(self, obj, context, path):
return swap_nibbles(b2h(obj))
def _encode(self, obj, context, path):
return h2b(swap_nibbles(obj))
class PlmnAdapter(BcdAdapter):
"""convert a bytes(3) type to BCD string like 262-02 or 262-002."""
def _decode(self, obj, context, path):
bcd = super()._decode(obj, context, path)
if bcd[3] == 'f':
return '-'.join([bcd[:3], bcd[4:]])
else:
return '-'.join([bcd[:3], bcd[3:]])
def _encode(self, obj, context, path):
l = obj.split('-')
if len(l[1]) == 2:
bcd = l[0] + 'f' + l[1]
else:
bcd = l[0] + l[1]
return super()._encode(bcd, context, path)
class InvertAdapter(Adapter):
"""inverse logic (false->true, true->false)."""
@staticmethod
def _invert_bool_in_obj(obj):
for k,v in obj.items():
# skip all private entries
if k.startswith('_'):
continue
if v == False:
obj[k] = True
elif v == True:
obj[k] = False
return obj
def _decode(self, obj, context, path):
return self._invert_bool_in_obj(obj)
def _encode(self, obj, context, path):
return self._invert_bool_in_obj(obj)
class Rpad(Adapter):
"""
Encoder appends padding bytes (b'\\xff') or characters up to target size.
Decoder removes trailing padding bytes/characters.
Parameters:
subcon: Subconstruct as defined by construct library
pattern: set padding pattern (default: b'\\xff')
num_per_byte: number of 'elements' per byte. E.g. for hex nibbles: 2
"""
def __init__(self, subcon, pattern=b'\xff', num_per_byte=1):
super().__init__(subcon)
self.pattern = pattern
self.num_per_byte = num_per_byte
def _decode(self, obj, context, path):
return obj.rstrip(self.pattern)
def _encode(self, obj, context, path):
target_size = self.sizeof() * self.num_per_byte
if len(obj) > target_size:
raise SizeofError("Input ({}) exceeds target size ({})".format(
len(obj), target_size))
return obj + self.pattern * (target_size - len(obj))
class MultiplyAdapter(Adapter):
"""
Decoder multiplies by multiplicator
Encoder divides by multiplicator
Parameters:
subcon: Subconstruct as defined by construct library
multiplier: Multiplier to apply to raw encoded value
"""
def __init__(self, subcon, multiplicator):
super().__init__(subcon)
self.multiplicator = multiplicator
def _decode(self, obj, context, path):
return obj * 8
def _encode(self, obj, context, path):
return obj // 8
class GsmStringAdapter(Adapter):
"""Convert GSM 03.38 encoded bytes to a string."""
def __init__(self, subcon, codec='gsm03.38', err='strict'):
super().__init__(subcon)
self.codec = codec
self.err = err
def _decode(self, obj, context, path):
return obj.decode(self.codec)
def _encode(self, obj, context, path):
return obj.encode(self.codec, self.err)
class Ipv4Adapter(Adapter):
"""
Encoder converts from 4 bytes to string representation (A.B.C.D).
Decoder converts from string representation (A.B.C.D) to four bytes.
"""
def _decode(self, obj, context, path):
ia = ipaddress.IPv4Address(obj)
return ia.compressed
def _encode(self, obj, context, path):
ia = ipaddress.IPv4Address(obj)
return ia.packed
class Ipv6Adapter(Adapter):
"""
Encoder converts from 16 bytes to string representation.
Decoder converts from string representation to 16 bytes.
"""
def _decode(self, obj, context, path):
ia = ipaddress.IPv6Address(obj)
return ia.compressed
def _encode(self, obj, context, path):
ia = ipaddress.IPv6Address(obj)
return ia.packed
def filter_dict(d, exclude_prefix='_'):
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
if not isinstance(d, dict):
return d
res = {}
for (key, value) in d.items():
if key.startswith(exclude_prefix):
continue
if type(value) is dict:
res[key] = filter_dict(value)
else:
res[key] = value
return res
def normalize_construct(c):
"""Convert a construct specific type to a related base type, mostly useful
so we can serialize it."""
# we need to include the filter_dict as we otherwise get elements like this
# in the dict: '_io': <_io.BytesIO object at 0x7fdb64e05860> which we cannot json-serialize
c = filter_dict(c)
if isinstance(c, Container) or isinstance(c, dict):
r = {k: normalize_construct(v) for (k, v) in c.items()}
elif isinstance(c, ListContainer):
r = [normalize_construct(x) for x in c]
elif isinstance(c, list):
r = [normalize_construct(x) for x in c]
elif isinstance(c, EnumIntegerString):
r = str(c)
else:
r = c
return r
def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_', context: dict = {}):
"""Helper function to wrap around normalize_construct() and filter_dict()."""
if not length:
length = len(raw_bin_data)
try:
parsed = c.parse(raw_bin_data, total_len=length, **context)
except StreamError as e:
# if the input is all-ff, this means the content is undefined. Let's avoid passing StreamError
# exceptions in those situations (which might occur if a length field 0xff is 255 but then there's
# actually less bytes in the remainder of the file.
if all([v == 0xff for v in raw_bin_data]):
return None
else:
raise e
return normalize_construct(parsed)
def build_construct(c, decoded_data, context: dict = {}):
"""Helper function to handle total_len."""
return c.build(decoded_data, total_len=None, **context)
# here we collect some shared / common definitions of data types
LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
# Default value for Reserved for Future Use (RFU) bits/bytes
# See TS 31.101 Sec. "3.4 Coding Conventions"
__RFU_VALUE = 0
# Field that packs Reserved for Future Use (RFU) bit
FlagRFU = Default(Flag, __RFU_VALUE)
# Field that packs Reserved for Future Use (RFU) byte
ByteRFU = Default(Byte, __RFU_VALUE)
# Field that packs all remaining Reserved for Future Use (RFU) bytes
GreedyBytesRFU = Default(GreedyBytes, b'')
def BitsRFU(n=1):
'''
Field that packs Reserved for Future Use (RFU) bit(s)
as defined in TS 31.101 Sec. "3.4 Coding Conventions"
Use this for (currently) unused/reserved bits whose contents
should be initialized automatically but should not be cleared
in the future or when restoring read data (unlike padding).
Parameters:
n (Integer): Number of bits (default: 1)
'''
return Default(BitsInteger(n), __RFU_VALUE)
def BytesRFU(n=1):
'''
Field that packs Reserved for Future Use (RFU) byte(s)
as defined in TS 31.101 Sec. "3.4 Coding Conventions"
Use this for (currently) unused/reserved bytes whose contents
should be initialized automatically but should not be cleared
in the future or when restoring read data (unlike padding).
Parameters:
n (Integer): Number of bytes (default: 1)
'''
return Default(Bytes(n), __RFU_VALUE)
def GsmString(n):
'''
GSM 03.38 encoded byte string of fixed length n.
Encoder appends padding bytes (b'\\xff') to maintain
length. Decoder removes those trailing bytes.
Exceptions are raised for invalid characters
and length excess.
Parameters:
n (Integer): Fixed length of the encoded byte string
'''
return GsmStringAdapter(Rpad(Bytes(n), pattern=b'\xff'), codec='gsm03.38')
def GsmOrUcs2String(n):
'''
GSM 03.38 or UCS-2 (TS 102 221 Annex A) encoded byte string of fixed length n.
Encoder appends padding bytes (b'\\xff') to maintain
length. Decoder removes those trailing bytes.
Exceptions are raised for invalid characters
and length excess.
Parameters:
n (Integer): Fixed length of the encoded byte string
'''
return GsmOrUcs2Adapter(Rpad(Bytes(n), pattern=b'\xff'))
class GreedyInteger(Construct):
"""A variable-length integer implementation, think of combining GrredyBytes with BytesInteger."""
def __init__(self, signed=False, swapped=False, minlen=0):
super().__init__()
self.signed = signed
self.swapped = swapped
self.minlen = minlen
def _parse(self, stream, context, path):
data = stream_read_entire(stream, path)
if evaluate(self.swapped, context):
data = swapbytes(data)
try:
return int.from_bytes(data, byteorder='big', signed=self.signed)
except ValueError as e:
raise IntegerError(str(e), path=path)
def __bytes_required(self, i, minlen=0):
if self.signed:
raise NotImplementedError("FIXME: Implement support for encoding signed integer")
# compute how many bytes we need
nbytes = 1
while True:
i = i >> 8
if i == 0:
break
else:
nbytes = nbytes + 1
# round up to the minimum number
# of bytes we anticipate
if nbytes < minlen:
nbytes = minlen
return nbytes
def _build(self, obj, stream, context, path):
if not isinstance(obj, integertypes):
raise IntegerError(f"value {obj} is not an integer", path=path)
length = self.__bytes_required(obj, self.minlen)
try:
data = obj.to_bytes(length, byteorder='big', signed=self.signed)
except ValueError as e:
raise IntegerError(str(e), path=path)
if evaluate(self.swapped, context):
data = swapbytes(data)
stream_write(stream, data, length, path)
return obj
# merged definitions of 24.008 + 23.040
TypeOfNumber = Enum(BitsInteger(3), unknown=0, international=1, national=2, network_specific=3,
short_code=4, alphanumeric=5, abbreviated=6, reserved_for_extension=7)
NumberingPlan = Enum(BitsInteger(4), unknown=0, isdn_e164=1, data_x121=3, telex_f69=4,
sc_specific_5=5, sc_specific_6=6, national=8, private=9,
ermes=10, reserved_cts=11, reserved_for_extension=15)
TonNpi = BitStruct('ext'/Flag, 'type_of_number'/TypeOfNumber, 'numbering_plan_id'/NumberingPlan)

View File

@@ -1,55 +1,10 @@
import sys
from typing import Optional, Tuple
from importlib import resources
class PMO:
"""Convenience conversion class for ProfileManagementOperation as used in ES9+ notifications."""
pmo4operation = {
'install': 0x80,
'enable': 0x40,
'disable': 0x20,
'delete': 0x10,
}
import asn1tools
def __init__(self, op: str):
if not op in self.pmo4operation:
raise ValueError('Unknown operation "%s"' % op)
self.op = op
def to_int(self):
return self.pmo4operation[self.op]
@staticmethod
def _num_bits(data: int)-> int:
for i in range(0, 8):
if data & (1 << i):
return 8-i
return 0
def to_bitstring(self) -> Tuple[bytes, int]:
"""return value in a format as used by asn1tools for BITSTRING."""
val = self.to_int()
return (bytes([val]), self._num_bits(val))
@classmethod
def from_int(cls, i: int) -> 'PMO':
"""Parse an integer representation."""
for k, v in cls.pmo4operation.items():
if v == i:
return cls(k)
raise ValueError('Unknown PMO 0x%02x' % i)
@classmethod
def from_bitstring(cls, bstr: Tuple[bytes, int]) -> 'PMO':
"""Parse a asn1tools BITSTRING representation."""
return cls.from_int(bstr[0][0])
def __str__(self):
return self.op
def compile_asn1_subdir(subdir_name:str, codec='der'):
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):
@@ -58,80 +13,4 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
asn_txt += "\n"
#else:
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
return asn1tools.compile_string(asn_txt, codec=codec)
# SGP.22 section 4.1 Activation Code
class ActivationCode:
def __init__(self, hostname:str, token:str, oid: Optional[str] = None, cc_required: Optional[bool] = False):
if '$' in hostname:
raise ValueError('$ sign not permitted in hostname')
self.hostname = hostname
if '$' in token:
raise ValueError('$ sign not permitted in token')
self.token = token
# TODO: validate OID
self.oid = oid
self.cc_required = cc_required
# only format 1 is specified and supported here
self.format = 1
@staticmethod
def decode_str(ac: str) -> dict:
if ac[0] != '1':
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
ac_elements = ac.split('$')
d = {
'oid': None,
'cc_required': False,
}
d['format'] = ac_elements.pop(0)
d['hostname'] = ac_elements.pop(0)
d['token'] = ac_elements.pop(0)
if len(ac_elements):
oid = ac_elements.pop(0)
if oid != '':
d['oid'] = oid
if len(ac_elements):
ccr = ac_elements.pop(0)
if ccr == '1':
d['cc_required'] = True
return d
@classmethod
def from_string(cls, ac: str) -> 'ActivationCode':
"""Create new instance from SGP.22 section 4.1 string representation."""
d = cls.decode_str(ac)
return cls(d['hostname'], d['token'], d['oid'], d['cc_required'])
def to_string(self, for_qrcode:bool = False) -> str:
"""Convert from internal representation to SGP.22 section 4.1 string representation."""
if for_qrcode:
ret = 'LPA:'
else:
ret = ''
ret += '%d$%s$%s' % (self.format, self.hostname, self.token)
if self.oid:
ret += '$%s' % (self.oid)
elif self.cc_required:
ret += '$'
if self.cc_required:
ret += '$1'
return ret
def __str__(self):
return self.to_string()
def to_qrcode(self):
"""Encode internal representation to QR code."""
import qrcode
qr = qrcode.QRCode()
qr.add_data(self.to_string(for_qrcode=True))
return qr.make_image()
def __repr__(self):
return "ActivationCode(format=%u, hostname='%s', token='%s', oid=%s, cc_required=%s)" % (self.format,
self.hostname,
self.token,
self.oid,
self.cc_required)
return asn1tools.compile_string(asn_txt, codec='der')

View File

@@ -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

View File

@@ -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
@@ -35,15 +36,12 @@ from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
from Cryptodome.Cipher import AES
from Cryptodome.Hash import CMAC
from osmocom.utils import b2h
from osmocom.tlv import bertlv_encode_len, bertlv_parse_one
from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
# don't log by default
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
MAX_SEGMENT_SIZE = 1020
class BspAlgo(abc.ABC):
blocksize: int
@@ -52,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__
@@ -83,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'
@@ -165,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'
@@ -287,9 +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)
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
self.c_algo.block_nr += 1
tdict, l, val, remain = bertlv_parse_one(payload)
return val
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:

View File

@@ -1,239 +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 requests
import logging
from datetime import datetime
import time
from pySim.esim.http_json_api import *
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class param:
class Iccid(ApiParamString):
"""String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
character F."""
@classmethod
def _encode(cls, data):
data = str(data)
# SGP.22 version prior to 2.2 do not require support for 19-digit ICCIDs, so let's always
# encode it with padding F at the end.
if len(data) == 19:
data += 'F'
return data
@classmethod
def verify_encoded(cls, data):
if len(data) not in [19, 20]:
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
@classmethod
def _decode(cls, data):
# strip trailing padding (if it's 20 digits)
if len(data) == 20 and data[-1] in ['F', 'f']:
data = data[:-1]
return data
@classmethod
def verify_decoded(cls, data):
data = str(data)
if len(data) not in [19, 20]:
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
if len(data) == 19:
decimal_part = data
else:
decimal_part = data[:-1]
final_part = data[-1:]
if final_part not in ['F', 'f'] and not final_part.isdecimal():
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
if not decimal_part.isdecimal():
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
class Eid(ApiParamString):
"""String of 32 decimal characters"""
@classmethod
def verify_encoded(cls, data):
if len(data) != 32:
raise ValueError('EID length invalid: "%s" (%u)' % (data, len(data)))
@classmethod
def verify_decoded(cls, data):
if not data.isdecimal():
raise ValueError('EID (%s) contains non-decimal characters' % data)
class ProfileType(ApiParamString):
pass
class MatchingId(ApiParamString):
pass
class ConfirmationCode(ApiParamString):
pass
class SmdsAddress(ApiParamFqdn):
pass
class ReleaseFlag(ApiParamBoolean):
pass
class FinalProfileStatusIndicator(ApiParamString):
pass
class Timestamp(ApiParamString):
"""String format as specified by W3C: YYYY-MM-DDThh:mm:ssTZD"""
@classmethod
def _decode(cls, data):
return datetime.fromisoformat(data)
@classmethod
def _encode(cls, data):
return datetime.isoformat(data)
class NotificationPointId(ApiParamInteger):
pass
class NotificationPointStatus(ApiParam):
pass
class ResultData(ApiParamBase64):
pass
class Es2PlusApiFunction(JsonHttpApiFunction):
"""Base classs for representing an ES2+ API Function."""
pass
# ES2+ DownloadOrder function (SGP.22 section 5.3.1)
class DownloadOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/downloadOrder'
input_params = {
'eid': param.Eid,
'iccid': param.Iccid,
'profileType': param.ProfileType
}
output_params = {
'header': JsonResponseHeader,
'iccid': param.Iccid,
}
output_mandatory = ['header', 'iccid']
# ES2+ ConfirmOrder function (SGP.22 section 5.3.2)
class ConfirmOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/confirmOrder'
input_params = {
'iccid': param.Iccid,
'eid': param.Eid,
'matchingId': param.MatchingId,
'confirmationCode': param.ConfirmationCode,
'smdsAddress': param.SmdsAddress,
'releaseFlag': param.ReleaseFlag,
}
input_mandatory = ['iccid', 'releaseFlag']
output_params = {
'header': JsonResponseHeader,
'eid': param.Eid,
'matchingId': param.MatchingId,
'smdpAddress': SmdpAddress,
}
output_mandatory = ['header', 'matchingId']
# ES2+ CancelOrder function (SGP.22 section 5.3.3)
class CancelOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/cancelOrder'
input_params = {
'iccid': param.Iccid,
'eid': param.Eid,
'matchingId': param.MatchingId,
'finalProfileStatusIndicator': param.FinalProfileStatusIndicator,
}
input_mandatory = ['finalProfileStatusIndicator', 'iccid']
output_params = {
'header': JsonResponseHeader,
}
output_mandatory = ['header']
# ES2+ ReleaseProfile function (SGP.22 section 5.3.4)
class ReleaseProfile(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/releaseProfile'
input_params = {
'iccid': param.Iccid,
}
input_mandatory = ['iccid']
output_params = {
'header': JsonResponseHeader,
}
output_mandatory = ['header']
# ES2+ HandleDownloadProgress function (SGP.22 section 5.3.5)
class HandleDownloadProgressInfo(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/handleDownloadProgressInfo'
input_params = {
'eid': param.Eid,
'iccid': param.Iccid,
'profileType': param.ProfileType,
'timestamp': param.Timestamp,
'notificationPointId': param.NotificationPointId,
'notificationPointStatus': param.NotificationPointStatus,
'resultData': param.ResultData,
}
input_mandatory = ['iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
expected_http_status = 204
class Es2pApiClient:
"""Main class representing a full ES2+ API client. Has one method for each API function."""
def __init__(self, url_prefix:str, func_req_id:str, server_cert_verify: str = None, client_cert: str = None):
self.func_id = 0
self.session = requests.Session()
if server_cert_verify:
self.session.verify = server_cert_verify
if client_cert:
self.session.cert = client_cert
self.downloadOrder = DownloadOrder(url_prefix, func_req_id, self.session)
self.confirmOrder = ConfirmOrder(url_prefix, func_req_id, self.session)
self.cancelOrder = CancelOrder(url_prefix, func_req_id, self.session)
self.releaseProfile = ReleaseProfile(url_prefix, func_req_id, self.session)
self.handleDownloadProgressInfo = HandleDownloadProgressInfo(url_prefix, func_req_id, self.session)
def _gen_func_id(self) -> str:
"""Generate the next function call id."""
self.func_id += 1
return 'FCI-%u-%u' % (time.time(), self.func_id)
def call_downloadOrder(self, data: dict) -> dict:
"""Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
return self.downloadOrder.call(data, self._gen_func_id())
def call_confirmOrder(self, data: dict) -> dict:
"""Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
return self.confirmOrder.call(data, self._gen_func_id())
def call_cancelOrder(self, data: dict) -> dict:
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
return self.cancelOrder.call(data, self._gen_func_id())
def call_releaseProfile(self, data: dict) -> dict:
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
return self.releaseProfile.call(data, self._gen_func_id())
def call_handleDownloadProgressInfo(self, data: dict) -> dict:
"""Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
return self.handleDownloadProgressInfo.call(data, self._gen_func_id())

View File

@@ -17,14 +17,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Dict, List, Optional
from cryptography.hazmat.primitives.asymmetric import ec
from osmocom.utils import b2h, h2b
from osmocom.tlv import bertlv_encode_tag, bertlv_encode_len, bertlv_parse_one_rawtag
from osmocom.tlv import bertlv_return_one_rawtlv
from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len
import pySim.esim.rsp as rsp
from pySim.esim.bsp import BspInstance
from pySim.esim import PMO
# Given that GSMA RSP uses ASN.1 in a very weird way, we actually cannot encode the full data type before
# signing, but we have to build parts of it separately first, then sign that, so we can put the signature
@@ -78,23 +74,6 @@ class ProfileMetadata:
self.iccid_bin = iccid_bin
self.spn = spn
self.profile_name = profile_name
self.icon = None
self.icon_type = None
self.notifications = []
def set_icon(self, is_png: bool, icon_data: bytes):
"""Set the icon that is part of the metadata."""
if len(icon_data) > 1024:
raise ValueError('Icon data must not exceed 1024 bytes')
self.icon = icon_data
if is_png:
self.icon_type = 1
else:
self.icon_type = 0
def add_notification(self, event: str, address: str):
"""Add an 'other' notification to the notification configuration of the metadata"""
self.notifications.append((event, address))
def gen_store_metadata_request(self) -> bytes:
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
@@ -103,15 +82,6 @@ class ProfileMetadata:
'serviceProviderName': self.spn,
'profileName': self.profile_name,
}
if self.icon:
smr['icon'] = self.icon
smr['iconType'] = self.icon_type
nci = []
for n in self.notifications:
pmo = PMO(n[0])
nci.append({'profileManagementOperation': pmo.to_bitstring(), 'notificationAddress': n[1]})
if len(nci):
smr['notificationConfigurationInfo'] = nci
return rsp.asn1.encode('StoreMetadataRequest', smr)
@@ -213,79 +183,3 @@ class BoundProfilePackage(ProfilePackage):
# manual DER encode: wrap in outer SEQUENCE
return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq
def decode(self, euicc_ot, eid: str, bpp_bin: bytes):
"""Decode a BPP into the PPP and subsequently UPP. This is what happens inside an eUICC."""
def split_bertlv_sequence(sequence: bytes) -> List[bytes]:
remainder = sequence
ret = []
while remainder:
_tag, _l, tlv, remainder = bertlv_return_one_rawtlv(remainder)
ret.append(tlv)
return ret
# we don't use rsp.asn1.decode('boundProfilePackage') here, as the BSP needs
# fully encoded + MACed TLVs including their tag + length values.
#bpp = rsp.asn1.decode('BoundProfilePackage', bpp_bin)
tag, _l, v, _remainder = bertlv_parse_one_rawtag(bpp_bin)
if len(_remainder):
raise ValueError('Excess data at end of TLV')
if tag != 0xbf36:
raise ValueError('Unexpected outer tag: %s' % tag)
# InitialiseSecureChannelRequest
tag, _l, iscr_bin, remainder = bertlv_return_one_rawtlv(v)
iscr = rsp.asn1.decode('InitialiseSecureChannelRequest', iscr_bin)
# configureIsdpRequest
tag, _l, firstSeqOf87, remainder = bertlv_parse_one_rawtag(remainder)
if tag != 0xa0:
raise ValueError("Unexpected 'firstSequenceOf87' tag: %s" % tag)
firstSeqOf87 = split_bertlv_sequence(firstSeqOf87)
# storeMetadataRequest
tag, _l, seqOf88, remainder = bertlv_parse_one_rawtag(remainder)
if tag != 0xa1:
raise ValueError("Unexpected 'sequenceOf88' tag: %s" % tag)
seqOf88 = split_bertlv_sequence(seqOf88)
tag, _l, tlv, remainder = bertlv_parse_one_rawtag(remainder)
if tag == 0xa2:
secondSeqOf87 = split_bertlv_sequence(tlv)
tag2, _l, seqOf86, remainder = bertlv_parse_one_rawtag(remainder)
if tag2 != 0xa3:
raise ValueError("Unexpected 'sequenceOf86' tag: %s" % tag)
seqOf86 = split_bertlv_sequence(seqOf86)
elif tag == 0xa3:
secondSeqOf87 = None
seqOf86 = split_bertlv_sequence(tlv)
else:
raise ValueError("Unexpected 'secondSequenceOf87' tag: %s" % tag)
# extract smdoOtpk from initialiseSecureChannel
smdp_otpk = iscr['smdpOtpk']
# Generate Session Keys using the CRT, opPK.DP.ECKA and otSK.EUICC.ECKA according to annex G
smdp_public_key = ec.EllipticCurvePublicKey.from_encoded_point(euicc_ot.curve, smdp_otpk)
self.shared_secret = euicc_ot.exchange(ec.ECDH(), smdp_public_key)
crt = iscr['controlRefTemplate']
bsp = BspInstance.from_kdf(self.shared_secret, int.from_bytes(crt['keyType'], 'big'), int.from_bytes(crt['keyLen'], 'big'), crt['hostId'], h2b(eid))
self.encoded_configureISDPRequest = bsp.demac_and_decrypt(firstSeqOf87)
self.configureISDPRequest = rsp.asn1.decode('ConfigureISDPRequest', self.encoded_configureISDPRequest)
self.encoded_storeMetadataRequest = bsp.demac_only(seqOf88)
self.storeMetadataRequest = rsp.asn1.decode('StoreMetadataRequest', self.encoded_storeMetadataRequest)
if secondSeqOf87 != None:
rsk_bin = bsp.demac_and_decrypt(secondSeqOf87)
rsk = rsp.asn1.decode('ReplaceSessionKeysRequest', rsk_bin)
# process replace_session_keys!
bsp = BspInstance(rsk['ppkEnc'], rsk['ppkCmac'], rsk['initialMacChainingValue'])
self.replaceSessionKeysRequest = rsk
self.upp = bsp.demac_and_decrypt(seqOf86)
return self.upp

View File

@@ -1,177 +0,0 @@
"""GSMA eSIM RSP ES9+ interface according ot SGP.22 v2.5"""
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
import logging
import time
import pySim.esim.rsp as rsp
from pySim.esim.http_json_api import *
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class param:
class RspAsn1Par(ApiParamBase64):
"""Generalized RSP ASN.1 parameter: base64-wrapped ASN.1 DER. Derived classes must provide
the asn1_type class variable to indicate the name of the ASN.1 type to use for encode/decode."""
asn1_type = None # must be overridden by derived class
@classmethod
def _decode(cls, data):
data = ApiParamBase64.decode(data)
return rsp.asn1.decode(cls.asn1_type, data)
@classmethod
def _encode(cls, data):
data = rsp.asn1.encode(cls.asn1_type, data)
return ApiParamBase64.encode(data)
class EuiccInfo1(RspAsn1Par):
asn1_type = 'EUICCInfo1'
class ServerSigned1(RspAsn1Par):
asn1_type = 'ServerSigned1'
class PrepareDownloadResponse(RspAsn1Par):
asn1_type = 'PrepareDownloadResponse'
class AuthenticateServerResponse(RspAsn1Par):
asn1_type = 'AuthenticateServerResponse'
class SmdpSigned2(RspAsn1Par):
asn1_type = 'SmdpSigned2'
class StoreMetadataRequest(RspAsn1Par):
asn1_type = 'StoreMetadataRequest'
class PendingNotification(RspAsn1Par):
asn1_type = 'PendingNotification'
class CancelSessionResponse(RspAsn1Par):
asn1_type = 'CancelSessionResponse'
class TransactionId(ApiParamString):
pass
class Es9PlusApiFunction(JsonHttpApiFunction):
pass
# ES9+ InitiateAuthentication function (SGP.22 section 6.5.2.6)
class InitiateAuthentication(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/initiateAuthentication'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'euiccChallenge': ApiParamBase64,
'euiccInfo1': param.EuiccInfo1,
'smdpAddress': SmdpAddress,
}
input_mandatory = ['euiccChallenge', 'euiccInfo1', 'smdpAddress']
output_params = {
'header': JsonResponseHeader,
'transactionId': param.TransactionId,
'serverSigned1': param.ServerSigned1,
'serverSignature1': ApiParamBase64,
'euiccCiPKIdToBeUsed': ApiParamBase64,
'serverCertificate': ApiParamBase64,
}
output_mandatory = ['header', 'transactionId', 'serverSigned1', 'serverSignature1',
'euiccCiPKIdToBeUsed', 'serverCertificate']
# ES9+ GetBoundProfilePackage function (SGP.22 section 6.5.2.7)
class GetBoundProfilePackage(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/getBoundProfilePackage'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'transactionId': param.TransactionId,
'prepareDownloadResponse': param.PrepareDownloadResponse,
}
input_mandatory = ['transactionId', 'prepareDownloadResponse']
output_params = {
'header': JsonResponseHeader,
'transactionId': param.TransactionId,
'boundProfilePackage': ApiParamBase64,
}
output_mandatory = ['header', 'transactionId', 'boundProfilePackage']
# ES9+ AuthenticateClient function (SGP.22 section 6.5.2.8)
class AuthenticateClient(Es9PlusApiFunction):
path= '/gsma/rsp2/es9plus/authenticateClient'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'transactionId': param.TransactionId,
'authenticateServerResponse': param.AuthenticateServerResponse,
}
input_mandatory = ['transactionId', 'authenticateServerResponse']
output_params = {
'header': JsonResponseHeader,
'transactionId': param.TransactionId,
'profileMetadata': param.StoreMetadataRequest,
'smdpSigned2': param.SmdpSigned2,
'smdpSignature2': ApiParamBase64,
'smdpCertificate': ApiParamBase64,
}
output_mandatory = ['header', 'transactionId', 'profileMetadata', 'smdpSigned2',
'smdpSignature2', 'smdpCertificate']
# ES9+ HandleNotification function (SGP.22 section 6.5.2.9)
class HandleNotification(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/handleNotification'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'pendingNotification': param.PendingNotification,
}
input_mandatory = ['pendingNotification']
expected_http_status = 204
# ES9+ CancelSession function (SGP.22 section 6.5.2.10)
class CancelSession(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/cancelSession'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'transactionId': param.TransactionId,
'cancelSessionResponse': param.CancelSessionResponse,
}
input_mandatory = ['transactionId', 'cancelSessionResponse']
class Es9pApiClient:
def __init__(self, url_prefix:str, server_cert_verify: str = None):
self.session = requests.Session()
self.session.verify = False # FIXME HACK
if server_cert_verify:
self.session.verify = server_cert_verify
self.initiateAuthentication = InitiateAuthentication(url_prefix, '', self.session)
self.authenticateClient = AuthenticateClient(url_prefix, '', self.session)
self.getBoundProfilePackage = GetBoundProfilePackage(url_prefix, '', self.session)
self.handleNotification = HandleNotification(url_prefix, '', self.session)
self.cancelSession = CancelSession(url_prefix, '', self.session)
def call_initiateAuthentication(self, data: dict) -> dict:
return self.initiateAuthentication.call(data)
def call_authenticateClient(self, data: dict) -> dict:
return self.authenticateClient.call(data)
def call_getBoundProfilePackage(self, data: dict) -> dict:
return self.getBoundProfilePackage.call(data)
def call_handleNotification(self, data: dict) -> dict:
return self.handleNotification.call(data)
def call_cancelSession(self, data: dict) -> dict:
return self.cancelSession.call(data)

View File

@@ -1,259 +0,0 @@
"""GSMA eSIM RSP HTTP/REST/JSON interface according to SGP.22 v2.5"""
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import requests
import logging
import json
from typing import Optional
import base64
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class ApiParam(abc.ABC):
"""A class reprsenting a single parameter in the API."""
@classmethod
def verify_decoded(cls, data):
"""Verify the decoded reprsentation of a value. Should raise an exception if somthing is odd."""
pass
@classmethod
def verify_encoded(cls, data):
"""Verify the encoded reprsentation of a value. Should raise an exception if somthing is odd."""
pass
@classmethod
def encode(cls, data):
"""[Validate and] Encode the given value."""
cls.verify_decoded(data)
encoded = cls._encode(data)
cls.verify_decoded(encoded)
return encoded
@classmethod
def _encode(cls, data):
"""encoder function, typically [but not always] overridden by derived class."""
return data
@classmethod
def decode(cls, data):
"""[Validate and] Decode the given value."""
cls.verify_encoded(data)
decoded = cls._decode(data)
cls.verify_decoded(decoded)
return decoded
@classmethod
def _decode(cls, data):
"""decoder function, typically [but not always] overridden by derived class."""
return data
class ApiParamString(ApiParam):
"""Base class representing an API parameter of 'string' type."""
pass
class ApiParamInteger(ApiParam):
"""Base class representing an API parameter of 'integer' type."""
@classmethod
def _decode(cls, data):
return int(data)
@classmethod
def _encode(cls, data):
return str(data)
@classmethod
def verify_decoded(cls, data):
if not isinstance(data, int):
raise TypeError('Expected an integer input data type')
@classmethod
def verify_encoded(cls, data):
if isinstance(data, int):
return
if not data.isdecimal():
raise ValueError('integer (%s) contains non-decimal characters' % data)
assert str(int(data)) == data
class ApiParamBoolean(ApiParam):
"""Base class representing an API parameter of 'boolean' type."""
@classmethod
def _encode(cls, data):
return bool(data)
class ApiParamFqdn(ApiParam):
"""String, as a list of domain labels concatenated using the full stop (dot, period) character as
separator between labels. Labels are restricted to the Alphanumeric mode character set defined in table 5
of ISO/IEC 18004"""
@classmethod
def verify_encoded(cls, data):
# FIXME
pass
class ApiParamBase64(ApiParam):
@classmethod
def _decode(cls, data):
return base64.b64decode(data)
@classmethod
def _encode(cls, data):
return base64.b64encode(data).decode('ascii')
class SmdpAddress(ApiParamFqdn):
pass
class JsonResponseHeader(ApiParam):
"""SGP.22 section 6.5.1.4."""
@classmethod
def verify_decoded(cls, data):
fe_status = data.get('functionExecutionStatus')
if not fe_status:
raise ValueError('Missing mandatory functionExecutionStatus in header')
status = fe_status.get('status')
if not status:
raise ValueError('Missing mandatory status in header functionExecutionStatus')
if status not in ['Executed-Success', 'Executed-WithWarning', 'Failed', 'Expired']:
raise ValueError('Unknown/unspecified status "%s"' % status)
class HttpStatusError(Exception):
pass
class HttpHeaderError(Exception):
pass
class ApiError(Exception):
"""Exception representing an error at the API level (status != Executed)."""
def __init__(self, func_ex_status: dict):
self.status = func_ex_status['status']
sec = {
'subjectCode': None,
'reasonCode': None,
'subjectIdentifier': None,
'message': None,
}
actual_sec = func_ex_status.get('statusCodeData', None)
sec.update(actual_sec)
self.subject_code = sec['subjectCode']
self.reason_code = sec['reasonCode']
self.subject_id = sec['subjectIdentifier']
self.message = sec['message']
def __str__(self):
return f'{self.status}("{self.subject_code}","{self.reason_code}","{self.subject_id}","{self.message}")'
class JsonHttpApiFunction(abc.ABC):
"""Base classs for representing an HTTP[s] API Function."""
# the below class variables are expected to be overridden in derived classes
path = None
# dictionary of input parameters. key is parameter name, value is ApiParam class
input_params = {}
# list of mandatory input parameters
input_mandatory = []
# dictionary of output parameters. key is parameter name, value is ApiParam class
output_params = {}
# list of mandatory output parameters (for successful response)
output_mandatory = []
# expected HTTP status code of the response
expected_http_status = 200
# the HTTP method used (GET, OPTIONS, HEAD, POST, PUT, PATCH or DELETE)
http_method = 'POST'
extra_http_req_headers = {}
def __init__(self, url_prefix: str, func_req_id: Optional[str], session: requests.Session):
self.url_prefix = url_prefix
self.func_req_id = func_req_id
self.session = session
def encode(self, data: dict, func_call_id: Optional[str] = None) -> dict:
"""Validate an encode input dict into JSON-serializable dict for request body."""
output = {}
if func_call_id:
output['header'] = {
'functionRequesterIdentifier': self.func_req_id,
'functionCallIdentifier': func_call_id
}
for p in self.input_mandatory:
if not p in data:
raise ValueError('Mandatory input parameter %s missing' % p)
for p, v in data.items():
p_class = self.input_params.get(p)
if not p_class:
logger.warning('Unexpected/unsupported input parameter %s=%s', p, v)
output[p] = v
else:
output[p] = p_class.encode(v)
return output
def decode(self, data: dict) -> dict:
"""[further] Decode and validate the JSON-Dict of the response body."""
output = {}
if 'header' in self.output_params:
# let's first do the header, it's special
if not 'header' in data:
raise ValueError('Mandatory output parameter "header" missing')
hdr_class = self.output_params.get('header')
output['header'] = hdr_class.decode(data['header'])
if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
raise ApiError(output['header']['functionExecutionStatus'])
# we can only expect mandatory parameters to be present in case of successful execution
for p in self.output_mandatory:
if p == 'header':
continue
if not p in data:
raise ValueError('Mandatory output parameter "%s" missing' % p)
for p, v in data.items():
p_class = self.output_params.get(p)
if not p_class:
logger.warning('Unexpected/unsupported output parameter "%s"="%s"', p, v)
output[p] = v
else:
output[p] = p_class.decode(v)
return output
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
"""Make an API call to the HTTP API endpoint represented by this object.
Input data is passed in `data` as json-serializable dict. Output data
is returned as json-deserialized dict."""
url = self.url_prefix + self.path
encoded = json.dumps(self.encode(data, func_call_id))
req_headers = {
'Content-Type': 'application/json',
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
}
req_headers.update(self.extra_http_req_headers)
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
response = self.session.request(self.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
logger.debug("HTTP RSP: %s" % (response.content))
if response.status_code != self.expected_http_status:
raise HttpStatusError(response)
if not response.headers.get('Content-Type').startswith(req_headers['Content-Type']):
raise HttpHeaderError(response)
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
raise HttpHeaderError(response)
if response.content:
return self.decode(response.json())
return None

View File

@@ -19,12 +19,12 @@
from typing import Optional
import shelve
import copyreg
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography import x509
from osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
from collections.abc import MutableMapping
from pySim.esim import compile_asn1_subdir
@@ -35,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 calls
self.ci_cert_id = ci_cert_id
# used at a later point between API calsl
self.euicc_cert: Optional[x509.Certificate] = None
self.eum_cert: Optional[x509.Certificate] = None
self.eid: Optional[bytes] = None
@@ -98,35 +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."""
def extract_euiccSigned1(authenticateServerResponse: bytes) -> bytes:
"""Extract the raw, DER-encoded binary euiccSigned1 field from the given AuthenticateServerResponse. This
is needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
rawtag, l, v, remainder = bertlv_parse_one_rawtag(authenticateServerResponse)
if len(remainder):
raise ValueError('Excess data at end of TLV')
if rawtag != 0xbf38:
raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
if rawtag != 0xa0:
raise ValueError('Unexpected tag where CHOICE was expected')
rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
if rawtag != 0x30:
raise ValueError('Unexpected tag where SEQUENCE was expected')
return tlv2
def extract_euiccSigned2(prepareDownloadResponse: bytes) -> bytes:
"""Extract the raw, DER-encoded binary euiccSigned2 field from the given prepareDownloadrResponse. This is
needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
rawtag, l, v, remainder = bertlv_parse_one_rawtag(prepareDownloadResponse)
if len(remainder):
raise ValueError('Excess data at end of TLV')
if rawtag != 0xbf21:
raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
if rawtag != 0xa0:
raise ValueError('Unexpected tag where CHOICE was expected')
rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
if rawtag != 0x30:
raise ValueError('Unexpected tag where SEQUENCE was expected')
return tlv2
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,120 +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 functools import total_ordering
from typing import List, Union
@total_ordering
class OID:
@staticmethod
def intlist_from_str(instr: str) -> List[int]:
return [int(x) for x in instr.split('.')]
@staticmethod
def str_from_intlist(intlist: List[int]) -> str:
return '.'.join([str(x) for x in intlist])
@staticmethod
def highest_oid(oids: List['OID']) -> 'OID':
return sorted(oids)[-1]
def __init__(self, initializer: Union[List[int], str]):
if isinstance(initializer, str):
self.intlist = self.intlist_from_str(initializer)
else:
self.intlist = initializer
def __str__(self) -> str:
return self.str_from_intlist(self.intlist)
def __repr__(self) -> str:
return 'OID(%s)' % (str(self))
def __eq__(self, other: 'OID'):
return (self.intlist == other.intlist)
def __ne__(self, other: 'OID'):
# implement based on __eq__
return not (self == other)
def cmp(self, other: 'OID'):
self_len = len(self.intlist)
other_len = len(other.intlist)
common_len = min(self_len, other_len)
max_len = max(self_len, other_len)
for i in range(0, max_len+1):
if i >= self_len:
# other list is longer
return -1
if i >= other_len:
# our list is longer
return 1
if self.intlist[i] > other.intlist[i]:
# our version is higher
return 1
if self.intlist[i] < other.intlist[i]:
# other version is higher
return -1
# continue to next digit
return 0
def __gt__(self, other: 'OID'):
if self.cmp(other) > 0:
return True
def prefix_match(self, oid_str: Union[str, 'OID']):
"""determine if oid_str is equal or below our OID."""
return str(oid_str).startswith(str(self))
class eOID(OID):
"""OID helper for TCA eUICC prefix"""
__prefix = [2,23,143,1]
def __init__(self, initializer):
if isinstance(initializer, str):
initializer = self.intlist_from_str(initializer)
super().__init__(self.__prefix + initializer)
MF = eOID("2.1")
DF_CD = eOID("2.2")
DF_TELECOM = eOID("2.3")
DF_TELECOM_v2 = eOID("2.3.2")
ADF_USIM_by_default = eOID("2.4")
ADF_USIM_by_default_v2 = eOID("2.4.2")
ADF_USIMopt_not_by_default = eOID("2.5")
ADF_USIMopt_not_by_default_v2 = eOID("2.5.2")
ADF_USIMopt_not_by_default_v3 = eOID("2.5.3")
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
ADF_ISIM_by_default = eOID("2.8")
ADF_ISIMopt_not_by_default = eOID("2.9")
ADF_ISIMopt_not_by_default_v2 = eOID("2.9.2")
ADF_CSIM_by_default = eOID("2.10")
ADF_CSIM_by_default_v2 = eOID("2.10.2")
ADF_CSIMopt_not_by_default = eOID("2.11")
ADF_CSIMopt_not_by_default_v2 = eOID("2.11.2")
DF_EAP = eOID("2.12")
DF_5GS = eOID("2.13")
DF_5GS_v2 = eOID("2.13.2")
DF_5GS_v3 = eOID("2.13.3")
DF_5GS_v4 = eOID("2.13.4")
DF_SAIP = eOID("2.14")
DF_SNPN = eOID("2.15")
DF_5GProSe = eOID("2.16")
IoT_by_default = eOID("2.17")
IoTopt_not_by_default = eOID("2.18")

View File

@@ -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 osmocom.tlv import camel_to_snake
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
from pySim.esim.saip import ProfileElement, ProfileElementSequence
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_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

View File

@@ -1,974 +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
from pySim.utils import all_subclasses, h2b
from pySim.filesystem import Path
import pySim.esim.saip.oid as OID
class FileTemplate:
"""Representation of a single file in a SimAlliance/TCA Profile Template. The argument order
is done to match that of the tables in Section 9 of the SAIP specification."""
def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
pe_name:Optional[str] = None, repeat:bool = False, ppath: List[int] = []):
"""
Args:
fid: The 16bit file-identifier of the file
name: The name of the file in human-readable "EF.FOO", "DF.BAR" notation
ftype: The type of the file; can be 'MF', 'ADF', 'DF', 'TR', 'LF', 'CY', 'BT'
nb_rec: Then number of records (only valid for 'LF' and 'CY')
size: The size of the file ('TR', 'BT'); size of each record ('LF, 'CY')
arr: The record number of EF.ARR for referenced access rules
sfi: The short file identifier, if any
default_val: The default value [pattern] of the file
content_rqd: Whether an instance of template *must* specify file contents
params: A list of parameters that an instance of the template *must* specify
ass_serv: The associated service[s] of the service table
high_update: Is this file of "high update frequency" type?
pe_name: The name of this file in the ASN.1 type of the PE. Auto-generated for most.
repeat: Whether the default_val pattern is a repeating pattern.
ppath: The intermediate path between the base_df of the ProfileTemplate and this file. If not
specified, the file will be created immediately underneath the base_df.
"""
# initialize from arguments
self.fid = fid
self.name = name
if pe_name:
self.pe_name = pe_name
else:
self.pe_name = self.name.replace('.','-').replace('_','-').lower()
self.file_type = ftype
if ftype in ['LF', 'CY']:
self.nb_rec = nb_rec
self.rec_len = size
elif ftype in ['TR', 'BT']:
self.file_size = size
self.arr = arr
self.sfi = sfi
self.default_val = default_val
self.default_val_repeat = repeat
self.content_rqd = content_rqd
self.params = params
self.ass_serv = ass_serv
self.high_update = high_update
self.ppath = ppath # parent path, if this FileTemplate is not immediately below the base_df
# initialize empty
self.parent = None
self.children = []
if self.default_val:
length = self._default_value_len() or 100
# run the method once to verify the pattern can be processed
self.expand_default_value_pattern(length)
def __str__(self) -> str:
return "FileTemplate(%s)" % (self.name)
def __repr__(self) -> str:
s_fid = "%04x" % self.fid if self.fid is not None else 'None'
s_arr = self.arr if self.arr is not None else 'None'
s_sfi = "%02x" % self.sfi if self.sfi is not None else 'None'
return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s, ppath=%s)" % (self.name, self.pe_name, s_fid, self.file_type, s_arr, s_sfi, self.ppath)
def print_tree(self, indent:str = ""):
"""recursive printing of FileTemplate tree structure."""
print("%s%s (%s)" % (indent, repr(self), self.path))
indent += " "
for c in self.children:
c.print_tree(indent)
@property
def path(self):
"""Return the path of the given File within the hierarchy."""
if self.parent:
return self.parent.path + self.name
else:
return Path(self.name)
def get_file_by_path(self, path: List[str]) -> Optional['FileTemplate']:
"""Return a FileTemplate matching the given path within this ProfileTemplate."""
if path[0].lower() != self.name.lower():
return None
for c in self.children:
if path[1].lower() == c.name.lower():
return c.get_file_by_path(path[1:])
def _default_value_len(self):
if self.file_type in ['TR']:
return self.file_size
elif self.file_type in ['LF', 'CY']:
return self.rec_len
def expand_default_value_pattern(self, length: Optional[int] = None) -> Optional[bytes]:
"""Expand the default value pattern to the specified length."""
if length is None:
length = self._default_value_len()
if length is None:
raise ValueError("%s does not have a default length" % self)
if not self.default_val:
return None
if not '...' in self.default_val:
return h2b(self.default_val)
l = self.default_val.split('...')
if len(l) != 2:
raise ValueError("Pattern '%s' contains more than one ..." % self.default_val)
prefix = h2b(l[0])
suffix = h2b(l[1])
pad_len = length - len(prefix) - len(suffix)
if pad_len <= 0:
ret = prefix + suffix
return ret[:length]
return prefix + prefix[-1:] * pad_len + suffix
class ProfileTemplate:
"""Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
consists of a number of file definitions. We implement each profile template as a class derived from this
base class. Each such derived class is a singleton and has no instances."""
created_by_default: bool = False
optional: bool = False
oid: Optional[OID.eOID] = None
files: List[FileTemplate] = []
# indicates that a given template does not have its own 'base DF', but that its contents merely
# extends that of the 'base DF' of another template
extends: Optional['ProfileTemplate'] = None
# indicates a parent ProfileTemplate below whose 'base DF' our files should be placed.
parent: Optional['ProfileTemplate'] = None
def __init_subclass__(cls, **kwargs):
"""This classmethod is called automatically after executing the subclass body. We use it to
initialize the cls.files_by_pename from the cls.files"""
super().__init_subclass__(**kwargs)
cur_df = None
cls.files_by_pename: dict[str,FileTemplate] = {}
cls.tree: List[FileTemplate] = []
if not cls.optional and not cls.files[0].file_type in ['MF', 'DF', 'ADF']:
raise ValueError('First file in non-optional template must be MF, DF or ADF (is: %s)' % cls.files[0])
for f in cls.files:
if f.file_type in ['MF', 'DF', 'ADF']:
if cur_df == None:
cls.tree.append(f)
f.parent = None
cur_df = f
else:
# "cd .."
if cur_df.parent:
cur_df = cur_df.parent
f.parent = cur_df
cur_df.children.append(f)
cur_df = f
else:
if cur_df == None:
cls.tree.append(f)
f.parent = None
else:
cur_df.children.append(f)
f.parent = cur_df
cls.files_by_pename[f.pe_name] = f
ProfileTemplateRegistry.add(cls)
@classmethod
def print_tree(cls):
for c in cls.tree:
c.print_tree()
@classmethod
def base_df(cls) -> FileTemplate:
"""Return the FileTemplate for the base DF of the given template. This may be a DF or ADF
within this template, or refer to another template (e.g. mandatory USIM if we are optional USIM."""
if cls.extends:
return cls.extends.base_df
return cls.files[0]
class ProfileTemplateRegistry:
"""A registry of profile templates. Exists as a singleton class with no instances and only
classmethods."""
by_oid = {}
@classmethod
def add(cls, tpl: ProfileTemplate):
"""Add a ProfileTemplate to the registry. There can only be one Template per OID."""
oid_str = str(tpl.oid)
if oid_str in cls.by_oid:
raise ValueError("We already have a template for OID %s" % oid_str)
cls.by_oid[oid_str] = tpl
@classmethod
def get_by_oid(cls, oid: Union[List[int], str]) -> Optional[ProfileTemplate]:
"""Look-up the ProfileTemplate based on its OID. The OID can be given either in dotted-string format,
or as a list of integers."""
if not isinstance(oid, str):
oid = OID.OID.str_from_intlist(oid)
return cls.by_oid.get(oid, None)
# below are transcribed template definitions from "ANNEX A (Normative): File Structure Templates Definition"
# of "Profile interoperability specification V3.3.1 Final" (unless other version explicitly specified).
# Section 9.2
class FilesAtMF(ProfileTemplate):
created_by_default = True
oid = OID.MF
files = [
FileTemplate(0x3f00, 'MF', 'MF', None, None, 14, None, None, None, params=['pinStatusTemplateDO']),
FileTemplate(0x2f05, 'EF.PL', 'TR', None, 2, 1, 0x05, 'FF...FF', None),
FileTemplate(0x2f02, 'EF.ICCID', 'TR', None, 10, 11, None, None, True),
FileTemplate(0x2f00, 'EF.DIR', 'LF', None, None, 10, 0x1e, None, True, params=['nb_rec', 'size']),
FileTemplate(0x2f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, params=['nb_rec', 'size']),
FileTemplate(0x2f08, 'EF.UMPC', 'TR', None, 5, 10, 0x08, None, False),
]
# Section 9.3
class FilesCD(ProfileTemplate):
created_by_default = False
oid = OID.DF_CD
files = [
FileTemplate(0x7f11, 'DF.CD', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f01, 'EF.LAUNCHPAD', 'TR', None, None, 2, None, None, True, params=['size']),
]
for i in range(0x40, 0x7f):
files.append(FileTemplate(0x6f00+i, 'EF.ICON', 'TR', None, None, 2, None, None, True, params=['size']))
# Section 9.4: Do this separately, so we can use them also from 9.5.3
df_pb_files = [
FileTemplate(0x5f3a, 'DF.PHONEBOOK', 'DF', None, None, 14, None, None, True, ['pinStatusTemplateDO']),
FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ppath=[0x5f3a]),
]
for i in range(0x38, 0x40):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi'], ppath=[0x5f3a]))
for i in range(0x40, 0x48):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
for i in range(0x48, 0x50):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
df_pb_files += [
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi'], ppath=[0x5f3a]),
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
]
for i in range(0x50, 0x58):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x58, 0x60):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x60, 0x68):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
for i in range(0x68, 0x70):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x70, 0x78):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x78, 0x80):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x80, 0x88):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x88, 0x90):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
for i in range(0x90, 0x98):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
for i in range(0x98, 0xa0):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
# Section 9.4 v2.3.1
class FilesTelecom(ProfileTemplate):
created_by_default = False
oid = OID.DF_TELECOM
base_path = Path('MF')
files = [
FileTemplate(0x7f10, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size'], ppath=[0x5f50]),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'], ppath=[0x5f50]))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
]
# Section 9.4
class FilesTelecomV2(ProfileTemplate):
created_by_default = False
oid = OID.DF_TELECOM_v2
base_path = Path('MF')
files = [
FileTemplate(0x7f10, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size'], ppath=[0x5f50]),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'],ppath=[0x5f50]))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]),
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]),
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # VST: 2
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # VST: 3
]
# Section 9.5.1 v2.3.1
class FilesUsimMandatory(ProfileTemplate):
created_by_default = True
oid = OID.ADF_USIM_by_default
files = [
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name = 'ef-keysPS'),
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 14, 2, 0x04, None, True),
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
]
# Section 9.5.1
class FilesUsimMandatoryV2(ProfileTemplate):
created_by_default = True
oid = OID.ADF_USIM_by_default_v2
files = [
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name='ef-keysPS'),
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 17, 2, 0x04, None, True),
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
]
# Section 9.5.2 v2.3.1
class FilesUsimOptional(ProfileTemplate):
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default
base_path = Path('ADF.USIM')
extends = FilesUsimMandatory
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000', False, ass_serv=[20], repeat=True),
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000', False, ass_serv=[42], repeat=True),
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43], repeat=True),
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
]
# Section 9.5.2
class FilesUsimOptionalV2(ProfileTemplate):
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default_v2
base_path = Path('ADF.USIM')
extends = FilesUsimMandatoryV2
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
FileTemplate(0x6ff3, 'EF.EPDGID', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
FileTemplate(0x6ff4, 'EF.EPDGSELECTION','TR',None,None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
FileTemplate(0x6ff5, 'EF.EPDGIDEM', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
FileTemplate(0x6ff6, 'EF.EPDGIDEMSEL','TR',None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
FileTemplate(0x6ff7, 'EF.FromPreferred','TR',None, 1, 2, None, '00', False, ass_serv=[114]),
FileTemplate(0x6ff8, 'EF.IMSConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[115]),
FileTemplate(0x6ff9, 'EF.3GPPPSDataOff','TR',None, 4, 2, None, None, True, ass_serv=[117]),
FileTemplate(0x6ffa, 'EF.3GPPPSDOSLIST','LF',None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[118]),
FileTemplate(0x6ffc, 'EF.XCAPConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[120]),
FileTemplate(0x6ffd, 'EF.EARFCNLIST','TR', None, None, 10, None, None, True, ['size'], ass_serv=[121]),
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
]
# Section 9.5.2.3 v3.3.1
class FilesUsimOptionalV3(ProfileTemplate):
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default_v3
base_path = Path('ADF.USIM')
extends = FilesUsimMandatoryV2
files = FilesUsimOptionalV2.files + [
FileTemplate(0x6f01, 'EF.eAKA', 'TR', None, 1, 3, None, None, True, ['size'], ass_serv=[134]),
]
# Section 9.5.3
class FilesUsimDfPhonebook(ProfileTemplate):
created_by_default = False
oid = OID.DF_PHONEBOOK_ADF_USIM
base_path = Path('ADF.USIM')
files = df_pb_files
# Section 9.5.4
class FilesUsimDfGsmAccess(ProfileTemplate):
created_by_default = False
oid = OID.DF_GSM_ACCESS_ADF_USIM
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x5f3b, 'DF.GSM-ACCESS','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[27]),
FileTemplate(0x4f20, 'EF.Kc', 'TR', None, 9, 5, 0x01, 'FF...FF07', False, ass_serv=[27], high_update=True),
FileTemplate(0x4f52, 'EF.KcGPRS', 'TR', None, 9, 5, 0x02, 'FF...FF07', False, ass_serv=[27], high_update=True),
FileTemplate(0x4f63, 'EF.CPBCCH', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[39], high_update=True),
FileTemplate(0x4f64, 'EF.InvScan', 'TR', None, 1, 2, None, '00', False, ass_serv=[40]),
]
# Section 9.5.11 v2.3.1
class FilesUsimDf5GS(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
]
# Section 9.5.11.2
class FilesUsimDf5GSv2(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS_v2
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
]
# Section 9.5.11.3
class FilesUsimDf5GSv3(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS_v3
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 2, 62, 5, 0x03, 'FF...FF', False, ass_serv=[122,136], high_update=True),
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 2, 62, 5, 0x04, 'FF...FF', False, ass_serv=[122,136], high_update=True),
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130], pe_name='ef-supinai'),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
]
# Section 9.5.11.4
class FilesUsimDf5GSv4(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS_v4
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 2, 62, 5, 0x03, 'FF...FF', False, ass_serv=[122,136], high_update=True),
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 2, 62, 5, 0x04, 'FF...FF', False, ass_serv=[122,136], high_update=True),
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130], pe_name='ef-supinai'),
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FF0000', False, ass_serv=[124]),
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
FileTemplate(0x4f0d, 'EF.CAG', 'TR', None, 2, 2, 0x0d, None, True, ass_serv=[137]),
FileTemplate(0x4f0e, 'EF.SOR_CMCI', 'TR', None, None, 2, 0x0e, None, True, ass_serv=[138]),
FileTemplate(0x4f0f, 'EF.DRI', 'TR', None, 7, 2, 0x0f, None, True, ass_serv=[150]),
FileTemplate(0x4f10, 'EF.5GSEDRX', 'TR', None, 2, 2, 0x10, None, True, ass_serv=[141]),
FileTemplate(0x4f11, 'EF.5GNSWO_CONF', 'TR', None, 1, 2, 0x11, None, True, ass_serv=[142]),
FileTemplate(0x4f15, 'EF.MCHPPLMN', 'TR', None, 1, 2, 0x15, None, True, ass_serv=[144]),
FileTemplate(0x4f16, 'EF.KAUSF_DERIVATION', 'TR', None, 1, 2, 0x16, None, True, ass_serv=[145]),
]
# Section 9.5.12
class FilesUsimDfSaip(ProfileTemplate):
created_by_default = False
oid = OID.DF_SAIP
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x6fd0, 'DF.SAIP', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[(124, 125)], pe_name='df-df-saip'),
FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF...FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
]
# Section 9.5.13
class FilesDfSnpn(ProfileTemplate):
created_by_default = False
oid = OID.DF_SNPN
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x5fe0, 'DF.SNPN', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[143], pe_name='df-df-snpn'),
FileTemplate(0x4f01, 'EF.PWS_SNPN', 'TR', None, 1, 10, None, None, True, ass_serv=[143]),
]
# Section 9.5.14
class FilesDf5GProSe(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GProSe
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x5ff0, 'DF.5G_ProSe', 'DF', None, None, 14, None, None, False, ['pinStatusTeimplateDO'], ass_serv=[139], pe_name='df-df-5g-prose'),
FileTemplate(0x4f01, 'EF.5G_PROSE_ST', 'TR', None, 1, 2, 0x01, None, True, ass_serv=[139]),
FileTemplate(0x4f02, 'EF.5G_PROSE_DD', 'TR', None, 26, 2, 0x02, None, True, ass_serv=[139,1001]),
FileTemplate(0x4f03, 'EF.5G_PROSE_DC', 'TR', None, 12, 2, 0x03, None, True, ass_serv=[139,1002]),
FileTemplate(0x4f04, 'EF.5G_PROSE_U2NRU', 'TR', None, 32, 2, 0x04, None, True, ass_serv=[139,1003]),
FileTemplate(0x4f05, 'EF.5G_PROSE_RU', 'TR', None, 29, 2, 0x05, None, True, ass_serv=[139,1004]),
FileTemplate(0x4f06, 'EF.5G_PROSE_UIR', 'TR', None, 32, 2, 0x06, None, True, ass_serv=[139,1005]),
]
# Section 9.6.1
class FilesIsimMandatory(ProfileTemplate):
created_by_default = True
oid = OID.ADF_ISIM_by_default
files = [
FileTemplate( None, 'ADF.ISIM', 'ADF', None, None, 14, None, None, False, ['aid','temporary_fid','pinStatusTemplateDO']),
FileTemplate(0x6f02, 'EF.IMPI', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x6f04, 'EF.IMPU', 'LF', 1, None, 2, 0x04, None, True, ['size']),
FileTemplate(0x6f03, 'EF.Domain', 'TR', None, None, 2, 0x05, None, True, ['size']),
FileTemplate(0x6f07, 'EF.IST', 'TR', None, 14, 2, 0x07, None, True),
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 3, 10, 0x03, '000000', False),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x06, None, True, ['nb_rec','size']),
]
# Section 9.6.2 v2.3.1
class FilesIsimOptional(ProfileTemplate):
created_by_default = False
optional = True
oid = OID.ADF_ISIMopt_not_by_default
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
files = [
FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
]
# Section 9.6.2
class FilesIsimOptionalv2(ProfileTemplate):
created_by_default = False
optional = True
oid = OID.ADF_ISIMopt_not_by_default_v2
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
files = [
FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
FileTemplate(0x6ff7, 'EF.FromPreferred','TR', None, 1, 2, None, '00', False, ass_serv=[17]),
FileTemplate(0x6ff8, 'EF.ImsConfigData','BT', None,None, 2, None, None, True, ['size'], ass_serv=[18]),
FileTemplate(0x6ffc, 'EF.XcapconfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[19]),
FileTemplate(0x6ffa, 'EF.WebRTCURI', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ass_serv=[20]),
FileTemplate(0x6ffa, 'EF.MudMidCfgData','BT',None, None, 2, None, None, True, ['size'], ass_serv=[21]),
]
# TODO: CSIM
# Section 9.8
class FilesEap(ProfileTemplate):
created_by_default = False
oid = OID.DF_EAP
files = [
FileTemplate( None, 'DF.EAP', 'DF', None, None, 14, None, None, False, ['fid','pinStatusTemplateDO'], ass_serv=[(124, 125)]),
FileTemplate(0x4f01, 'EF.EAPKEYS', 'TR', None, None, 2, None, None, True, ['size'], high_update=True),
FileTemplate(0x4f02, 'EF.EAPSTATUS', 'TR', None, 1, 2, None, '00', False, high_update=True),
FileTemplate(0x4f03, 'EF.PUId', 'TR', None, None, 2, None, None, True, ['size']),
FileTemplate(0x4f04, 'EF.Ps', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], high_update=True),
FileTemplate(0x4f20, 'EF.CurID', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], high_update=True),
FileTemplate(0x4f21, 'EF.RelID', 'TR', None, None, 5, None, None, True, ['size']),
FileTemplate(0x4f22, 'EF.Realm', 'TR', None, None, 5, None, None, True, ['size']),
]
# Section 9.9 Access Rules Definition
ARR_DEFINITION = {
1: ['8001019000', '800102A406830101950108', '800158A40683010A950108'],
2: ['800101A406830101950108', '80015AA40683010A950108'],
3: ['80015BA40683010A950108'],
4: ['8001019000', '80011A9700', '800140A40683010A950108'],
5: ['800103A406830101950108', '800158A40683010A950108'],
6: ['800111A406830101950108', '80014AA40683010A950108'],
7: ['800103A406830101950108', '800158A40683010A950108', '840132A406830101950108'],
8: ['800101A406830101950108', '800102A406830181950108', '800158A40683010A950108'],
9: ['8001019000', '80011AA406830101950108', '800140A40683010A950108'],
10: ['8001019000', '80015AA40683010A950108'],
11: ['8001019000', '800118A40683010A950108', '8001429700'],
12: ['800101A406830101950108', '80015A9700'],
13: ['800113A406830101950108', '800148A40683010A950108'],
14: ['80015EA40683010A950108'],
}
class SaipSpecVersionMeta(type):
def __getitem__(self, ver: str):
"""Syntactic sugar so that SaipSpecVersion['2.3.0'] will work."""
return SaipSpecVersion.for_version(ver)
class SaipSpecVersion(object, metaclass=SaipSpecVersionMeta):
"""Represents a specific version of the SIMalliance / TCA eUICC Profile Package:
Interoperable Format Technical Specification."""
version = None
oids = []
@classmethod
def suports_template_OID(cls, OID: OID.OID) -> bool:
"""Return if a given spec version supports a template of given OID."""
return OID in cls.oids
@classmethod
def version_match(cls, ver: str) -> bool:
"""Check if the given version-string matches the classes version. trailing zeroes are ignored,
so that for example 2.2.0 will be considered equal to 2.2"""
def strip_trailing_zeroes(l: List):
while l[-1] == '0':
l.pop()
cls_ver_l = cls.version.split('.')
strip_trailing_zeroes(cls_ver_l)
ver_l = ver.split('.')
strip_trailing_zeroes(ver_l)
return cls_ver_l == ver_l
@staticmethod
def for_version(req_version: str) -> Optional['SaipSpecVersion']:
"""Return the subclass for the requested version number string."""
for cls in all_subclasses(SaipSpecVersion):
if cls.version_match(req_version):
return cls
class SaipSpecVersion101(SaipSpecVersion):
version = '1.0.1'
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM, OID.ADF_USIM_by_default, OID.ADF_USIMopt_not_by_default,
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.ADF_ISIM_by_default,
OID.ADF_ISIMopt_not_by_default, OID.ADF_CSIM_by_default, OID.ADF_CSIMopt_not_by_default]
class SaipSpecVersion20(SaipSpecVersion):
version = '2.0'
# no changes in filesystem teplates to previous 1.0.1
oids = SaipSpecVersion101.oids
class SaipSpecVersion21(SaipSpecVersion):
version = '2.1'
# no changes in filesystem teplates to previous 2.0
oids = SaipSpecVersion20.oids
class SaipSpecVersion22(SaipSpecVersion):
version = '2.2'
oids = SaipSpecVersion21.oids + [OID.DF_EAP]
class SaipSpecVersion23(SaipSpecVersion):
version = '2.3'
oids = SaipSpecVersion22.oids + [OID.DF_5GS, OID.DF_SAIP]
class SaipSpecVersion231(SaipSpecVersion):
version = '2.3.1'
# no changes in filesystem teplates to previous 2.3
oids = SaipSpecVersion23.oids
class SaipSpecVersion31(SaipSpecVersion):
version = '3.1'
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM_v2, OID.ADF_USIM_by_default_v2, OID.ADF_USIMopt_not_by_default_v2,
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.DF_5GS_v2, OID.DF_5GS_v3, OID.DF_SAIP,
OID.ADF_ISIM_by_default, OID.ADF_ISIMopt_not_by_default_v2, OID.ADF_CSIM_by_default_v2,
OID.ADF_CSIMopt_not_by_default_v2, OID.DF_EAP]
class SaipSpecVersion32(SaipSpecVersion):
version = '3.2'
# no changes in filesystem teplates to previous 3.1
oids = SaipSpecVersion31.oids
class SaipSpecVersion331(SaipSpecVersion):
version = '3.3.1'
oids = SaipSpecVersion32.oids + [OID.ADF_USIMopt_not_by_default_v3, OID.DF_5GS_v4, OID.DF_SAIP, OID.DF_SNPN, OID.DF_5GProSe, OID.IoT_by_default, OID.IoTopt_not_by_default]

View File

@@ -92,43 +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')
def check_identification_unique(self, pes: ProfileElementSequence):
"""Ensure that each PE has a unique identification value."""
id_list = [pe.header['identification'] for pe in pes.pe_list if pe.header]
if len(id_list) != len(set(id_list)):
raise ProfileError('PE identification values are not unique')
FileChoiceList = List[Tuple]
class FileError(ProfileError):
pass
class FileConstraintChecker:
def check(self, l: FileChoiceList):
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(l)
class FileCheckBasicStructure(FileConstraintChecker):
def check_seqence(self, l: FileChoiceList):
by_type = {}
for k, v in l:
if k in by_type:
by_type[k].append(v)
else:
by_type[k] = [v]
if 'doNotCreate' in by_type:
if len(l) != 1:
raise FileError("doNotCreate must be the only element")
if 'fileDescriptor' in by_type:
if len(by_type['fileDescriptor']) != 1:
raise FileError("fileDescriptor must be the only element")
if l[0][0] != 'fileDescriptor':
raise FileError("fileDescriptor must be the first element")
def check_forbidden(self, l: FileChoiceList):
"""Perform checks for forbidden parameters as described in Section 8.3.3."""

View File

@@ -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

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""
Various definitions related to GSMA consumer + IoT eSIM / eUICC
Various definitions related to GSMA eSIM / eUICC
Does *not* implement anything related to M2M eUICC
Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
Related Specs: GSMA SGP.22, GSMA SGP.02, etc.
"""
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
@@ -23,56 +21,17 @@ Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pySim.tlv import *
from pySim.construct import *
from construct import Optional as COptional
from construct import *
import argparse
from construct import Array, Struct, FlagsEnum, GreedyRange
from cmd2 import cmd2, CommandSet, with_default_category
from osmocom.utils import Hexstr
from osmocom.tlv import *
from osmocom.construct import *
from pySim.exceptions import SwMatchError
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
from pySim.commands import SimCardCommands
from pySim.filesystem import CardADF, CardApplication
from pySim.utils import Hexstr, SwHexstr
import pySim.global_platform
# SGP.02 Section 2.2.2
class Sgp02Eid(BER_TLV_IE, tag=0x5a):
_construct = BcdAdapter(GreedyBytes)
# patch this into global_platform, to allow 'get_data sgp02_eid' in EF.ECASD
pySim.global_platform.DataCollection.possible_nested.append(Sgp02Eid)
def compute_eid_checksum(eid) -> str:
"""Compute and add/replace check digits of an EID value according to GSMA SGP.29 Section 10."""
if isinstance(eid, str):
if len(eid) == 30:
# first pad by 2 digits
eid += "00"
elif len(eid) == 32:
# zero the last two digits
eid = eid[:-2] + "00"
else:
raise ValueError("and EID must be 30 or 32 digits")
eid_int = int(eid)
elif isinstance(eid, int):
eid_int = eid
if eid_int % 100:
# zero the last two digits
eid_int -= eid_int % 100
# Using the resulting 32 digits as a decimal integer, compute the remainder of that number on division by
# 97, Subtract the remainder from 98, and use the decimal result for the two check digits, if the result
# is one digit long, its value SHALL be prefixed by one digit of 0.
csum = 98 - (eid_int % 97)
eid_int += csum
return str(eid_int)
def verify_eid_checksum(eid) -> bool:
"""Verify the check digits of an EID value according to GSMA SGP.29 Section 10."""
# Using the 32 digits as a decimal integer, compute the remainder of that number on division by 97. If the
# remainder of the division is 1, the verification is successful; otherwise the EID is invalid.
return int(eid) % 97 == 1
class VersionAdapter(Adapter):
"""convert an EUICC Version (3-int array) to a textual representation."""
@@ -90,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
@@ -176,7 +144,7 @@ class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
pass
class SeqNumber(BER_TLV_IE, tag=0x80):
_construct = Asn1DerInteger()
_construct = GreedyInteger()
class NotificationAddress(BER_TLV_IE, tag=0x0c):
_construct = Utf8Adapter(GreedyBytes)
class Iccid(BER_TLV_IE, tag=0x5a):
@@ -290,7 +258,7 @@ class EumCertificate(BER_TLV_IE, tag=0xa5):
_construct = GreedyBytes
class EuiccCertificate(BER_TLV_IE, tag=0xa6):
_construct = GreedyBytes
class GetCertsError(BER_TLV_IE, tag=0x81):
class GetCertsError(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
pass
@@ -303,9 +271,9 @@ class EimFqdn(BER_TLV_IE, tag=0x81):
class EimIdType(BER_TLV_IE, tag=0x82):
_construct = Enum(Int8ub, eimIdTypeOid=1, eimIdTypeFqdn=2, eimIdTypeProprietary=3)
class CounterValue(BER_TLV_IE, tag=0x83):
_construct = Asn1DerInteger()
_construct = GreedyInteger
class AssociationToken(BER_TLV_IE, tag=0x84):
_construct = Asn1DerInteger()
_construct = GreedyInteger
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
eimProprietary=4)
@@ -318,24 +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()]
# we attempt to retrieve ISD-R key material from CardKeyProvider identified by EID
self.adf.scp_key_identity = 'EID'
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 = '80E29100%02x%s00' % (len(tx_do)//2, tx_do)
return scc.send_apdu_checksw(capdu, exp_sw)
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
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:
@@ -345,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()
@@ -356,13 +321,6 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
else:
return None
@staticmethod
def get_eid(scc: SimCardCommands) -> str:
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
ged = CardApplicationISDR.store_data_tlv(scc, ged_cmd, GetEuiccData)
d = ged.to_dict()
return flatten_dict_lists(d['get_euicc_data'])['eid_value']
def decode_select_response(self, data_hex: Hexstr) -> object:
t = FciTemplate()
t.from_tlv(h2b(data_hex))
@@ -378,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']))
@@ -393,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']))
@@ -428,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']))
@@ -449,14 +407,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c EnableProfile function."""
if opts.isdp_aid:
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
elif opts.iccid:
if opts.iccid:
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
else:
# this is guaranteed by argparse; but we need this to make pylint happy
raise ValueError('Either ISD-P AID or ICCID must be given')
ep_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
ep = 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']))
@@ -471,14 +426,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c DisableProfile function."""
if opts.isdp_aid:
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
elif opts.iccid:
if opts.iccid:
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
else:
# this is guaranteed by argparse; but we need this to make pylint happy
raise ValueError('Either ISD-P AID or ICCID must be given')
dp_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
dp = 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']))
@@ -492,28 +444,26 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c DeleteProfile function."""
if opts.isdp_aid:
p_id = IsdpAid(decoded=opts.isdp_aid)
elif opts.iccid:
if opts.iccid:
p_id = Iccid(decoded=opts.iccid)
else:
# this is guaranteed by argparse; but we need this to make pylint happy
raise ValueError('Either ISD-P AID or ICCID must be given')
dp_cmd_contents = [p_id]
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
dp = 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) = 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']))
set_nickname_parser = argparse.ArgumentParser()
set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
set_nickname_parser.add_argument('ICCID', help='ICCID of the profile whose nickname to set')
set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
@cmd2.with_argparser(set_nickname_parser)
def do_set_nickname(self, opts):
@@ -521,38 +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_certs_resp']))
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()]
# we attempt to retrieve ECASD key material from CardKeyProvider identified by EID
self.adf.scp_key_identity = 'EID'
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
pass
class CardApplicationISDR(CardApplication):
def __init__(self):
super().__init__('ISD-R', adf=ADF_ISDR(), sw=sw_isdr)
class CardApplicationECASD(CardApplication):
def __init__(self):
super().__init__('ECASD', adf=ADF_ECASD(), sw=sw_isdr)

View File

@@ -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):
@@ -49,18 +52,9 @@ class SwMatchError(Exception):
self.sw_expected = sw_expected
self.rs = rs
@property
def description(self):
def __str__(self):
if self.rs and self.rs.lchan[0]:
r = self.rs.lchan[0].interpret_sw(self.sw_actual)
if r:
return "%s - %s" % (r[0], r[1])
return ''
def __str__(self):
description = self.description
if description:
description = ": " + description
else:
description = "."
return "SW match failed! Expected %s and got %s%s" % (self.sw_expected, self.sw_actual, description)
return "SW match failed! Expected %s and got %s: %s - %s" % (self.sw_expected, self.sw_actual, r[0], r[1])
return "SW match failed! Expected %s and got %s." % (self.sw_expected, self.sw_actual)

View File

@@ -9,7 +9,7 @@ The classes are intended to represent the *specification* of the filesystem,
not the actual contents / runtime state of interacting with a given smart card.
"""
# (C) 2021-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
@@ -24,25 +24,25 @@ not the actual contents / runtime state of interacting with a given smart card.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 osmocom.utils import h2b, b2h, is_hex, auto_int, auto_uint8, auto_uint16, is_hexstr, JsonEncoder
from osmocom.tlv import bertlv_parse_one
from osmocom.construct import filter_dict, parse_construct, build_construct
from pySim.utils import sw_match
from pySim.jsonpath import js_path_modify
from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, Hexstr
from pySim.construct import filter_dict, parse_construct, build_construct
from pySim.exceptions import *
from pySim.jsonpath import js_path_find, js_path_modify
from pySim.commands import SimCardCommands
from pySim.exceptions import SwMatchError
# int: a single service is associated with this file
# list: any of the listed services requires this file
@@ -65,13 +65,13 @@ class CardFile:
Args:
fid : File Identifier (4 hex digits)
sfid : Short File Identifier (2 hex digits, optional)
name : Brief name of the file, like EF_ICCID
name : Brief name of the file, lik EF_ICCID
desc : Description of the file
parent : Parent CardFile object within filesystem hierarchy
profile : Card profile that this file should be part of
service : Service (SST/UST/IST) associated with the file
"""
if not isinstance(self, CardADF) and fid is None:
if not isinstance(self, CardADF) and fid == None:
raise ValueError("fid is mandatory")
if fid:
fid = fid.lower()
@@ -136,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,
@@ -147,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]
@@ -167,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
@@ -179,7 +179,7 @@ class CardFile:
"""Return a dict of {'identifier': self} tuples.
Args:
alias : Add an alias with given name to 'self'
alias : Add an alias with given name to 'self'
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
@@ -282,33 +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")
@staticmethod
def export(as_json: bool, lchan):
"""
Export file contents in the form of commandline script. This method is meant to be overloaded by a subclass in
case any exportable contents are present. The generated script may contain multiple command lines separated by
line breaks ("\n"), where the last commandline shall have no line break at the end
(e.g. "update_record 1 112233\nupdate_record 1 445566"). Naturally this export method will always refer to the
currently selected file of the presented lchan.
"""
return "# %s has no exportable contents" % str(lchan.selected_file)
else:
raise ValueError("self.service must be either int or list or tuple")
class CardDF(CardFile):
@@ -316,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 = {}
@@ -424,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:
@@ -433,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)):
@@ -458,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)
@@ -527,7 +518,7 @@ class CardADF(CardDF):
super().__init__(**kwargs)
# reference to CardApplication may be set from CardApplication constructor
self.application = None # type: Optional[CardApplication]
self.aid = aid.lower() # Application Identifier
self.aid = aid # Application Identifier
self.has_fs = has_fs # Flag to tell whether the ADF supports a filesystem or not
mf = self.get_mf()
if mf:
@@ -542,15 +533,6 @@ class CardADF(CardDF):
else:
return self.aid
@staticmethod
def export(as_json: bool, lchan):
"""
Export application specific parameters that are not part of the UICC filesystem.
"""
if not isinstance(lchan.selected_file, CardADF):
raise TypeError('currently selected file is not of type CardADF')
return lchan.selected_file.application.export(as_json, lchan)
class CardEF(CardFile):
"""EF (Entry File) in the smart card filesystem"""
@@ -591,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):
@@ -604,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()
@@ -621,42 +606,44 @@ 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('--json-path', type=str,
help='JSON path to modify specific element of file only')
upd_bin_dec_parser.add_argument('DATA', help='Abstract data (JSON format) to write')
@cmd2.with_argparser(upd_bin_dec_parser)
def do_update_binary_decoded(self, opts):
"""Encode + Update (Write) data of a transparent EF"""
if opts.json_path:
(data_json, _sw) = self._cmd.lchan.read_binary_dec()
(data_json, sw) = self._cmd.lchan.read_binary_dec()
js_path_modify(data_json, opts.json_path,
json.loads(opts.DATA))
json.loads(opts.data))
else:
data_json = json.loads(opts.DATA)
(data, _sw) = self._cmd.lchan.update_binary_dec(data_json)
data_json = json.loads(opts.data)
(data, sw) = self._cmd.lchan.update_binary_dec(data_json)
if data:
self._cmd.poutput_json(data)
def do_edit_binary_decoded(self, _opts):
def do_edit_binary_decoded(self, opts):
"""Edit the JSON representation of the EF contents in an editor."""
(orig_json, _sw) = self._cmd.lchan.read_binary_dec()
(orig_json, sw) = self._cmd.lchan.read_binary_dec()
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -669,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)
@@ -710,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()
@@ -737,32 +724,13 @@ 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()
return {'raw': raw_bin_data.hex()}
def __get_size(self, total_len: Optional[int] = None) -> Optional[int]:
"""Get the size (total length) of the file"""
# Caller has provided the actual total length of the file, this should be the default case
if total_len is not None:
return total_len
if self.size is None:
return None
# Alternatively use the recommended size from the specification
if self.size[1] is not None:
return self.size[1]
# In case no recommended size is specified, use the minimum size
if self.size[0] is not None:
return self.size[0]
return None
def encode_bin(self, abstract_data: dict, total_len: Optional[int] = None) -> bytearray:
def encode_bin(self, abstract_data: dict) -> bytearray:
"""Encode abstract representation into raw (binary) data.
A derived class would typically provide an _encode_bin() or _encode_hex() method
@@ -771,26 +739,25 @@ class TransparentEF(CardEF):
Args:
abstract_data : dict representing the decoded data
total_len : expected total length of the encoded data (file size)
Returns:
binary encoded data
"""
method = getattr(self, '_encode_bin', None)
if callable(method):
return method(abstract_data, total_len = self.__get_size(total_len))
return method(abstract_data)
method = getattr(self, '_encode_hex', None)
if callable(method):
return h2b(method(abstract_data, total_len = self.__get_size(total_len)))
return h2b(method(abstract_data))
if self._construct:
return build_construct(self._construct, abstract_data, {'total_len' : self.__get_size(total_len)})
if self._tlv:
return build_construct(self._construct, abstract_data)
elif self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return t.to_tlv()
raise NotImplementedError(
"%s encoder not yet implemented. Patches welcome." % self)
def encode_hex(self, abstract_data: dict, total_len: Optional[int] = None) -> str:
def encode_hex(self, abstract_data: dict) -> str:
"""Encode abstract representation into raw (hex string) data.
A derived class would typically provide an _encode_bin() or _encode_hex() method
@@ -799,45 +766,25 @@ class TransparentEF(CardEF):
Args:
abstract_data : dict representing the decoded data
total_len : expected total length of the encoded data (file size)
Returns:
hex string encoded data
"""
method = getattr(self, '_encode_hex', None)
if callable(method):
return method(abstract_data, total_len = self.__get_size(total_len))
return method(abstract_data)
method = getattr(self, '_encode_bin', None)
if callable(method):
raw_bin_data = method(abstract_data, total_len = self.__get_size(total_len))
raw_bin_data = method(abstract_data)
return b2h(raw_bin_data)
if self._construct:
return b2h(build_construct(self._construct, abstract_data, {'total_len':self.__get_size(total_len)}))
if self._tlv:
return b2h(build_construct(self._construct, abstract_data))
elif self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return b2h(t.to_tlv())
raise NotImplementedError(
"%s encoder not yet implemented. Patches welcome." % self)
@staticmethod
def export(as_json: bool, lchan):
"""
Export the file contents of a TransparentEF. This method returns a shell command string (See also ShellCommand
definition in this class) that can be used to write the file contents back.
"""
if lchan.selected_file_structure() != 'transparent':
raise ValueError("selected file has structure type '%s', expecting a file with structure 'transparent'" %
lchan.selected_file_structure())
export_str = ""
if as_json:
result = lchan.read_binary_dec()
export_str += ("update_binary_decoded '%s'\n" % json.dumps(result[0], cls=JsonEncoder))
else:
result = lchan.read_binary()
export_str += ("update_binary %s\n" % str(result[0]))
return export_str.strip()
class LinFixedEF(CardEF):
"""Linear Fixed EF (Entry File) in the smart card filesystem.
@@ -848,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):
@@ -861,43 +812,43 @@ class LinFixedEF(CardEF):
read_rec_parser = argparse.ArgumentParser()
read_rec_parser.add_argument(
'--count', type=auto_uint8, default=1, help='Number of records to be read, beginning at record_nr')
'record_nr', type=int, help='Number of record to be read')
read_rec_parser.add_argument(
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
'--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:
recnr = opts.record_nr + r
(data, sw) = self._cmd.lchan.read_record(recnr)
if (len(data) > 0):
recstr = str(data)
else:
recstr = "(empty)"
self._cmd.poutput("%03d %s" % (recnr, recstr))
read_rec_dec_parser = argparse.ArgumentParser()
read_rec_dec_parser.add_argument(
'record_nr', type=int, help='Number of record to be read')
read_rec_dec_parser.add_argument('--oneline', action='store_true',
help='No JSON pretty-printing, dump as a single line')
read_rec_dec_parser.add_argument(
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
@cmd2.with_argparser(read_rec_dec_parser)
def do_read_record_decoded(self, opts):
"""Read + decode a record from a record-oriented EF"""
(data, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
(data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
self._cmd.poutput_json(data, opts.oneline)
read_recs_parser = argparse.ArgumentParser()
@cmd2.with_argparser(read_recs_parser)
def do_read_records(self, _opts):
def do_read_records(self, opts):
"""Read all records from a record-oriented EF"""
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
for recnr in range(1, 1 + num_of_rec):
(data, _sw) = self._cmd.lchan.read_record(recnr)
if len(data) > 0:
(data, sw) = self._cmd.lchan.read_record(recnr)
if (len(data) > 0):
recstr = str(data)
else:
recstr = "(empty)"
@@ -914,51 +865,53 @@ 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=int, help='Number of record to be read')
upd_rec_dec_parser.add_argument(
'data', help='Abstract data (JSON format) to write')
upd_rec_dec_parser.add_argument('--json-path', type=str,
help='JSON path to modify specific element of record only')
upd_rec_dec_parser.add_argument(
'RECORD_NR', type=auto_uint8, help='Number of record to be read')
upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
@cmd2.with_argparser(upd_rec_dec_parser)
def do_update_record_decoded(self, opts):
"""Encode + Update (write) data to a record-oriented EF"""
if opts.json_path:
(data_json, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
(data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
(data, _sw) = self._cmd.lchan.update_record_dec(
opts.RECORD_NR, data_json)
(data, sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, data_json)
if data:
self._cmd.poutput(data)
edit_rec_dec_parser = argparse.ArgumentParser()
edit_rec_dec_parser.add_argument(
'RECORD_NR', type=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
@@ -971,8 +924,8 @@ class LinFixedEF(CardEF):
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
(data, _sw) = self._cmd.lchan.update_record_dec(
opts.RECORD_NR, edited_json)
(data, sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, edited_json)
if data:
self._cmd.poutput_json(data)
@@ -1017,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()
@@ -1045,32 +998,13 @@ 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()
return {'raw': raw_hex_data}
def __get_rec_len(self, total_len: Optional[int] = None) -> Optional[int]:
"""Get the length (total length) of the file record"""
# Caller has provided the actual total length of the record, this should be the default case
if total_len is not None:
return total_len
if self.rec_len is None:
return None
# Alternatively use the recommended length from the specification
if self.rec_len[1] is not None:
return self.rec_len[1]
# In case no recommended length is specified, use the minimum length
if self.rec_len[0] is not None:
return self.rec_len[0]
return None
def encode_record_hex(self, abstract_data: dict, record_nr: int, total_len: Optional[int] = None) -> str:
def encode_record_hex(self, abstract_data: dict, record_nr: int) -> str:
"""Encode abstract representation into raw (hex string) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
@@ -1080,27 +1014,26 @@ class LinFixedEF(CardEF):
Args:
abstract_data : dict representing the decoded data
record_nr : record number (1 for first record, ...)
total_len : expected total length of the encoded data (record length)
Returns:
hex string encoded data
"""
method = getattr(self, '_encode_record_hex', None)
if callable(method):
return method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len))
return method(abstract_data, record_nr=record_nr)
method = getattr(self, '_encode_record_bin', None)
if callable(method):
raw_bin_data = method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len))
raw_bin_data = method(abstract_data, record_nr=record_nr)
return b2h(raw_bin_data)
if self._construct:
return b2h(build_construct(self._construct, abstract_data, {'total_len':self.__get_rec_len(total_len)}))
if self._tlv:
return b2h(build_construct(self._construct, abstract_data))
elif self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return b2h(t.to_tlv())
raise NotImplementedError(
"%s encoder not yet implemented. Patches welcome." % self)
def encode_record_bin(self, abstract_data: dict, record_nr : int, total_len: Optional[int] = None) -> bytearray:
def encode_record_bin(self, abstract_data: dict, record_nr : int) -> bytearray:
"""Encode abstract representation into raw (binary) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
@@ -1110,73 +1043,24 @@ class LinFixedEF(CardEF):
Args:
abstract_data : dict representing the decoded data
record_nr : record number (1 for first record, ...)
total_len : expected total length of the encoded data (record length)
Returns:
binary encoded data
"""
method = getattr(self, '_encode_record_bin', None)
if callable(method):
return method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len))
return method(abstract_data, record_nr=record_nr)
method = getattr(self, '_encode_record_hex', None)
if callable(method):
return h2b(method(abstract_data, record_nr=record_nr, total_len = self.__get_rec_len(total_len)))
return h2b(method(abstract_data, record_nr=record_nr))
if self._construct:
return build_construct(self._construct, abstract_data, {'total_len':self.__get_rec_len(total_len)})
if self._tlv:
return build_construct(self._construct, abstract_data)
elif self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return t.to_tlv()
raise NotImplementedError(
"%s encoder not yet implemented. Patches welcome." % self)
@staticmethod
def export(as_json: bool, lchan):
"""
Export the file contents of a LinFixedEF (or a CyclicEF). This method returns a shell command string (See also
ShellCommand definition in this class) that can be used to write the file contents back.
"""
# A CyclicEF is a subclass of LinFixedEF.
if lchan.selected_file_structure() != 'linear_fixed' and lchan.selected_file_structure() != 'cyclic':
raise ValueError("selected file has structure type '%s', expecting a file with structure 'linear_fixed' or 'cyclic'" %
lchan.selected_file_structure())
export_str = ""
# Use number of records specified in select response
num_of_rec = lchan.selected_file_num_of_rec()
if num_of_rec:
for r in range(1, num_of_rec + 1):
if as_json:
result = lchan.read_record_dec(r)
export_str += ("update_record_decoded %d '%s'\n" % (r, json.dumps(result[0], cls=JsonEncoder)))
else:
result = lchan.read_record(r)
export_str += ("update_record %d %s\n" % (r, str(result[0])))
# In case the select response does not return the number of records, read until we hit the first record that
# cannot be read.
else:
r = 1
while True:
try:
if as_json:
result = lchan.read_record_dec(r)
export_str += ("update_record_decoded %d '%s'\n" % (r, json.dumps(result[0], cls=JsonEncoder)))
else:
result = lchan.read_record(r)
export_str += ("update_record %d %s\n" % (r, str(result[0])))
except SwMatchError as e:
# We are past the last valid record - stop
if e.sw_actual == "9402":
break
# Some other problem occurred
else:
raise e
r = r + 1
return export_str.strip()
class CyclicEF(LinFixedEF):
"""Cyclic EF (Entry File) in the smart card filesystem"""
@@ -1233,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()
@@ -1260,26 +1144,13 @@ 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()
return {'raw': raw_hex_data}
def __get_rec_len(self, total_len: Optional[int] = None) -> Optional[int]:
"""Get the length (total length) of the file record"""
# Caller has provided the actual total length of the record, this should be the default case
if total_len is not None:
return total_len
# Alternatively use the record length from the specification
if self.rec_len:
return self.rec_len
return None
def encode_record_hex(self, abstract_data: dict, total_len: Optional[int] = None) -> str:
def encode_record_hex(self, abstract_data: dict) -> str:
"""Encode abstract representation into raw (hex string) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
@@ -1288,27 +1159,25 @@ class TransRecEF(TransparentEF):
Args:
abstract_data : dict representing the decoded data
total_len : expected total length of the encoded data (record length)
Returns:
hex string encoded data
"""
method = getattr(self, '_encode_record_hex', None)
if callable(method):
return method(abstract_data, total_len = self.__get_rec_len(total_len))
return method(abstract_data)
method = getattr(self, '_encode_record_bin', None)
if callable(method):
return b2h(method(abstract_data, total_len = self.__get_rec_len(total_len)))
return b2h(method(abstract_data))
if self._construct:
return b2h(filter_dict(build_construct(self._construct, abstract_data,
{'total_len':self.__get_rec_len(total_len)})))
if self._tlv:
return b2h(filter_dict(build_construct(self._construct, abstract_data)))
elif self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return b2h(t.to_tlv())
raise NotImplementedError(
"%s encoder not yet implemented. Patches welcome." % self)
def encode_record_bin(self, abstract_data: dict, total_len: Optional[int] = None) -> bytearray:
def encode_record_bin(self, abstract_data: dict) -> bytearray:
"""Encode abstract representation into raw (binary) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
@@ -1317,20 +1186,18 @@ class TransRecEF(TransparentEF):
Args:
abstract_data : dict representing the decoded data
total_len : expected total length of the encoded data (record length)
Returns:
binary encoded data
"""
method = getattr(self, '_encode_record_bin', None)
if callable(method):
return method(abstract_data, total_len = self.__get_rec_len(total_len))
return method(abstract_data)
method = getattr(self, '_encode_record_hex', None)
if callable(method):
return h2b(method(abstract_data, total_len = self.__get_rec_len(total_len)))
return h2b(method(abstract_data))
if self._construct:
return filter_dict(build_construct(self._construct, abstract_data,
{'total_len':self.__get_rec_len(total_len)}))
if self._tlv:
return filter_dict(build_construct(self._construct, abstract_data))
elif self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return t.to_tlv()
@@ -1342,8 +1209,8 @@ class TransRecEF(TransparentEF):
for i in range(0, len(raw_bin_data), self.rec_len)]
return [self.decode_record_bin(x) for x in chunks]
def _encode_bin(self, abstract_data, **kwargs) -> bytes:
chunks = [self.encode_record_bin(x, total_len = kwargs.get('total_len', None)) for x in abstract_data]
def _encode_bin(self, abstract_data) -> bytes:
chunks = [self.encode_record_bin(x) for x in abstract_data]
# FIXME: pad to file size
return b''.join(chunks)
@@ -1360,50 +1227,48 @@ class BerTlvEF(CardEF):
class ShellCommands(CommandSet):
"""Shell commands specific for BER-TLV EFs."""
def __init__(self):
super().__init__()
retrieve_data_parser = argparse.ArgumentParser()
retrieve_data_parser.add_argument(
'TAG', type=auto_int, help='BER-TLV Tag of value to retrieve')
'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
@cmd2.with_argparser(retrieve_data_parser)
def do_retrieve_data(self, opts):
"""Retrieve (Read) data from a BER-TLV EF"""
(data, _sw) = self._cmd.lchan.retrieve_data(opts.TAG)
(data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
self._cmd.poutput(data)
def do_retrieve_tags(self, _opts):
def do_retrieve_tags(self, opts):
"""List tags available in a given BER-TLV EF"""
tags = self._cmd.lchan.retrieve_tags()
self._cmd.poutput(tags)
set_data_parser = argparse.ArgumentParser()
set_data_parser.add_argument(
'TAG', type=auto_int, help='BER-TLV Tag of value to set')
set_data_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
'tag', type=auto_int, help='BER-TLV Tag of value to set')
set_data_parser.add_argument(
'data', help='Data bytes (hex format) to write')
@cmd2.with_argparser(set_data_parser)
def do_set_data(self, opts):
"""Set (Write) data for a given tag in a BER-TLV EF"""
(data, _sw) = self._cmd.lchan.set_data(opts.TAG, opts.data)
(data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
if data:
self._cmd.poutput(data)
del_data_parser = argparse.ArgumentParser()
del_data_parser.add_argument(
'TAG', type=auto_int, help='BER-TLV Tag of value to set')
'tag', type=auto_int, help='BER-TLV Tag of value to set')
@cmd2.with_argparser(del_data_parser)
def do_delete_data(self, opts):
"""Delete data for a given tag in a BER-TLV EF"""
(data, _sw) = self._cmd.lchan.set_data(opts.TAG, None)
"""Delete data for a given tag in a BER-TLV EF"""
(data, sw) = self._cmd.lchan.set_data(opts.tag, None)
if data:
self._cmd.poutput(data)
def do_delete_all(self, opts):
"""Delete all data from a BER-TLV EF"""
tags = self._cmd.lchan.retrieve_tags()
for tag in tags:
self._cmd.lchan.set_data(tag, None)
def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
size: Size = (1, None), **kwargs):
"""
@@ -1420,34 +1285,6 @@ class BerTlvEF(CardEF):
self.size = size
self.shell_commands = [self.ShellCommands()]
@staticmethod
def export(as_json: bool, lchan):
"""
Export the file contents of a BerTlvEF. This method returns a shell command string (See also ShellCommand
definition in this class) that can be used to write the file contents back.
"""
if lchan.selected_file_structure() != 'ber_tlv':
raise ValueError("selected file has structure type '%s', expecting a file with structure 'ber_tlv'" %
lchan.selected_file_structure())
# TODO: Add JSON output as soon as we have a set_data_decoded command and a retrieve_data_dec method.
if as_json:
raise NotImplementedError("BerTlvEF encoder not yet implemented. Patches welcome.")
export_str = ""
tags = lchan.retrieve_tags()
if tags == []:
export_str += "# empty file, no tags"
else:
export_str += "delete_all\n"
for t in tags:
result = lchan.retrieve_data(t)
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
export_str += ("set_data 0x%02x %s\n" % (t, b2h(val)))
return export_str.strip()
def interpret_sw(sw_data: dict, sw: str):
"""Interpret a given status word.
@@ -1478,11 +1315,9 @@ class CardApplication:
adf : ADF name
sw : Dict of status word conversions
"""
if aid:
aid = aid.lower()
self.name = name
self.adf = adf
self.sw = sw or {}
self.sw = sw or dict()
# back-reference from ADF to Applicaiton
if self.adf:
self.aid = aid or self.adf.aid
@@ -1504,15 +1339,6 @@ class CardApplication:
"""
return interpret_sw(self.sw, sw)
@staticmethod
def export(as_json: bool, lchan):
"""
Export application specific parameters, in the form of commandline script. (see also comment in the export
method of class "CardFile")
"""
return "# %s has no exportable features" % str(lchan.selected_file)
class CardModel(abc.ABC):
"""A specific card model, typically having some additional vendor-specific files. All
@@ -1544,53 +1370,3 @@ class CardModel(abc.ABC):
for m in CardModel.__subclasses__():
if m.match(scc):
m.add_files(rs)
class Path:
"""Representation of a file-system path."""
def __init__(self, p: Union[str, List[str], List[int]]):
# split if given as single string with slahes
if isinstance(p, str):
p = p.split('/')
elif len(p) and isinstance(p[0], int):
p = ['%04x' % x for x in p]
# make sure internal representation alwas is uppercase only
self.list = [x.upper() for x in p]
def __str__(self) -> str:
return '/'.join(self.list)
def __repr__(self) -> str:
return 'Path(%s)' % (str(self))
def __eq__(self, other: 'Path') -> bool:
return self.list == other.list
def __getitem__(self, i):
return self.list[i]
def __len__(self):
return len(self.list)
def __add__(self, a):
if isinstance(a, list):
l = self.list + a
elif isinstance(a, Path):
l = self.list + a.list
else:
l = self.list + [a]
return Path(l)
def relative_to_mf(self) -> 'Path':
"""Return a path relative to MF, i.e. without initial explicit MF."""
if len(self.list) and self.list[0] in ['MF', '3F00']:
return Path(self.list[1:])
return self
def is_parent(self, other: 'Path') -> bool:
"""Is this instance a parent of the given other instance?"""
if len(self.list) >= len(other.list):
return False
if other.list[:len(self.list)] == self.list:
return True
return False

345
pySim/global_platform.py Normal file
View 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)

View File

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

View File

@@ -1,93 +0,0 @@
"""GlobalPlatform Remote Application Management over HTTP Card Specification v2.3 - Amendment B.
Also known as SCP81 for SIM/USIM/UICC/eUICC/eSIM OTA.
"""
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from construct import Struct, Int8ub, Int16ub, Bytes, GreedyBytes, GreedyString, BytesInteger
from construct import this, len_, Rebuild, Const
from construct import Optional as COptional
from osmocom.tlv import BER_TLV_IE
from pySim import cat
# Table 3-3 + Section 3.8.1
class RasConnectionParams(BER_TLV_IE, tag=0x84, nested=cat.OpenChannel.nested_collection_cls.possible_nested):
pass
# Table 3-3 + Section 3.8.2
class SecurityParams(BER_TLV_IE, tag=0x85):
_test_de_encode = [
( '850804deadbeef020040', {'kid': 64,'kvn': 0, 'psk_id': b'\xde\xad\xbe\xef', 'sha_type': None} )
]
_construct = Struct('_psk_id_len'/Rebuild(Int8ub, len_(this.psk_id)), 'psk_id'/Bytes(this._psk_id_len),
'_kid_kvn_len'/Const(2, Int8ub), 'kvn'/Int8ub, 'kid'/Int8ub,
'sha_type'/COptional(Int8ub))
# Table 3-3 + ?
class ExtendedSecurityParams(BER_TLV_IE, tag=0xA5):
_construct = GreedyBytes
# Table 3-3 + Section 3.8.3
class SessionRetryPolicyParams(BER_TLV_IE, tag=0x86):
_construct = Struct('retry_counter'/Int16ub,
'retry_waiting_delay'/BytesInteger(5),
'retry_report_failure'/COptional(GreedyBytes))
# Table 3-3 + Section 3.8.4
class AdminHostParam(BER_TLV_IE, tag=0x8A):
_test_de_encode = [
( '8a0a61646d696e2e686f7374', 'admin.host' ),
]
_construct = GreedyString('utf-8')
# Table 3-3 + Section 3.8.5
class AgentIdParam(BER_TLV_IE, tag=0x8B):
_construct = GreedyString('utf-8')
# Table 3-3 + Section 3.8.6
class AdminUriParam(BER_TLV_IE, tag=0x8C):
_test_de_encode = [
( '8c1668747470733a2f2f61646d696e2e686f73742f757269', 'https://admin.host/uri' ),
]
_construct = GreedyString('utf-8')
# Table 3-3
class HttpPostParams(BER_TLV_IE, tag=0x89, nested=[AdminHostParam, AgentIdParam, AdminUriParam]):
pass
# Table 3-3
class AdmSessionParams(BER_TLV_IE, tag=0x83, nested=[RasConnectionParams, SecurityParams,
ExtendedSecurityParams, SessionRetryPolicyParams,
HttpPostParams]):
pass
# Table 3-3 + Section 3.11.4
class RasFqdn(BER_TLV_IE, tag=0xD6):
_construct = GreedyBytes # FIXME: DNS String
# Table 3-3 + Section 3.11.7
class DnsConnectionParams(BER_TLV_IE, tag=0xFA, nested=cat.OpenChannel.nested_collection_cls.possible_nested):
pass
# Table 3-3
class DnsResolutionParams(BER_TLV_IE, tag=0xB3, nested=[RasFqdn, DnsConnectionParams]):
pass
# Table 3-3
class AdmSessTriggerParams(BER_TLV_IE, tag=0x81, nested=[AdmSessionParams, DnsResolutionParams]):
pass

View File

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

View File

@@ -1,107 +0,0 @@
# coding=utf-8
"""GlobalPLatform UICC Configuration 1.0 parameters
(C) 2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from construct import Optional as COptional
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
from osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import *
# Section 11.6.2.3 / Table 11-58
class SecurityDomainAid(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
class LoadFileDataBlockSignature(BER_TLV_IE, tag=0xc3):
_construct = GreedyBytes
class DapBlock(BER_TLV_IE, tag=0xe2, nested=[SecurityDomainAid, LoadFileDataBlockSignature]):
pass
class LoadFileDataBlock(BER_TLV_IE, tag=0xc4):
_construct = GreedyBytes
class Icv(BER_TLV_IE, tag=0xd3):
_construct = GreedyBytes
class CipheredLoadFileDataBlock(BER_TLV_IE, tag=0xd4):
_construct = GreedyBytes
class LoadFile(TLV_IE_Collection, nested=[DapBlock, LoadFileDataBlock, Icv, CipheredLoadFileDataBlock]):
pass
# UICC Configuration v1.0.1 / Section 4.3.2
class UiccScp(BER_TLV_IE, tag=0x81):
_construct = Struct('scp'/Int8ub, 'i'/Int8ub)
class AcceptExtradAppsAndElfToSd(BER_TLV_IE, tag=0x82):
_construct = GreedyBytes
class AcceptDelOfAssocSd(BER_TLV_IE, tag=0x83):
_construct = GreedyBytes
class LifeCycleTransitionToPersonalized(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
class CasdCapabilityInformation(BER_TLV_IE, tag=0x86):
_construct = GreedyBytes
class AcceptExtradAssocAppsAndElf(BER_TLV_IE, tag=0x87):
_construct = GreedyBytes
# Security Domain Install Parameters (inside C9 during INSTALL [for install])
class UiccSdInstallParams(TLV_IE_Collection, nested=[UiccScp, AcceptExtradAppsAndElfToSd, AcceptDelOfAssocSd,
LifeCycleTransitionToPersonalized,
CasdCapabilityInformation, AcceptExtradAssocAppsAndElf]):
def has_scp(self, scp: int) -> bool:
"""Determine if SD Installation parameters already specify given SCP."""
for c in self.children:
if not isinstance(c, UiccScp):
continue
if c.decoded['scp'] == scp:
return True
return False
def add_scp(self, scp: int, i: int):
"""Add given SCP (and i parameter) to list of SCP of the Security Domain Install Params.
Example: add_scp(0x03, 0x70) for SCP03, or add_scp(0x02, 0x55) for SCP02."""
if self.has_scp(scp):
raise ValueError('SCP%02x already present' % scp)
self.children.append(UiccScp(decoded={'scp': scp, 'i': i}))
def remove_scp(self, scp: int):
"""Remove given SCP from list of SCP of the Security Domain Install Params."""
for c in self.children:
if not isinstance(c, UiccScp):
continue
if c.decoded['scp'] == scp:
self.children.remove(c)
return
raise ValueError("SCP%02x not present" % scp)
# Key Usage:
# KVN 0x01 .. 0x0F reserved for SCP80
# KVN 0x11 reserved for DAP specified in ETSI TS 102 226
# KVN 0x20 .. 0x2F reserved for SCP02
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
# KVN 0x30 .. 0x3F reserved for SCP03
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
# KVN 0x70 KID 0x01: Token key (RSA public or DES)
# KVN 0x71 KID 0x01: Receipt key (DES)
# KVN 0x73 KID 0x01: DAP verifiation key (RS public or DES)
# KVN 0x74 reserved for CASD
# KID 0x01: PK.CA.AUT
# KID 0x02: SK.CASD.AUT (PK) and KS.CASD.AUT (Non-PK)
# KID 0x03: SK.CASD.CT (P) and KS.CASD.CT (Non-PK)
# KVN 0x75 KID 0x01: 16-byte DES key for Ciphered Load File Data Block
# KVN 0xFF reserved for ISD with SCP02 without SCP80 s upport

View File

@@ -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 osmocom.construct import *
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
@@ -290,7 +296,7 @@ class EF_Predefined(LinFixedEF):
else:
return parse_construct(self.construct_others, raw_bin_data)
def _encode_record_bin(self, abstract_data : dict, record_nr : int, **kwargs) -> bytearray:
def _encode_record_bin(self, abstract_data : dict, record_nr : int) -> bytearray:
r = None
if record_nr == 1:
r = self.construct_first.build(abstract_data)

214
pySim/gsmtap.py Normal file
View File

@@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
""" Osmocom GSMTAP python implementation.
GSMTAP is a packet format used for conveying a number of different
telecom-related protocol traces over UDP.
"""
#
# Copyright (C) 2022 Harald Welte <laforge@gnumonks.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import socket
from typing import List, Dict, Optional
from construct import Optional as COptional
from construct import *
from pySim.construct import *
# The root definition of GSMTAP can be found at
# https://cgit.osmocom.org/cgit/libosmocore/tree/include/osmocom/core/gsmtap.h
GSMTAP_UDP_PORT = 4729
# GSMTAP_TYPE_*
gsmtap_type_construct = Enum(Int8ub,
gsm_um = 0x01,
gsm_abis = 0x02,
gsm_um_burst = 0x03,
sim = 0x04,
tetra_i1 = 0x05,
tetra_i1_burst = 0x06,
wimax_burst = 0x07,
gprs_gb_llc = 0x08,
gprs_gb_sndcp = 0x09,
gmr1_um = 0x0a,
umts_rlc_mac = 0x0b,
umts_rrc = 0x0c,
lte_rrc = 0x0d,
lte_mac = 0x0e,
lte_mac_framed = 0x0f,
osmocore_log = 0x10,
qc_diag = 0x11,
lte_nas = 0x12,
e1_t1 = 0x13)
# TYPE_UM_BURST
gsmtap_subtype_burst_construct = Enum(Int8ub,
unknown = 0x00,
fcch = 0x01,
partial_sch = 0x02,
sch = 0x03,
cts_sch = 0x04,
compact_sch = 0x05,
normal = 0x06,
dummy = 0x07,
access = 0x08,
none = 0x09)
gsmtap_subtype_wimax_burst_construct = Enum(Int8ub,
cdma_code = 0x10,
fch = 0x11,
ffb = 0x12,
pdu = 0x13,
hack = 0x14,
phy_attributes = 0x15)
# GSMTAP_CHANNEL_*
gsmtap_subtype_um_construct = Enum(Int8ub,
unknown = 0x00,
bcch = 0x01,
ccch = 0x02,
rach = 0x03,
agch = 0x04,
pch = 0x05,
sdcch = 0x06,
sdcch4 = 0x07,
sdcch8 = 0x08,
facch_f = 0x09,
facch_h = 0x0a,
pacch = 0x0b,
cbch52 = 0x0c,
pdtch = 0x0d,
ptcch = 0x0e,
cbch51 = 0x0f,
voice_f = 0x10,
voice_h = 0x11)
# GSMTAP_SIM_*
gsmtap_subtype_sim_construct = Enum(Int8ub,
apdu = 0x00,
atr = 0x01,
pps_req = 0x02,
pps_rsp = 0x03,
tpdu_hdr = 0x04,
tpdu_cmd = 0x05,
tpdu_rsp = 0x06,
tpdu_sw = 0x07)
gsmtap_subtype_tetra_construct = Enum(Int8ub,
bsch = 0x01,
aach = 0x02,
sch_hu = 0x03,
sch_hd = 0x04,
sch_f = 0x05,
bnch = 0x06,
stch = 0x07,
tch_f = 0x08,
dmo_sch_s = 0x09,
dmo_sch_h = 0x0a,
dmo_sch_f = 0x0b,
dmo_stch = 0x0c,
dmo_tch = 0x0d)
gsmtap_subtype_gmr1_construct = Enum(Int8ub,
unknown = 0x00,
bcch = 0x01,
ccch = 0x02,
pch = 0x03,
agch = 0x04,
bach = 0x05,
rach = 0x06,
cbch = 0x07,
sdcch = 0x08,
tachh = 0x09,
gbch = 0x0a,
tch3 = 0x10,
tch6 = 0x14,
tch9 = 0x18)
gsmtap_subtype_e1t1_construct = Enum(Int8ub,
lapd = 0x01,
fr = 0x02,
raw = 0x03,
trau16 = 0x04,
trau8 = 0x05)
gsmtap_arfcn_construct = BitStruct('pcs'/Flag, 'uplink'/Flag, 'arfcn'/BitsInteger(14))
gsmtap_hdr_construct = Struct('version'/Int8ub,
'hdr_len'/Int8ub,
'type'/gsmtap_type_construct,
'timeslot'/Int8ub,
'arfcn'/gsmtap_arfcn_construct,
'signal_dbm'/Int8sb,
'snr_db'/Int8sb,
'frame_nr'/Int32ub,
'sub_type'/Switch(this.type, {
'gsm_um': gsmtap_subtype_um_construct,
'gsm_um_burst': gsmtap_subtype_burst_construct,
'sim': gsmtap_subtype_sim_construct,
'tetra_i1': gsmtap_subtype_tetra_construct,
'tetra_i1_burst': gsmtap_subtype_tetra_construct,
'wimax_burst': gsmtap_subtype_wimax_burst_construct,
'gmr1_um': gsmtap_subtype_gmr1_construct,
'e1_t1': gsmtap_subtype_e1t1_construct,
}),
'antenna_nr'/Int8ub,
'sub_slot'/Int8ub,
'res'/Int8ub,
'body'/GreedyBytes)
osmocore_log_ts_construct = Struct('sec'/Int32ub, 'usec'/Int32ub)
osmocore_log_level_construct = Enum(Int8ub, debug=1, info=3, notice=5, error=7, fatal=8)
gsmtap_osmocore_log_hdr_construct = Struct('ts'/osmocore_log_ts_construct,
'proc_name'/PaddedString(16, 'ascii'),
'pid'/Int32ub,
'level'/osmocore_log_level_construct,
Bytes(3),
'subsys'/PaddedString(16, 'ascii'),
'src_file'/Struct('name'/PaddedString(32, 'ascii'), 'line_nr'/Int32ub))
class GsmtapMessage:
"""Class whose objects represent a single GSMTAP message. Can encode and decode messages."""
def __init__(self, encoded = None):
self.encoded = encoded
self.decoded = None
def decode(self):
self.decoded = parse_construct(gsmtap_hdr_construct, self.encoded)
return self.decoded
def encode(self, decoded):
self.encoded = gsmtap_hdr_construct.build(decoded)
return self.encoded
class GsmtapSource:
def __init__(self, bind_ip:str='127.0.0.1', bind_port:int=4729):
self.bind_ip = bind_ip
self.bind_port = bind_port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind((self.bind_ip, self.bind_port))
def read_packet(self) -> GsmtapMessage:
data, addr = self.sock.recvfrom(1024)
gsmtap_msg = GsmtapMessage(data)
gsmtap_msg.decode()
if gsmtap_msg.decoded['version'] != 0x02:
raise ValueError('Unknown GSMTAP version 0x%02x' % gsmtap_msg.decoded['version'])
return gsmtap_msg.decoded, addr

View File

@@ -17,9 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from construct import GreedyBytes, GreedyString
from osmocom.tlv import *
from osmocom.construct import *
from construct import *
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
# Table 91 + Section 8.2.1.2
class ApplicationId(BER_TLV_IE, tag=0x4f):

View File

@@ -1,19 +0,0 @@
# JavaCard related utilities
import zipfile
import struct
import sys
import io
def ijc_to_cap(in_file: io.IOBase, out_zip: zipfile.ZipFile, p : str = "foo"):
"""Convert an ICJ (Interoperable Java Card) file [back] to a CAP file."""
TAGS = ["Header", "Directory", "Applet", "Import", "ConstantPool", "Class", "Method", "StaticField", "RefLocation", "Export", "Descriptor", "Debug"]
b = in_file.read()
while len(b):
tag, size = struct.unpack('!BH', b[0:3])
out_zip.writestr(p+"/javacard/"+TAGS[tag-1]+".cap", b[0:3+size])
b = b[3+size:]
# example usage:
# with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
# ijc_to_cap(f, z)

View File

@@ -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

View File

@@ -7,16 +7,16 @@ from smartcard.util import toBytes
from pytlv.TLV import *
from pySim.cards import SimCardBase, UiccCardBase
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_msisdn, enc_msisdn
from pySim.utils import enc_plmn, get_addr_type
from pySim.utils import is_hex, h2b, b2h, h2s, s2h, lpad, rpad
from pySim.legacy.utils import enc_ePDGSelection, format_xplmn_w_act, format_xplmn, dec_st, enc_st
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv, dec_msisdn, enc_msisdn
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv
from pySim.legacy.ts_51_011 import EF, DF
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
from pySim.profile.ts_51_011 import EF_AD, EF_SPN
from pySim.ts_51_011 import EF_AD, EF_SPN
def format_addr(addr: str, addr_type: str) -> str:
"""
@@ -753,7 +753,7 @@ class GrcardSim(SimCard):
# Set the Ki using proprietary command
pdu = '80d4020010' + p['ki']
data, sw = self._scc.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)

View File

@@ -20,10 +20,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Optional, Tuple
from pySim.utils import Hexstr, rpad, enc_plmn, h2i, i2s, s2h
from pySim.utils import dec_xplmn_w_act, dec_xplmn, dec_mcc_from_plmn, dec_mnc_from_plmn
from osmocom.utils import swap_nibbles, h2b, b2h
def hexstr_to_Nbytearr(s, nbytes):
return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
@@ -148,7 +146,7 @@ def dec_st(st, table="sim") -> str:
from pySim.ts_31_102 import EF_UST_map
lookup_map = EF_UST_map
else:
from pySim.profile.ts_51_011 import EF_SST_map
from pySim.ts_51_011 import EF_SST_map
lookup_map = EF_SST_map
st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
@@ -332,82 +330,3 @@ def enc_addr_tlv(addr, addr_type='00'):
s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
return s
def dec_msisdn(ef_msisdn: Hexstr) -> Optional[Tuple[int, int, Optional[str]]]:
"""
Decode MSISDN from EF.MSISDN or EF.ADN (same structure).
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3.
"""
# Convert from str to (kind of) 'bytes'
ef_msisdn = h2b(ef_msisdn)
# Make sure mandatory fields are present
if len(ef_msisdn) < 14:
raise ValueError("EF.MSISDN is too short")
# Skip optional Alpha Identifier
xlen = len(ef_msisdn) - 14
msisdn_lhv = ef_msisdn[xlen:]
# Parse the length (in bytes) of the BCD encoded number
bcd_len = msisdn_lhv[0]
# BCD length = length of dial num (max. 10 bytes) + 1 byte ToN and NPI
if bcd_len == 0xff:
return None
elif bcd_len > 11 or bcd_len < 1:
raise ValueError(
"Length of MSISDN (%d bytes) is out of range" % bcd_len)
# Parse ToN / NPI
ton = (msisdn_lhv[1] >> 4) & 0x07
npi = msisdn_lhv[1] & 0x0f
bcd_len -= 1
# No MSISDN?
if not bcd_len:
return (npi, ton, None)
msisdn = swap_nibbles(b2h(msisdn_lhv[2:][:bcd_len])).rstrip('f')
# International number 10.5.118/3GPP TS 24.008
if ton == 0x01:
msisdn = '+' + msisdn
return (npi, ton, msisdn)
def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
"""
Encode MSISDN as LHV so it can be stored to EF.MSISDN.
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3. (The result
will not contain the optional Alpha Identifier at the beginning.)
Default NPI / ToN values:
- NPI: ISDN / telephony numbering plan (E.164 / E.163),
- ToN: network specific or international number (if starts with '+').
"""
# If no MSISDN is supplied then encode the file contents as all "ff"
if msisdn in ["", "+"]:
return "ff" * 14
# Leading '+' indicates International Number
if msisdn[0] == '+':
msisdn = msisdn[1:]
ton = 0x01
# An MSISDN must not exceed 20 digits
if len(msisdn) > 20:
raise ValueError("msisdn must not be longer than 20 digits")
# Append 'f' padding if number of digits is odd
if len(msisdn) % 2 > 0:
msisdn += 'f'
# BCD length also includes NPI/ToN header
bcd_len = len(msisdn) // 2 + 1
npi_ton = (npi & 0x0f) | ((ton & 0x07) << 4) | 0x80
bcd = rpad(swap_nibbles(msisdn), 10 * 2) # pad to 10 octets
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)

View File

@@ -1,6 +1,6 @@
"""Code related to SIM/UICC OTA according to TS 102 225 + TS 31.115."""
# (C) 2021-2024 by Harald Welte <laforge@osmocom.org>
# (C) 2021-2023 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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, Tuple
from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
from osmocom.construct import *
from osmocom.utils import b2h
from pySim.sms import UserDataHeader
from typing import Optional
# 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
@@ -99,17 +97,6 @@ SmsCommandPacket = Struct('cmd_pkt_len'/Int16ub,
'tar'/Bytes(3),
'secured_data'/GreedyBytes)
# TS 102 226 Section 8.2.1.3.2.1
SimFileAccessAndToolkitAppSpecParams = Struct('access_domain'/Prefixed(Int8ub, GreedyBytes),
'prio_level_of_tk_app_inst'/Int8ub,
'max_num_of_timers'/Int8ub,
'max_text_length_for_menu_entry'/Int8ub,
'menu_entries'/PrefixedArray(Int8ub, Struct('id'/Int8ub,
'pos'/Int8ub)),
'max_num_of_channels'/Int8ub,
'msl'/Prefixed(Int8ub, GreedyBytes),
'tar_values'/Prefixed(Int8ub, GreedyRange(Bytes(3))))
class OtaKeyset:
"""The OTA related data (key material, counter) to be used in encrypt/decrypt."""
def __init__(self, algo_crypt: str, kic_idx: int, kic: bytes,
@@ -125,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
@@ -141,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
@@ -201,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."""
@@ -210,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:
@@ -248,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:
@@ -333,7 +324,6 @@ class OtaDialectSms(OtaDialect):
'response_status'/ResponseStatus,
'cc_rc'/Bytes(this.rhl-10),
'secured_data'/GreedyBytes)
hdr_construct = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
# length of signature in octets
@@ -344,7 +334,6 @@ class OtaDialectSms(OtaDialect):
len_cipher = 6 + len_sig + len(apdu)
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
pad_cnt = len(padding)
apdu = bytes(apdu) # make a copy so we don't modify the input data
apdu += padding
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
@@ -355,7 +344,8 @@ class OtaDialectSms(OtaDialect):
chl = 13 + len_sig
# CHL + SPI (+ KIC + KID)
part_head = self.hdr_construct.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
c = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
part_head = c.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
#print("part_head: %s" % b2h(part_head))
# CNTR + PCNTR (CNTR not used)
@@ -397,54 +387,8 @@ class OtaDialectSms(OtaDialect):
#print("envelope_data: %s" % b2h(envelope_data))
if len(envelope_data) > 140:
raise ValueError('Cannot encode command in a single SMS; Fragmentation not implemented')
return envelope_data
def decode_cmd(self, otak: OtaKeyset, encoded: bytes) -> Tuple[bytes, dict, bytes]:
"""Decode an encoded (encrypted, signed) OTA SMS Command-APDU."""
if True: # TODO: how to decide?
cpl = int.from_bytes(encoded[:2], 'big')
part_head = encoded[2:2+8]
ciph = encoded[2+8:]
envelope_data = otak.crypt.decrypt(ciph)
else:
part_head = encoded[:8]
envelope_data = encoded[8:]
hdr_dec = self.hdr_construct.parse(part_head)
# strip counter part from front of envelope_data
part_cnt = envelope_data[:6]
cntr = int.from_bytes(part_cnt[:5], 'big')
pad_cnt = int.from_bytes(part_cnt[5:], 'big')
envelope_data = envelope_data[6:]
spi = hdr_dec['spi']
if spi['rc_cc_ds'] == 'cc':
# split cc from front of APDU
cc = envelope_data[:8]
apdu = envelope_data[8:]
# verify CC
temp_data = cpl.to_bytes(2, 'big') + part_head + part_cnt + apdu
otak.auth.check_sig(temp_data, cc)
elif spi['rc_cc_ds'] == 'rc':
# CRC32
crc32_rx = int.from_bytes(envelope_data[:4], 'big')
# FIXME: crc32_computed = zlip.crc32(
# FIXME: verify RC
raise NotImplementedError
apdu = envelope_data[4:]
elif spi['rc_cc_ds'] == 'no_rc_cc_ds':
apdu = envelope_data
else:
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
apdu = apdu[:len(apdu)-pad_cnt]
return hdr_dec['tar'], spi, apdu
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
if isinstance(data, str):
data = h2b(data)
@@ -455,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
@@ -492,7 +436,7 @@ class OtaDialectSms(OtaDialect):
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
if res.response_status == 'por_ok' and len(res['secured_data']):
if res.response_status == 'por_ok':
dec = CompactRemoteResp.parse(res['secured_data'])
else:
dec = None

View File

@@ -1,77 +0,0 @@
import pprint
from pprint import PrettyPrinter
from functools import singledispatch, wraps
from typing import get_type_hints
from pySim.utils import b2h
def common_container_checks(f):
type_ = get_type_hints(f)['object']
base_impl = type_.__repr__
empty_repr = repr(type_()) # {}, [], ()
too_deep_repr = f'{empty_repr[0]}...{empty_repr[-1]}' # {...}, [...], (...)
@wraps(f)
def wrapper(object, context, maxlevels, level):
if type(object).__repr__ is not base_impl: # subclassed repr
return repr(object)
if not object: # empty, short-circuit
return empty_repr
if maxlevels and level >= maxlevels: # exceeding the max depth
return too_deep_repr
oid = id(object)
if oid in context: # self-reference
return pprint._recursion(object)
context[oid] = 1
result = f(object, context, maxlevels, level)
del context[oid]
return result
return wrapper
@singledispatch
def saferepr(object, context, maxlevels, level):
return repr(object)
@saferepr.register
def _handle_bytes(object: bytes, *args):
if len(object) <= 40:
return '"%s"' % b2h(object)
else:
return '"%s...%s"' % (b2h(object[:20]), b2h(object[-20:]))
@saferepr.register
@common_container_checks
def _handle_dict(object: dict, context, maxlevels, level):
level += 1
contents = [
f'{saferepr(k, context, maxlevels, level)}: '
f'{saferepr(v, context, maxlevels, level)}'
for k, v in sorted(object.items(), key=pprint._safe_tuple)
]
return f'{{{", ".join(contents)}}}'
@saferepr.register
@common_container_checks
def _handle_list(object: list, context, maxlevels, level):
level += 1
contents = [
f'{saferepr(v, context, maxlevels, level)}'
for v in object
]
return f'[{", ".join(contents)}]'
@saferepr.register
@common_container_checks
def _handle_tuple(object: tuple, context, maxlevels, level):
level += 1
if len(object) == 1:
return f'({saferepr(object[0], context, maxlevels, level)},)'
contents = [
f'{saferepr(v, context, maxlevels, level)}'
for v in object
]
return f'({", ".join(contents)})'
class HexBytesPrettyPrinter(PrettyPrinter):
def format(self, *args):
# it doesn't matter what the boolean values are here
return saferepr(*args), True, False

View File

@@ -21,14 +21,57 @@
# 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.exceptions import SwMatchError
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,
fids: List[str]) -> bool:
cla_byte_bak = scc.cla_byte
sel_ctrl_bak = scc.sel_ctrl
scc.reset_card()
scc.cla_byte = cla_byte
scc.sel_ctrl = sel_ctrl
rc = True
try:
for fid in fids:
scc.select_file(fid)
except:
rc = False
scc.reset_card()
scc.cla_byte = cla_byte_bak
scc.sel_ctrl = sel_ctrl_bak
return rc
def match_uicc(scc: SimCardCommands) -> bool:
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
card is considered a UICC card.
"""
return _mf_select_test(scc, "00", "0004", ["3f00"])
def match_sim(scc: SimCardCommands) -> bool:
""" Try to access MF via 2G APDUs (3GPP TS 11.11), if this works, the card
is also a simcard. This will be the case for most UICC cards, but there may
also be plain UICC cards without 2G support as well.
"""
return _mf_select_test(scc, "a0", "0000", ["3f00"])
def match_ruim(scc: SimCardCommands) -> bool:
""" Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
the card is considered an R-UIM card for CDMA.
"""
return _mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
class CardProfile:
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
@@ -95,39 +138,13 @@ class CardProfile:
return data_hex
@staticmethod
def _mf_select_test(scc: SimCardCommands,
cla_byte: str, sel_ctrl: str,
fids: List[str]) -> bool:
"""Helper function used by some derived _try_match_card() methods."""
scc.reset_card()
scc.cla_byte = cla_byte
scc.sel_ctrl = sel_ctrl
for fid in fids:
scc.select_file(fid)
@classmethod
@abc.abstractmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
"""Try to see if the specific profile matches the card. This method is a
def match_with_card(scc: SimCardCommands) -> bool:
"""Check if the specific profile matches the card. This method is a
placeholder that is overloaded by specific dirived classes. The method
actively probes the card to make sure the profile class matches the
physical card. This usually also means that the card is reset during
the process, so this method must not be called at random times. It may
only be called on startup. If there is no exception raised, we assume
the card matches the profile.
Args:
scc: SimCardCommands class
"""
pass
@classmethod
def match_with_card(cls, scc: SimCardCommands) -> bool:
"""Check if the specific profile matches the card. The method
actively probes the card to make sure the profile class matches the
physical card. This usually also means that the card is reset during
the process, so this method must not be called at random times. It may
only be called on startup.
Args:
@@ -135,17 +152,7 @@ class CardProfile:
Returns:
match = True, no match = False
"""
sel_backup = scc.sel_ctrl
cla_backup = scc.cla_byte
try:
cls._try_match_card(scc)
return True
except SwMatchError:
return False
finally:
scc.sel_ctrl = sel_backup
scc.cla_byte = cla_backup
scc.reset_card()
return False
@staticmethod
def pick(scc: SimCardCommands):
@@ -159,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)
@@ -179,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
@@ -186,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

View File

@@ -1,58 +0,0 @@
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.commands import SimCardCommands
from pySim.euicc import CardApplicationISDR, AID_ISD_R, AID_ECASD, GetCertsReq, GetCertsResp
class CardProfileEuiccSGP32(CardProfileUICC):
ORDER = 5
def __init__(self):
super().__init__(name='IoT eUICC (SGP.32)')
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
# try a command only supported by SGP.32
scc.cla_byte = "00"
scc.select_adf(AID_ISD_R)
CardApplicationISDR.store_data_tlv(scc, GetCertsReq(), GetCertsResp)
class CardProfileEuiccSGP22(CardProfileUICC):
ORDER = 6
def __init__(self):
super().__init__(name='Consumer eUICC (SGP.22)')
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
# try to read EID from ISD-R
scc.cla_byte = "00"
scc.select_adf(AID_ISD_R)
eid = CardApplicationISDR.get_eid(scc)
# TODO: Store EID identity?
class CardProfileEuiccSGP02(CardProfileUICC):
ORDER = 7
def __init__(self):
super().__init__(name='M2M eUICC (SGP.02)')
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
scc.cla_byte = "00"
scc.select_adf(AID_ECASD)
scc.get_data(0x5a)
# TODO: Store EID identity?

View File

@@ -1,394 +0,0 @@
# coding=utf-8
"""Card Profile of ETSI TS 102 221, the core UICC spec.
(C) 2021-2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from construct import Struct, FlagsEnum, GreedyString
from osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import BER_TLV_IE
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile import CardProfile
from pySim import iso7816_4
from pySim.ts_102_221 import decode_select_response, ts_102_22x_cmdset
from pySim.ts_102_221 import AM_DO_EF, SC_DO, AdditionalInterfacesSupport, AdditionalTermCapEuicc
from pySim.ts_102_221 import TerminalPowerSupply, ExtendedLchanTerminalSupport, TerminalCapability
# A UICC will usually also support 2G functionality. If this is the case, we
# need to add DF_GSM and DF_TELECOM along with the UICC related files
from pySim.profile.ts_51_011 import AddonSIM, EF_ICCID, EF_PL
from pySim.profile.gsm_r import AddonGSMR
from pySim.profile.cdma_ruim import AddonRUIM
# TS 102 221 Section 13.1
class EF_DIR(LinFixedEF):
_test_de_encode = [
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
{ "application_label": "USim1" },
{ "discretionary_template": h2b("a00c80011781025f608203454150") } ] }
),
( '61194f10a0000000871004ffffffff890709000050054953696d31',
{ "application_template": [ { "application_id": h2b("a0000000871004ffffffff8907090000") },
{ "application_label": "ISim1" } ] }
),
]
class ApplicationLabel(BER_TLV_IE, tag=0x50):
# TODO: UCS-2 coding option as per Annex A of TS 102 221
_construct = GreedyString('ascii')
# see https://github.com/PyCQA/pylint/issues/5794
#pylint: disable=undefined-variable
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
iso7816_4.ApplicationRelatedDOSet]):
pass
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
self._tlv = EF_DIR.ApplicationTemplate
# TS 102 221 Section 13.4
class EF_ARR(LinFixedEF):
_test_de_encode = [
( '800101a40683010a950108800106900080016097008401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "control_reference_template": "ADM1" } ],
[ { "access_mode": [ "write_append", "update_erase" ] },
{ "always": None } ],
[ { "access_mode": [ "delete_file", "terminate_ef" ] },
{ "never": None } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
( '80010190008001029700800118a40683010a9501088401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "always": None } ],
[ { "access_mode": [ "update_erase" ] },
{ "never": None } ],
[ { "access_mode": [ "activate_file_or_record", "deactivate_file_or_record" ] },
{ "control_reference_template": "ADM1" } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
]
def __init__(self, fid='2f06', sfid=0x06, name='EF.ARR', desc='Access Rule Reference'):
super().__init__(fid, sfid=sfid, name=name, desc=desc)
# add those commands to the general commands of a TransparentEF
self.shell_commands += [self.AddlShellCommands()]
@staticmethod
def flatten(inp: list):
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
def sc_abbreviate(sc):
if 'always' in sc:
return 'always'
elif 'never' in sc:
return 'never'
elif 'control_reference_template' in sc:
return sc['control_reference_template']
else:
return sc
by_mode = {}
for t in inp:
am = t[0]
sc = t[1]
sc_abbr = sc_abbreviate(sc)
if 'access_mode' in am:
for m in am['access_mode']:
by_mode[m] = sc_abbr
elif 'command_header' in am:
ins = am['command_header']['INS']
if 'CLA' in am['command_header']:
cla = am['command_header']['CLA']
else:
cla = None
cmd = ts_102_22x_cmdset.lookup(ins, cla)
if cmd:
name = cmd.name.lower().replace(' ', '_')
by_mode[name] = sc_abbr
else:
raise ValueError
else:
raise ValueError
return by_mode
def _decode_record_bin(self, raw_bin_data, **kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
dec = arr_seq.decode_multi(raw_bin_data)
# we cannot pass the result through flatten() here, as we don't have a related
# 'un-flattening' decoder, and hence would be unable to encode :(
return dec[0]
def _encode_record_bin(self, in_json, **kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
return arr_seq.encode_multi(in_json)
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
def do_read_arr_record(self, opts):
"""Read one EF.ARR record in flattened, human-friendly form."""
(data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
data = self._cmd.lchan.selected_file.flatten(data)
self._cmd.poutput_json(data, opts.oneline)
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
def do_read_arr_records(self, opts):
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
# collect all results in list so they are rendered as JSON list when printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
data = self._cmd.lchan.selected_file.flatten(data)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
# TS 102 221 Section 13.6
class EF_UMPC(TransparentEF):
_test_de_encode = [
( '3cff02', { "max_current_mA": 60, "t_op_s": 255,
"addl_info": { "req_inc_idle_current": False, "support_uicc_suspend": True } } ),
( '320500', { "max_current_mA": 50, "t_op_s": 5, "addl_info": {"req_inc_idle_current": False,
"support_uicc_suspend": False } } ),
]
def __init__(self, fid='2f08', sfid=0x08, name='EF.UMPC', desc='UICC Maximum Power Consumption'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(5, 5))
addl_info = FlagsEnum(Byte, req_inc_idle_current=1,
support_uicc_suspend=2)
self._construct = Struct(
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
class CardProfileUICC(CardProfile):
ORDER = 10
def __init__(self, name='UICC'):
files = [
EF_DIR(),
EF_ICCID(),
EF_PL(),
EF_ARR(),
# FIXME: DF.CD
EF_UMPC(),
]
addons = [
AddonSIM,
AddonGSMR,
AddonRUIM,
]
sw = {
'Normal': {
'9000': 'Normal ending of the command',
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
},
'Postponed processing': {
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
},
'Warnings': {
'6200': 'No information given, state of non-volatile memory unchanged',
'6281': 'Part of returned data may be corrupted',
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
'6283': 'Selected file invalidated/disabled; needs to be activated before use',
'6284': 'Selected file in termination state',
'62f1': 'More data available',
'62f2': 'More data available and proactive command pending',
'62f3': 'Response data available',
'63f1': 'More data expected',
'63f2': 'More data expected and proactive command pending',
'63cx': 'Command successful but after using an internal update retry routine X times',
},
'Execution errors': {
'6400': 'No information given, state of non-volatile memory unchanged',
'6500': 'No information given, state of non-volatile memory changed',
'6581': 'Memory problem',
},
'Checking errors': {
'6700': 'Wrong length',
'67xx': 'The interpretation of this status word is command dependent',
'6b00': 'Wrong parameter(s) P1-P2',
'6d00': 'Instruction code not supported or invalid',
'6e00': 'Class not supported',
'6f00': 'Technical problem, no precise diagnosis',
'6fxx': 'The interpretation of this status word is command dependent',
},
'Functions in CLA not supported': {
'6800': 'No information given',
'6881': 'Logical channel not supported',
'6882': 'Secure messaging not supported',
},
'Command not allowed': {
'6900': 'No information given',
'6981': 'Command incompatible with file structure',
'6982': 'Security status not satisfied',
'6983': 'Authentication/PIN method blocked',
'6984': 'Referenced data invalidated',
'6985': 'Conditions of use not satisfied',
'6986': 'Command not allowed (no EF selected)',
'6989': 'Command not allowed - secure channel - security not satisfied',
},
'Wrong parameters': {
'6a80': 'Incorrect parameters in the data field',
'6a81': 'Function not supported',
'6a82': 'File not found',
'6a83': 'Record not found',
'6a84': 'Not enough memory space',
'6a86': 'Incorrect parameters P1 to P2',
'6a87': 'Lc inconsistent with P1 to P2',
'6a88': 'Referenced data not found',
},
'Application errors': {
'9850': 'INCREASE cannot be performed, max value reached',
'9862': 'Authentication error, application specific',
'9863': 'Security session or association expired',
'9864': 'Minimum UICC suspension time is too long',
},
}
super().__init__(name, desc='ETSI TS 102 221', cla="00",
sel_ctrl="0004", files_in_mf=files, sw=sw,
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
@staticmethod
def decode_select_response(data_hex: str) -> object:
"""ETSI TS 102 221 Section 11.1.1.3"""
return decode_select_response(data_hex)
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
card is considered a UICC card."""
cls._mf_select_test(scc, "00", "0004", ["3f00"])
@with_default_category('TS 102 221 Specific Commands')
class AddlShellCommands(CommandSet):
suspend_uicc_parser = argparse.ArgumentParser()
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
help='Proposed minimum duration of suspension')
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
help='Proposed maximum duration of suspension')
# not ISO7816-4 but TS 102 221
@cmd2.with_argparser(suspend_uicc_parser)
def do_suspend_uicc(self, opts):
"""Perform the SUSPEND UICC command. Only supported on some UICC (check EF.UMPC)."""
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
max_len_secs=opts.max_duration_secs)
self._cmd.poutput(
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
resume_uicc_parser = argparse.ArgumentParser()
resume_uicc_parser.add_argument('TOKEN', type=str, help='Token provided during SUSPEND')
@cmd2.with_argparser(resume_uicc_parser)
def do_resume_uicc(self, opts):
"""Perform the REUSME UICC operation. Only supported on some UICC. Also: A power-cycle
of the card is required between SUSPEND and RESUME, and only very few non-RESUME
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
self._cmd.card._scc.resume_uicc(opts.TOKEN)
term_cap_parser = argparse.ArgumentParser()
# power group
tc_power_grp = term_cap_parser.add_argument_group('Terminal Power Supply')
tc_power_grp.add_argument('--used-supply-voltage-class', type=str, choices=['a','b','c','d','e'],
help='Actual used Supply voltage class')
tc_power_grp.add_argument('--maximum-available-power-supply', type=auto_uint8,
help='Maximum available power supply of the terminal')
tc_power_grp.add_argument('--actual-used-freq-100k', type=auto_uint8,
help='Actual used clock frequency (in units of 100kHz)')
# no separate groups for those two
tc_elc_grp = term_cap_parser.add_argument_group('Extended logical channels terminal support')
tc_elc_grp.add_argument('--extended-logical-channel', action='store_true',
help='Extended Logical Channel supported')
tc_aif_grp = term_cap_parser.add_argument_group('Additional interfaces support')
tc_aif_grp.add_argument('--uicc-clf', action='store_true',
help='Local User Interface in the Device (LUId) supported')
# eUICC group
tc_euicc_grp = term_cap_parser.add_argument_group('Additional Terminal capability indications related to eUICC')
tc_euicc_grp.add_argument('--lui-d', action='store_true',
help='Local User Interface in the Device (LUId) supported')
tc_euicc_grp.add_argument('--lpd-d', action='store_true',
help='Local Profile Download in the Device (LPDd) supported')
tc_euicc_grp.add_argument('--lds-d', action='store_true',
help='Local Discovery Service in the Device (LPDd) supported')
tc_euicc_grp.add_argument('--lui-e-scws', action='store_true',
help='LUIe based on SCWS supported')
tc_euicc_grp.add_argument('--metadata-update-alerting', action='store_true',
help='Metadata update alerting supported')
tc_euicc_grp.add_argument('--enterprise-capable-device', action='store_true',
help='Enterprise Capable Device')
tc_euicc_grp.add_argument('--lui-e-e4e', action='store_true',
help='LUIe using E4E (ENVELOPE tag E4) supported')
tc_euicc_grp.add_argument('--lpr', action='store_true',
help='LPR (LPA Proxy) supported')
@cmd2.with_argparser(term_cap_parser)
def do_terminal_capability(self, opts):
"""Perform the TERMINAL CAPABILITY function. Used to inform the UICC about terminal capability."""
ps_flags = {}
addl_if_flags = {}
euicc_flags = {}
opts_dict = vars(opts)
power_items = ['used_supply_voltage_class', 'maximum_available_power_supply', 'actual_used_freq_100k']
if any(opts_dict[x] for x in power_items):
if not all(opts_dict[x] for x in power_items):
raise argparse.ArgumentTypeError('If any of the Terminal Power Supply group options are used, all must be specified')
for k, v in opts_dict.items():
if k in AdditionalInterfacesSupport._construct.flags.keys():
addl_if_flags[k] = v
elif k in AdditionalTermCapEuicc._construct.flags.keys():
euicc_flags[k] = v
elif k in [f.name for f in TerminalPowerSupply._construct.subcons]:
if k == 'used_supply_voltage_class' and v:
v = {v: True}
ps_flags[k] = v
child_list = []
if any(x for x in ps_flags.values()):
child_list.append(TerminalPowerSupply(decoded=ps_flags))
if opts.extended_logical_channel:
child_list.append(ExtendedLchanTerminalSupport())
if any(x for x in addl_if_flags.values()):
child_list.append(AdditionalInterfacesSupport(decoded=addl_if_flags))
if any(x for x in euicc_flags.values()):
child_list.append(AdditionalTermCapEuicc(decoded=euicc_flags))
print(child_list)
tc = TerminalCapability(children=child_list)
self.terminal_capability(b2h(tc.to_tlv()))
def terminal_capability(self, data:Hexstr):
cmd_hex = "80AA0000%02x%s" % (len(data)//2, data)
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)

View File

@@ -18,9 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, Tuple
from osmocom.utils import h2b, i2h, is_hex, Hexstr
from osmocom.tlv import bertlv_parse_one
from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
from pySim.exceptions import *
from pySim.filesystem import *
@@ -30,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."""
@@ -50,9 +50,6 @@ class RuntimeState:
self.lchan = {}
# the basic logical channel always exists
self.lchan[0] = RuntimeLchan(0, self)
# this is a dict of card identities which different parts of the code might populate,
# typically with something like ICCID, EID, ATR, ...
self.identity = {}
# make sure the class and selection control bytes, which are specified
# by the card profile are used
@@ -119,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))
@@ -135,18 +132,13 @@ class RuntimeState:
"""
# delete all lchan != 0 (basic lchan)
for lchan_nr in list(self.lchan.keys()):
self.lchan[lchan_nr].scc.scp = None
if lchan_nr == 0:
continue
del self.lchan[lchan_nr]
atr = i2h(self.card.reset())
if cmd_app:
cmd_app.lchan = self.lchan[0]
# select MF to reset internal state and to verify card really works
self.lchan[0].select('MF', cmd_app)
self.lchan[0].selected_adf = None
# store ATR as part of our card identies dict
self.identity['ATR'] = atr
return atr
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
@@ -211,18 +203,6 @@ class RuntimeLchan:
def selected_file_num_of_rec(self) -> Optional[int]:
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
def selected_file_record_len(self) -> Optional[int]:
return self.selected_file_fcp['file_descriptor'].get('record_len')
def selected_file_size(self) -> Optional[int]:
return self.selected_file_fcp.get('file_size')
def selected_file_reserved_file_size(self) -> Optional[int]:
return self.selected_file_fcp['proprietary_information'].get('reserved_file_size')
def selected_file_maximum_file_size(self) -> Optional[int]:
return self.selected_file_fcp['proprietary_information'].get('maximum_file_size')
def get_cwd(self) -> CardDF:
"""Obtain the current working directory.
@@ -247,42 +227,6 @@ class RuntimeLchan:
node = node.parent
return None
def get_file_by_name(self, name: str) -> CardFile:
"""Obtain the file object from the file system tree by its name without actually selecting the file.
Returns:
CardFile() instance or None"""
# handling of entire paths with multiple directories/elements
if '/' in name:
pathlist = name.split('/')
# treat /DF.GSM/foo like MF/DF.GSM/foo
if pathlist[0] == '':
pathlist[0] = 'MF'
else:
pathlist = [name]
# start in the current working directory (we can still
# select any ADF and the MF from here, so those will be
# among the selectables).
file = self.get_cwd()
for p in pathlist:
# Look for the next file in the path list
selectables = file.get_selectables()
file = None
for selectable in selectables:
if selectable == p:
file = selectables[selectable]
break
# When we hit none, then the given path must be invalid
if file is None:
return None
# Return the file object found at the tip of the path
return file
def interpret_sw(self, sw: str):
"""Interpret a given status word relative to the currently selected application
or the underlying card profile.
@@ -311,8 +255,7 @@ class RuntimeLchan:
raise ValueError(
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
# unregister commands of old file
self.unregister_cmds(cmd_app)
self._select_pre(cmd_app)
try:
# We access the card through the select_file method of the scc object.
@@ -321,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:
@@ -345,6 +288,12 @@ class RuntimeLchan:
self._select_post(cmd_app, f, data)
def _select_pre(self, cmd_app):
# unregister commands of old file
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.unregister_command_set(c)
def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
# we store some reference data (see above) about the currently selected file.
# This data must be updated after every select.
@@ -355,12 +304,11 @@ 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
self.register_cmds(cmd_app)
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.register_command_set(c)
def select_file(self, file: CardFile, cmd_app=None):
"""Select a file (EF, DF, ADF, MF, ...).
@@ -369,31 +317,11 @@ class RuntimeLchan:
file : CardFile [or derived class] instance
cmd_app : Command Application State (for unregistering old file commands)
"""
if not isinstance(file, CardADF) and self.selected_adf and self.selected_adf.has_fs == False:
# Not every application that may be present on a GlobalPlatform card will support the SELECT
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
# "select by name" method, which means we can only select an application and not a file.
# The consequence of this is that we may get trapped in an application that does not have
# ISIM/USIM like file system support and the only way to leave that application is to select
# an ISIM/USIM application in order to get the file system access back.
#
# To automate this escape-route we will first select an arbitrary ADF that has file system support first
# and then continue normally.
for selectable in self.rs.mf.get_selectables().items():
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
self.select(selectable[1].name, cmd_app)
break
# we need to find a path from our self.selected_file to the destination
inter_path = self.selected_file.build_select_path_to(file)
if not inter_path:
raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
# unregister commands of old file
self.unregister_cmds(cmd_app)
self._select_pre(cmd_app)
# be sure the variables that we pass to _select_post contain valid values.
selected_file = self.selected_file
@@ -409,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)
@@ -463,11 +391,10 @@ 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_filename(self, name: str):
"""Get the related CardFile object for a specified filename."""
def get_file_for_selectable(self, name: str):
sels = self.selected_file.get_selectables()
return sels[name]
@@ -488,8 +415,7 @@ class RuntimeLchan:
binary data read from the file
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
raise TypeError("Only works with TransparentEF")
return self.scc.read_binary(self.selected_file.fid, length, offset)
def read_binary_dec(self) -> Tuple[dict, str]:
@@ -513,8 +439,7 @@ class RuntimeLchan:
offset : Offset into the file from which to write 'data_hex'
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
raise TypeError("Only works with TransparentEF")
return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
def update_binary_dec(self, data: dict):
@@ -524,7 +449,7 @@ class RuntimeLchan:
Args:
data : abstract data which is to be encoded and written
"""
data_hex = self.selected_file.encode_hex(data, self.selected_file_size())
data_hex = self.selected_file.encode_hex(data)
return self.update_binary(data_hex)
def read_record(self, rec_nr: int = 0):
@@ -536,8 +461,7 @@ class RuntimeLchan:
hex string of binary data contained in record
"""
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
raise TypeError("Only works with Linear Fixed EF")
# returns a string of hex nibbles
return self.scc.read_record(self.selected_file.fid, rec_nr)
@@ -560,8 +484,7 @@ class RuntimeLchan:
data_hex : Hex string binary data to be written
"""
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
raise TypeError("Only works with Linear Fixed EF")
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
conserve=self.rs.conserve_write,
leftpad=self.selected_file.leftpad)
@@ -574,7 +497,7 @@ class RuntimeLchan:
rec_nr : Record number to read
data_hex : Abstract data to be written
"""
data_hex = self.selected_file.encode_record_hex(data, rec_nr, self.selected_file_record_len())
data_hex = self.selected_file.encode_record_hex(data, rec_nr)
return self.update_record(rec_nr, data_hex)
def retrieve_data(self, tag: int = 0):
@@ -597,10 +520,9 @@ class RuntimeLchan:
list of integer tags contained in EF
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
data, _sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
_tag, _length, value, _remainder = bertlv_parse_one(h2b(data))
raise TypeError("Only works with BER-TLV EF")
data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
tag, length, value, remainder = bertlv_parse_one(h2b(data))
return list(value)
def set_data(self, tag: int, data_hex: str):
@@ -611,18 +533,14 @@ class RuntimeLchan:
data_hex : Hex string binary data to be written (value portion)
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
raise TypeError("Only works with BER-TLV EF")
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
def register_cmds(self, cmd_app=None):
"""Register command set that is associated with the currently selected file"""
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.register_command_set(c)
def unregister_cmds(self, cmd_app=None):
"""Unregister command set that is associated with the currently selected file"""
"""Unregister all file specific commands."""
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.unregister_command_set(c)

View File

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

View File

@@ -20,11 +20,12 @@
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
from osmocom.construct import HexAdapter, BcdAdapter, TonNpi
from osmocom.utils import Hexstr, h2b, b2h
from pySim.construct import HexAdapter, BcdAdapter, TonNpi
from pySim.utils import Hexstr, h2b, b2h
from smpp.pdu import pdu_types, operations
@@ -50,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''})
@@ -116,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)
@@ -128,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:
@@ -184,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]
@@ -204,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 = {
@@ -213,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)
@@ -223,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']
@@ -274,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):
@@ -283,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
@@ -301,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]
@@ -312,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 = {
@@ -322,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':
@@ -332,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:
@@ -366,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)
@@ -378,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,

View File

@@ -17,16 +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, Const
from construct import Optional as COptional
from osmocom.utils import *
from osmocom.construct import *
from pytlv.TLV import *
from struct import pack, unpack
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.runtime import RuntimeState
from pySim.ts_102_221 import CardProfileUICC
from pySim.construct import *
from construct import *
import pySim
key_type2str = {
@@ -72,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):
@@ -181,7 +179,7 @@ class DF_SYSTEM(CardDF):
self.add_files(files)
def decode_select_response(self, resp_hex):
return CardProfileUICC.decode_select_response(resp_hex)
return pySim.ts_102_221.CardProfileUICC.decode_select_response(resp_hex)
class EF_USIM_SQN(TransparentEF):
@@ -207,18 +205,6 @@ class EF_USIM_SQN(TransparentEF):
class EF_USIM_AUTH_KEY(TransparentEF):
_test_de_encode = [
( '141898d827f70120d33b3e7462ee5fd6fe6ca53d7a0a804561646816d7b0c702fb',
{ "cfg": { "only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True, "algorithm": "milenage" },
"key": "1898d827f70120d33b3e7462ee5fd6fe", "op_opc": "6ca53d7a0a804561646816d7b0c702fb" } ),
( '160a04101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f000102030405060708090a0b0c0d0e0f',
{ "cfg" : { "algorithm" : "tuak", "key_length" : 128, "sres_deriv_func_in_2g" : 1, "use_opc_instead_of_op" : True },
"tuak_cfg" : { "ck_and_ik_size" : 128, "mac_size" : 128, "res_size" : 128 },
"num_of_keccak_iterations" : 4,
"k" : "000102030405060708090a0b0c0d0e0f",
"op_opc" : "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
} ),
]
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
super().__init__(fid, name=name, desc='USIM authentication key')
Algorithm = Enum(Nibble, milenage=4, sha1_aka=5, tuak=6, xor=15)
@@ -253,7 +239,7 @@ class EF_USIM_AUTH_KEY(TransparentEF):
else:
return parse_construct(self._construct, raw_bin_data)
def _encode_bin(self, abstract_data: dict, **kwargs) -> bytearray:
def _encode_bin(self, abstract_data: dict) -> bytearray:
if abstract_data['cfg']['algorithm'] == 'tuak':
return build_construct(self._constr_tuak, abstract_data)
else:

458
pySim/tlv.py Normal file
View File

@@ -0,0 +1,458 @@
"""object-oriented TLV parser/encoder library."""
# (C) 2021 by Harald Welte <laforge@osmocom.org>
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, List, Dict, Any, Tuple
from bidict import bidict
from construct import *
from pySim.utils import bertlv_encode_len, bertlv_parse_len, bertlv_encode_tag, bertlv_parse_tag
from pySim.utils import comprehensiontlv_encode_tag, comprehensiontlv_parse_tag
from pySim.utils import bertlv_parse_tag_raw, comprehensiontlv_parse_tag_raw
from pySim.construct import build_construct, parse_construct, LV, HexAdapter, BcdAdapter, BitsRFU, GsmStringAdapter
from pySim.exceptions import *
import inspect
import abc
import re
def camel_to_snake(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
class TlvMeta(abc.ABCMeta):
"""Metaclass which we use to set some class variables at the time of defining a subclass.
This allows us to create subclasses for each TLV/IE type, where the class represents fixed
parameters like the tag/type and instances of it represent the actual TLV data."""
def __new__(metacls, name, bases, namespace, **kwargs):
#print("TlvMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
x = super().__new__(metacls, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.tag = namespace.get('tag', kwargs.get('tag', None))
x.desc = namespace.get('desc', kwargs.get('desc', None))
nested = namespace.get('nested', kwargs.get('nested', None))
if nested is None or inspect.isclass(nested) and issubclass(nested, TLV_IE_Collection):
# caller has specified TLV_IE_Collection sub-class, we can directly reference it
x.nested_collection_cls = nested
else:
# caller passed list of other TLV classes that might possibly appear within us,
# build a dynamically-created TLV_IE_Collection sub-class and reference it
name = 'auto_collection_%s' % (name)
cls = type(name, (TLV_IE_Collection,), {'nested': nested})
x.nested_collection_cls = cls
return x
class TlvCollectionMeta(abc.ABCMeta):
"""Metaclass which we use to set some class variables at the time of defining a subclass.
This allows us to create subclasses for each Collection type, where the class represents fixed
parameters like the nested IE classes and instances of it represent the actual TLV data."""
def __new__(metacls, name, bases, namespace, **kwargs):
#print("TlvCollectionMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
x = super().__new__(metacls, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
return x
class Transcodable(abc.ABC):
_construct = None
"""Base class for something that can be encoded + encoded. Decoding and Encoding happens either
* via a 'construct' object stored in a derived class' _construct variable, or
* via a 'construct' object stored in an instance _construct variable, or
* via a derived class' _{to,from}_bytes() methods."""
def __init__(self):
self.encoded = None
self.decoded = None
self._construct = None
def to_bytes(self, context: dict = {}) -> bytes:
"""Convert from internal representation to binary bytes. Store the binary result
in the internal state and return it."""
if self.decoded == None:
do = b''
elif self._construct:
do = build_construct(self._construct, self.decoded, context)
elif self.__class__._construct:
do = build_construct(self.__class__._construct, self.decoded, context)
else:
do = self._to_bytes()
self.encoded = do
return do
# not an abstractmethod, as it is only required if no _construct exists
def _to_bytes(self):
raise NotImplementedError('%s._to_bytes' % type(self).__name__)
def from_bytes(self, do: bytes, context: dict = {}):
"""Convert from binary bytes to internal representation. Store the decoded result
in the internal state and return it."""
self.encoded = do
if self.encoded == b'':
self.decoded = None
elif self._construct:
self.decoded = parse_construct(self._construct, do, context=context)
elif self.__class__._construct:
self.decoded = parse_construct(self.__class__._construct, do, context=context)
else:
self.decoded = self._from_bytes(do)
return self.decoded
# not an abstractmethod, as it is only required if no _construct exists
def _from_bytes(self, do: bytes):
raise NotImplementedError('%s._from_bytes' % type(self).__name__)
class IE(Transcodable, metaclass=TlvMeta):
# we specify the metaclass so any downstream subclasses will automatically use it
"""Base class for various Information Elements. We understand the notion of a hierarchy
of IEs on top of the Transcodable class."""
# this is overridden by the TlvMeta metaclass, if it is used to create subclasses
nested_collection_cls = None
tag = None
def __init__(self, **kwargs):
super().__init__()
self.nested_collection = None
if self.nested_collection_cls:
self.nested_collection = self.nested_collection_cls()
# if we are a constructed IE, [ordered] list of actual child-IE instances
self.children = kwargs.get('children', [])
self.decoded = kwargs.get('decoded', None)
def __repr__(self):
"""Return a string representing the [nested] IE data (for print)."""
if len(self.children):
member_strs = [repr(x) for x in self.children]
return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
else:
return '%s(%s)' % (type(self).__name__, self.decoded)
def to_dict(self):
"""Return a JSON-serializable dict representing the [nested] IE data."""
if len(self.children):
v = [x.to_dict() for x in self.children]
else:
v = self.decoded
return {camel_to_snake(type(self).__name__): v}
def from_dict(self, decoded: dict):
"""Set the IE internal decoded representation to data from the argument.
If this is a nested IE, the child IE instance list is re-created."""
expected_key_name = camel_to_snake(type(self).__name__)
if not expected_key_name in decoded:
raise ValueError("Dict %s doesn't contain expected key %s" % (decoded, expected_key_name))
if self.nested_collection:
self.children = self.nested_collection.from_dict(decoded[expected_key_name])
else:
self.children = []
self.decoded = decoded[expected_key_name]
def is_constructed(self):
"""Is this IE constructed by further nested IEs?"""
if len(self.children):
return True
else:
return False
@abc.abstractmethod
def to_ie(self, context: dict = {}) -> bytes:
"""Convert the internal representation to entire IE including IE header."""
def to_bytes(self, context: dict = {}) -> bytes:
"""Convert the internal representation *of the value part* to binary bytes."""
if self.is_constructed():
# concatenate the encoded IE of all children to form the value part
out = b''
for c in self.children:
out += c.to_ie(context=context)
return out
else:
return super().to_bytes(context=context)
def from_bytes(self, do: bytes, context: dict = {}):
"""Parse *the value part* from binary bytes to internal representation."""
if self.nested_collection:
self.children = self.nested_collection.from_bytes(do, context=context)
else:
self.children = []
return super().from_bytes(do, context=context)
class TLV_IE(IE):
"""Abstract base class for various TLV type Information Elements."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
def _compute_tag(self) -> int:
"""Compute the tag (sometimes the tag encodes part of the value)."""
return self.tag
@classmethod
@abc.abstractmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
"""Obtain the raw TAG at the start of the bytes provided by the user."""
@classmethod
@abc.abstractmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
"""Obtain the length encoded at the start of the bytes provided by the user."""
@abc.abstractmethod
def _encode_tag(self) -> bytes:
"""Encode the tag part. Must be provided by derived (TLV format specific) class."""
@abc.abstractmethod
def _encode_len(self, val: bytes) -> bytes:
"""Encode the length part assuming a certain binary value. Must be provided by
derived (TLV format specific) class."""
def to_ie(self, context: dict = {}):
return self.to_tlv(context=context)
def to_tlv(self, context: dict = {}):
"""Convert the internal representation to binary TLV bytes."""
val = self.to_bytes(context=context)
return self._encode_tag() + self._encode_len(val) + val
def from_tlv(self, do: bytes, context: dict = {}):
if len(do) == 0:
return {}, b''
(rawtag, remainder) = self.__class__._parse_tag_raw(do)
if rawtag:
if rawtag != self._compute_tag():
raise ValueError("%s: Encountered tag %s doesn't match our supported tag %s" %
(self, rawtag, self.tag))
(length, remainder) = self.__class__._parse_len(remainder)
value = remainder[:length]
remainder = remainder[length:]
else:
value = do
remainder = b''
dec = self.from_bytes(value, context=context)
return dec, remainder
class BER_TLV_IE(TLV_IE):
"""TLV_IE formatted as ASN.1 BER described in ITU-T X.690 8.1.2."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
@classmethod
def _decode_tag(cls, do: bytes) -> Tuple[dict, bytes]:
return bertlv_parse_tag(do)
@classmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_tag_raw(do)
@classmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_len(do)
def _encode_tag(self) -> bytes:
return bertlv_encode_tag(self._compute_tag())
def _encode_len(self, val: bytes) -> bytes:
return bertlv_encode_len(len(val))
class COMPR_TLV_IE(TLV_IE):
"""TLV_IE formated as COMPREHENSION-TLV as described in ETSI TS 101 220."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.comprehension = False
@classmethod
def _decode_tag(cls, do: bytes) -> Tuple[dict, bytes]:
return comprehensiontlv_parse_tag(do)
@classmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
return comprehensiontlv_parse_tag_raw(do)
@classmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_len(do)
def _encode_tag(self) -> bytes:
return comprehensiontlv_encode_tag(self._compute_tag())
def _encode_len(self, val: bytes) -> bytes:
return bertlv_encode_len(len(val))
class TLV_IE_Collection(metaclass=TlvCollectionMeta):
# we specify the metaclass so any downstream subclasses will automatically use it
"""A TLV_IE_Collection consists of multiple TLV_IE classes identified by their tags.
A given encoded DO may contain any of them in any order, and may contain multiple instances
of each DO."""
# this is overridden by the TlvCollectionMeta metaclass, if it is used to create subclasses
possible_nested = []
def __init__(self, desc=None, **kwargs):
self.desc = desc
#print("possible_nested: ", self.possible_nested)
self.members = kwargs.get('nested', self.possible_nested)
self.members_by_tag = {}
self.members_by_name = {}
self.members_by_tag = {m.tag: m for m in self.members}
self.members_by_name = {camel_to_snake(m.__name__): m for m in self.members}
# if we are a constructed IE, [ordered] list of actual child-IE instances
self.children = kwargs.get('children', [])
self.encoded = None
def __str__(self):
member_strs = [str(x) for x in self.members]
return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
def __repr__(self):
member_strs = [repr(x) for x in self.members]
return '%s(%s)' % (self.__class__, ','.join(member_strs))
def __add__(self, other):
"""Extending TLV_IE_Collections with other TLV_IE_Collections or TLV_IEs."""
if isinstance(other, TLV_IE_Collection):
# adding one collection to another
members = self.members + other.members
return TLV_IE_Collection(self.desc, nested=members)
elif inspect.isclass(other) and issubclass(other, TLV_IE):
# adding a member to a collection
return TLV_IE_Collection(self.desc, nested=self.members + [other])
else:
raise TypeError
def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
"""Create a list of TLV_IEs from the collection based on binary input data.
Args:
binary : binary bytes of encoded data
Returns:
list of instances of TLV_IE sub-classes containing parsed data
"""
self.encoded = binary
# list of instances of TLV_IE collection member classes appearing in the data
res = []
remainder = binary
first = next(iter(self.members_by_tag.values()))
# iterate until no binary trailer is left
while len(remainder):
context['siblings'] = res
# obtain the tag at the start of the remainder
tag, r = first._parse_tag_raw(remainder)
if tag == None:
break
if tag in self.members_by_tag:
cls = self.members_by_tag[tag]
# create an instance and parse accordingly
inst = cls()
dec, remainder = inst.from_tlv(remainder, context=context)
res.append(inst)
else:
# unknown tag; create the related class on-the-fly using the same base class
name = 'unknown_%s_%X' % (first.__base__.__name__, tag)
cls = type(name, (first.__base__,), {'tag': tag, 'possible_nested': [],
'nested_collection_cls': None})
cls._from_bytes = lambda s, a: {'raw': a.hex()}
cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
# create an instance and parse accordingly
inst = cls()
dec, remainder = inst.from_tlv(remainder, context=context)
res.append(inst)
self.children = res
return res
def from_dict(self, decoded: List[dict]) -> List[TLV_IE]:
"""Create a list of TLV_IE instances from the collection based on an array
of dicts, where they key indicates the name of the TLV_IE subclass to use."""
# list of instances of TLV_IE collection member classes appearing in the data
res = []
# iterate over members of the list passed into "decoded"
for i in decoded:
# iterate over all the keys (typically one!) within the current list item dict
for k in i.keys():
# check if we have a member identified by the dict key
if k in self.members_by_name:
# resolve the class for that name; create an instance of it
cls = self.members_by_name[k]
inst = cls()
if cls.nested_collection_cls:
# in case of collections, we want to pass the raw "value" portion to from_dict,
# as to_dict() below intentionally omits the collection-class-name as key
inst.from_dict(i[k])
else:
inst.from_dict({k: i[k]})
res.append(inst)
else:
raise ValueError('%s: Unknown TLV Class %s in %s; expected %s' %
(self, k, decoded, self.members_by_name.keys()))
self.children = res
return res
def to_dict(self):
# we intentionally return not a dict, but a list of dicts. We could prefix by
# self.__class__.__name__, but that is usually some meaningless auto-generated collection name.
return [x.to_dict() for x in self.children]
def to_bytes(self, context: dict = {}):
out = b''
context['siblings'] = self.children
for c in self.children:
out += c.to_tlv(context=context)
return out
def from_tlv(self, do, context: dict = {}):
return self.from_bytes(do, context=context)
def to_tlv(self, context: dict = {}):
return self.to_bytes(context=context)
def flatten_dict_lists(inp):
"""hierarchically flatten each list-of-dicts into a single dict. This is useful to
make the output of hierarchical TLV decoder structures flatter and more easy to read."""
def are_all_elements_dict(l):
for e in l:
if not isinstance(e, dict):
return False
return True
def are_elements_unique(lod):
set_of_keys = set([list(x.keys())[0] for x in lod])
return len(lod) == len(set_of_keys)
if isinstance(inp, list):
if are_all_elements_dict(inp) and are_elements_unique(inp):
# flatten into one shared dict
newdict = {}
for e in inp:
key = list(e.keys())[0]
newdict[key] = e[key]
inp = newdict
# process result as any native dict
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
else:
return [flatten_dict_lists(x) for x in inp]
elif isinstance(inp, dict):
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
else:
return inp

View File

@@ -8,10 +8,10 @@ import abc
import argparse
from typing import Optional, Tuple
from construct import Construct
from osmocom.utils import b2h, h2b, i2h, Hexstr
from pySim.exceptions import *
from pySim.utils import SwHexstr, SwMatchstr, ResTuple, sw_match, parse_command_apdu
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
#
@@ -40,18 +40,6 @@ class ApduTracer:
def trace_response(self, cmd, sw, resp):
pass
def trace_reset(self):
pass
class StdoutApduTracer(ApduTracer):
"""Minimalistic APDU tracer, printing commands to stdout."""
def trace_response(self, cmd, sw, resp):
print("-> %s %s" % (cmd[:10], cmd[10:]))
print("<- %s: %s" % (sw, resp))
def trace_reset(self):
print("-- RESET")
class ProactiveHandler(abc.ABC):
"""Abstract base class representing the interface of some code that handles
the proactive commands, as returned by the card in responses to the FETCH
@@ -69,18 +57,7 @@ class ProactiveHandler(abc.ABC):
"""Default handler for not otherwise handled proactive commands."""
raise NotImplementedError('No handler method for %s' % pcmd.decoded)
def prepare_response(self, pcmd: ProactiveCommand, general_result: str = 'performed_successfully'):
# The Command Details are echoed from the command that has been processed.
(command_details,) = [c for c in pcmd.children if isinstance(c, CommandDetails)]
# invert the device identities
(command_dev_ids,) = [c for c in pcmd.children if isinstance(c, DeviceIdentities)]
rsp_dev_ids = DeviceIdentities()
rsp_dev_ids.from_dict({'device_identities': {
'dest_dev_id': command_dev_ids.decoded['source_dev_id'],
'source_dev_id': command_dev_ids.decoded['dest_dev_id']}})
result = Result()
result.from_dict({'result': {'general_result': general_result, 'additional_information': ''}})
return [command_details, rsp_dev_ids, result]
class LinkBase(abc.ABC):
"""Base class for link/transport to card."""
@@ -90,16 +67,14 @@ class LinkBase(abc.ABC):
self.sw_interpreter = sw_interpreter
self.apdu_tracer = apdu_tracer
self.proactive_handler = proactive_handler
self.apdu_strict = False
@abc.abstractmethod
def __str__(self) -> str:
"""Implementation specific method for printing an information to identify the device."""
@abc.abstractmethod
def _send_apdu(self, apdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the APDU. This method must accept APDUs as defined in
ISO/IEC 7816-3, section 12.1 """
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the PDU."""
def set_sw_interpreter(self, interp):
"""Set an (optional) status word interpreter."""
@@ -125,62 +100,62 @@ class LinkBase(abc.ABC):
"""
@abc.abstractmethod
def _reset_card(self):
"""Resets the card (power down/up)
"""
def reset_card(self):
"""Resets the card (power down/up)
"""
if self.apdu_tracer:
self.apdu_tracer.trace_reset()
return self._reset_card()
def send_apdu(self, apdu: Hexstr) -> ResTuple:
def send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU with minimal processing
Args:
apdu : string of hexadecimal characters (ex. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12.1)
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")
"""
# To make sure that no invalid APDUs can be passed further down into the transport layer, we parse the APDU.
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
if self.apdu_tracer:
self.apdu_tracer.trace_command(apdu)
# Handover APDU to concrete transport layer implementation
(data, sw) = self._send_apdu(apdu)
self.apdu_tracer.trace_command(pdu)
(data, sw) = self._send_apdu_raw(pdu)
if self.apdu_tracer:
self.apdu_tracer.trace_response(apdu, sw, data)
# The APDU case (See aso ISO/IEC 7816-3, table 12) dictates if we should receive a response or not. If we
# receive a response in an APDU case that does not allow the reception of a respnse we print a warning to
# make the user/caller aware of the problem. Since the transaction is over at this point and data was received
# we count it as a successful transaction anyway, even though the spec was violated. The problem is most likely
# caused by a missing Le field in the APDU. This is an error that the caller/user should correct to avoid
# problems at some later point when a different transport protocol or transport layer implementation is used.
# All APDUs passed to this function must comply to ISO/IEC 7816-3, section 12.
if len(data) > 0 and (case == 3 or case == 1):
exeption_str = 'received unexpected response data, incorrect APDU-case ' + \
'(%d, should be %d, missing Le field?)!' % (case, case + 1)
if self.apdu_strict:
raise ValueError(exeption_str)
else:
print('Warning: %s' % exeption_str)
self.apdu_tracer.trace_response(pdu, sw, data)
return (data, sw)
def send_apdu_checksw(self, apdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
def send_apdu(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
data, sw = self.send_apdu_raw(pdu)
# When we have sent the first APDU, the SW may indicate that there are response bytes
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if (sw is not None):
while ((sw[0:2] == '9f') or (sw[0:2] == '61')):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
d, sw = self.send_apdu_raw(pdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
pdu_gr = pdu[0:8] + sw[2:4]
data, sw = self.send_apdu_raw(pdu_gr)
return data, sw
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW
Args:
apdu : string of hexadecimal characters (ex. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12.1)
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:
@@ -188,7 +163,7 @@ class LinkBase(abc.ABC):
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
rv = self.send_apdu(apdu)
rv = self.send_apdu(pdu)
last_sw = rv[1]
while sw == '9000' and sw_match(last_sw, '91xx'):
@@ -207,26 +182,31 @@ class LinkBase(abc.ABC):
pcmd = ProactiveCommand()
parsed = pcmd.from_tlv(h2b(fetch_rv[0]))
print("FETCH: %s (%s)" % (fetch_rv[0], type(parsed).__name__))
result = Result()
if self.proactive_handler:
# Extension point: If this does return a list of TLV objects,
# they could be appended after the Result; if the first is a
# Result, that cuold replace the one built here.
ti_list = self.proactive_handler.receive_fetch_raw(pcmd, parsed)
if not ti_list:
ti_list = self.proactive_handler.prepare_response(pcmd, 'FIXME')
self.proactive_handler.receive_fetch_raw(pcmd, parsed)
result.from_dict({'general_result': 'performed_successfully', 'additional_information': ''})
else:
ti_list = self.proactive_handler.prepare_response(pcmd, 'command_beyond_terminal_capability')
result.from_dict({'general_result': 'command_beyond_terminal_capability', 'additional_information': ''})
# Send response immediately, thus also flushing out any further
# proactive commands that the card already wants to send
#
# Structure as per TS 102 223 V4.4.0 Section 6.8
# The Command Details are echoed from the command that has been processed.
(command_details,) = [c for c in pcmd.decoded.children if isinstance(c, CommandDetails)]
# The Device Identities are fixed. (TS 102 223 V4.0.0 Section 6.8.2)
device_identities = DeviceIdentities()
device_identities.from_dict({'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'})
# Testing hint: The value of tail does not influence the behavior
# of an SJA2 that sent ans SMS, so this is implemented only
# following TS 102 223, and not fully tested.
ti_list_bin = [x.to_tlv() for x in ti_list]
tail = b''.join(ti_list_bin)
tail = command_details.to_tlv() + device_identities.to_tlv() + result.to_tlv()
# Testing hint: In contrast to the above, this part is positively
# essential to get the SJA2 to provide the later parts of a
# multipart SMS in response to an OTA RFM command.
@@ -239,88 +219,55 @@ class LinkBase(abc.ABC):
raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter)
return rv
class LinkBaseTpdu(LinkBase):
# Use the T=0 TPDU format by default as this is the most commonly used transport protocol.
protocol = 0
def set_tpdu_format(self, protocol: int):
"""Set TPDU format. Each transport protocol has its specific TPDU format. This method allows the
concrete transport layer implementation to set the TPDU format it expects. (This method must not be
called by higher layers. Switching the TPDU format does not switch the transport protocol that the
reader uses on the wire)
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:
protocol : number of the transport protocol used. (0 => T=0, 1 => T=1)
"""
self.protocol = protocol
@abc.abstractmethod
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the resulting TPDU. This method must accept TPDUs as defined in
ETSI TS 102 221, section 7.3.1 and 7.3.2, depending on the protocol selected. """
def _send_apdu(self, apdu: Hexstr) -> ResTuple:
"""Transforms APDU into a TPDU and sends it. The response TPDU is returned as APDU back to the caller.
Args:
apdu : string of hexadecimal characters (eg. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12)
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(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
Tuple of (decoded_data, sw)
"""
if self.protocol == 0:
return self.__send_apdu_T0(apdu)
elif self.protocol == 1:
return self.__send_apdu_transparent(apdu)
raise ValueError('unspported protocol selected (T=%d)' % self.protocol)
def __send_apdu_T0(self, apdu: Hexstr) -> ResTuple:
# Transform the given APDU to the T=0 TPDU format and send it. Automatically fetch the response (case #4 APDUs)
# (see also ETSI TS 102 221, section 7.3.1.1)
# Transform APDU to T=0 TPDU (see also ETSI TS 102 221, section 7.3.1)
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
if case == 1:
# Attach an Le field to all case #1 APDUs (see also ETSI TS 102 221, section 7.3.1.1.1)
tpdu = apdu + '00'
elif case == 4:
# Remove the Le field from all case #4 APDUs (see also ETSI TS 102 221, section 7.3.1.1.4)
tpdu = apdu[:-2]
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:
tpdu = apdu
rsp = None
return (rsp, sw)
prev_tpdu = tpdu
data, sw = self.send_tpdu(tpdu)
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.
# When we have sent the first APDU, the SW may indicate that there are response bytes
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
prev_tpdu = tpdu_gr
d, sw = self.send_tpdu(tpdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
data, sw = self.send_tpdu(tpdu_gr)
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)
return data, sw
def __send_apdu_transparent(self, apdu: Hexstr) -> ResTuple:
# In cases where the TPDU format is the same as the APDU format, we may pass the given APDU through without modification
# (This is the case for T=1, see also ETSI TS 102 221, section 7.3.2.0.)
return self.send_tpdu(apdu)
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
"""Add all reader related arguments to the given argparse.Argumentparser instance."""
@@ -328,15 +275,11 @@ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
from pySim.transport.pcsc import PcscSimLink
from pySim.transport.modem_atcmd import ModemATCommandLink
from pySim.transport.calypso import CalypsoSimLink
from pySim.transport.wsrc import WsrcSimLink
SerialSimLink.argparse_add_reader_args(arg_parser)
PcscSimLink.argparse_add_reader_args(arg_parser)
ModemATCommandLink.argparse_add_reader_args(arg_parser)
CalypsoSimLink.argparse_add_reader_args(arg_parser)
WsrcSimLink.argparse_add_reader_args(arg_parser)
arg_parser.add_argument('--apdu-trace', action='store_true',
help='Trace the command/response APDUs exchanged with the card')
return arg_parser
@@ -345,9 +288,6 @@ def init_reader(opts, **kwargs) -> LinkBase:
"""
Init card reader driver
"""
if opts.apdu_trace and not 'apdu_tracer' in kwargs:
kwargs['apdu_tracer'] = StdoutApduTracer()
if opts.pcsc_dev is not None or opts.pcsc_regex is not None:
from pySim.transport.pcsc import PcscSimLink
sl = PcscSimLink(opts, **kwargs)
@@ -357,9 +297,6 @@ def init_reader(opts, **kwargs) -> LinkBase:
elif opts.modem_dev is not None:
from pySim.transport.modem_atcmd import ModemATCommandLink
sl = ModemATCommandLink(opts, **kwargs)
elif opts.wsrc_server_url is not None:
from pySim.transport.wsrc import WsrcSimLink
sl = WsrcSimLink(opts, **kwargs)
else: # Serial reader is default
print("No reader/driver specified; falling back to default (Serial reader)")
from pySim.transport.serial import SerialSimLink

View File

@@ -22,11 +22,10 @@ import socket
import os
import argparse
from typing import Optional
from osmocom.utils import h2b, b2h, Hexstr
from pySim.transport import LinkBaseTpdu
from pySim.exceptions import ReaderError, ProtocolError
from pySim.utils import ResTuple
from pySim.transport import LinkBase
from pySim.exceptions import *
from pySim.utils import h2b, b2h, Hexstr, ResTuple
class L1CTLMessage:
@@ -59,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,12 +69,12 @@ class L1CTLMessageSIM(L1CTLMessage):
L1CTL_SIM_REQ = 0x16
L1CTL_SIM_CONF = 0x17
def __init__(self, tpdu):
super().__init__(self.L1CTL_SIM_REQ)
self.data += tpdu
def __init__(self, pdu):
super(L1CTLMessageSIM, self).__init__(self.L1CTL_SIM_REQ)
self.data += pdu
class CalypsoSimLink(LinkBaseTpdu):
class CalypsoSimLink(LinkBase):
"""Transport Link for Calypso based phones."""
name = 'Calypso-based (OsmocomBB) reader'
@@ -109,7 +108,7 @@ class CalypsoSimLink(LinkBaseTpdu):
rsp = self.sock.recv(exp_len)
return rsp
def _reset_card(self):
def reset_card(self):
# Request FULL reset
req_msg = L1CTLMessageReset()
self.sock.send(req_msg.gen_msg())
@@ -129,10 +128,10 @@ class CalypsoSimLink(LinkBaseTpdu):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ...
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
# Request sending of TPDU
req_msg = L1CTLMessageSIM(h2b(tpdu))
# Request FULL reset
req_msg = L1CTLMessageSIM(h2b(pdu))
self.sock.send(req_msg.gen_msg())
# Read message length first

View File

@@ -17,22 +17,21 @@
#
import logging as log
import serial
import time
import re
import argparse
from typing import Optional
import serial
from osmocom.utils import Hexstr
from pySim.utils import ResTuple
from pySim.transport import LinkBaseTpdu
from pySim.exceptions import ReaderError, ProtocolError
from pySim.utils import Hexstr, ResTuple
from pySim.transport import LinkBase
from pySim.exceptions import *
# HACK: if somebody needs to debug this thing
# log.root.setLevel(log.DEBUG)
class ModemATCommandLink(LinkBaseTpdu):
class ModemATCommandLink(LinkBase):
"""Transport Link for 3GPP TS 27.007 compliant modems."""
name = "modem for Generic SIM Access (3GPP TS 27.007)"
@@ -58,7 +57,7 @@ class ModemATCommandLink(LinkBaseTpdu):
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
@@ -68,9 +67,9 @@ class ModemATCommandLink(LinkBaseTpdu):
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
@@ -92,7 +91,8 @@ class ModemATCommandLink(LinkBaseTpdu):
break
time.sleep(patience)
its += 1
log.debug('Command took %0.6fs (%d cycles a %fs)', time.time() - t_start, its, patience)
log.debug('Command took %0.6fs (%d cycles a %fs)',
time.time() - t_start, its, patience)
if self._echo:
# Skip echo chars
@@ -120,12 +120,13 @@ class ModemATCommandLink(LinkBaseTpdu):
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):
def reset_card(self):
# Reset the modem, just to be sure
if self.send_at_cmd('ATZ') != [b'OK']:
raise ReaderError('Failed to reset the modem')
@@ -134,7 +135,7 @@ class ModemATCommandLink(LinkBaseTpdu):
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 ...
@@ -145,12 +146,12 @@ class ModemATCommandLink(LinkBaseTpdu):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ...
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
# Make sure pdu has upper case hex digits [A-F]
tpdu = tpdu.upper()
pdu = pdu.upper()
# Prepare the command as described in 8.17
cmd = 'AT+CSIM=%d,\"%s\"' % (len(tpdu), tpdu)
cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)
log.debug('Sending command: %s', cmd)
# Send AT+CSIM command to the modem
@@ -164,14 +165,14 @@ class ModemATCommandLink(LinkBaseTpdu):
# Make sure that the response has format: b'+CSIM: %d,\"%s\"'
try:
result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
(_rsp_tpdu_len, rsp_tpdu) = 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_tpdu[:-4].decode().lower()
sw = rsp_tpdu[-4:].decode().lower()
log.debug('Command response: %s, %s', data, sw)
data = rsp_pdu[:-4].decode().lower()
sw = rsp_pdu[-4:].decode().lower()
log.debug('Command response: %s, %s', data, sw)
return data, sw
def __str__(self) -> str:

View File

@@ -19,22 +19,19 @@
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 smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
from osmocom.utils import h2i, i2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
from pySim.transport import LinkBaseTpdu
from pySim.utils import ResTuple
from pySim.transport import LinkBase
from pySim.utils import h2i, i2h, Hexstr, ResTuple
class PcscSimLink(LinkBaseTpdu):
class PcscSimLink(LinkBase):
""" pySim: PCSC reader transport link."""
name = 'PC/SC'
@@ -59,8 +56,6 @@ class PcscSimLink(LinkBaseTpdu):
raise ReaderError('No matching reader found for regex %s' % opts.pcsc_regex)
self._con = self._reader.createConnection()
if not getattr(opts, "pcsc_shared", False):
self._con = ExclusiveConnectCardConnection(self._con)
def __del__(self):
try:
@@ -68,14 +63,15 @@ class PcscSimLink(LinkBaseTpdu):
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):
@@ -84,23 +80,12 @@ class PcscSimLink(LinkBaseTpdu):
# is disconnected
self.disconnect()
# Make card connection and select a suitable communication protocol
self._con.connect()
supported_protocols = self._con.getProtocol();
self.disconnect()
if (supported_protocols & CardConnection.T0_protocol):
protocol = CardConnection.T0_protocol
self.set_tpdu_format(0)
elif (supported_protocols & CardConnection.T1_protocol):
protocol = CardConnection.T1_protocol
self.set_tpdu_format(1)
else:
raise ReaderError('Unsupported card protocol')
self._con.connect(protocol)
except CardConnectionException as exc:
raise ProtocolError() from exc
except NoCardException as exc:
raise NoCardError() from exc
# Explicitly select T=0 communication protocol
self._con.connect(CardConnection.T0_protocol)
except CardConnectionException:
raise ProtocolError()
except NoCardException:
raise NoCardError()
def get_atr(self) -> Hexstr:
return self._con.getATR()
@@ -108,13 +93,17 @@ class PcscSimLink(LinkBaseTpdu):
def disconnect(self):
self._con.disconnect()
def _reset_card(self):
def reset_card(self):
self.disconnect()
self.connect()
return 1
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
data, sw1, sw2 = self._con.transmit(h2i(tpdu))
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
apdu = h2i(pdu)
data, sw1, sw2 = self._con.transmit(apdu)
sw = [sw1, sw2]
# Return value
@@ -131,8 +120,6 @@ access smart card readers, and is available on a variety of operating systems, s
Windows, MacOS X and Linux. Most vendors of smart card readers provide drivers that offer a PC/SC
interface, if not even a generic USB CCID driver is used. You can use a tool like ``pcsc_scan -r``
to obtain a list of readers available on your system. """)
pcsc_group.add_argument('--pcsc-shared', action='store_true',
help='Open PC/SC reaer in SHARED access (default: EXCLUSIVE)')
dev_group = pcsc_group.add_mutually_exclusive_group()
dev_group.add_argument('-p', '--pcsc-device', type=int, dest='pcsc_dev', metavar='PCSC', default=None,
help='Number of PC/SC reader to use for SIM access')

View File

@@ -16,19 +16,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import serial
import time
import os
import argparse
from typing import Optional
import serial
from osmocom.utils import h2b, b2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError
from pySim.transport import LinkBaseTpdu
from pySim.utils import ResTuple
from pySim.transport import LinkBase
from pySim.utils import h2b, b2h, Hexstr, ResTuple
class SerialSimLink(LinkBaseTpdu):
class SerialSimLink(LinkBase):
""" pySim: Transport Link for serial (RS232) based readers included with simcard"""
name = 'Serial'
@@ -52,7 +51,7 @@ class SerialSimLink(LinkBaseTpdu):
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):
@@ -63,7 +62,8 @@ class SerialSimLink(LinkBaseTpdu):
self.reset_card()
if not newcardonly:
return
existing = True
else:
existing = True
except NoCardError:
pass
@@ -86,7 +86,7 @@ class SerialSimLink(LinkBaseTpdu):
# Tolerate a couple of protocol error ... can happen if
# we try when the card is 'half' inserted
pe += 1
if pe > 2:
if (pe > 2):
raise
# Timed out ...
@@ -101,15 +101,15 @@ class SerialSimLink(LinkBaseTpdu):
def disconnect(self):
pass # Nothing to do really ...
def _reset_card(self):
rv = self.__reset_card()
def reset_card(self):
rv = self._reset_card()
if rv == 0:
raise NoCardError()
if rv < 0:
elif rv < 0:
raise ProtocolError()
return rv
def __reset_card(self):
def _reset_card(self):
self._atr = None
rst_meth_map = {
'rts': self._sl.setRTS,
@@ -120,8 +120,8 @@ class SerialSimLink(LinkBaseTpdu):
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
@@ -187,13 +187,13 @@ class SerialSimLink(LinkBaseTpdu):
def _rx_byte(self):
return self._sl.read()
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
tpdu = h2b(tpdu)
data_len = tpdu[4] # P3
pdu = h2b(pdu)
data_len = pdu[4] # P3
# Send first CLASS,INS,P1,P2,P3
self._tx_string(tpdu[0:5])
self._tx_string(pdu[0:5])
# Wait ack which can be
# - INS: Command acked -> go ahead
@@ -201,9 +201,9 @@ class SerialSimLink(LinkBaseTpdu):
# - SW1: The card can apparently proceed ...
while True:
b = self._rx_byte()
if ord(b) == tpdu[1]:
if ord(b) == pdu[1]:
break
if b != '\x60':
elif b != '\x60':
# Ok, it 'could' be SW1
sw1 = b
sw2 = self._rx_byte()
@@ -214,15 +214,15 @@ class SerialSimLink(LinkBaseTpdu):
raise ProtocolError()
# Send data (if any)
if len(tpdu) > 5:
self._tx_string(tpdu[5:])
if len(pdu) > 5:
self._tx_string(pdu[5:])
# Receive data (including SW !)
# length = [P3 - tx_data (=len(tpdu)-len(hdr)) + 2 (SW1//2) ]
to_recv = data_len - len(tpdu) + 5 + 2
# length = [P3 - tx_data (=len(pdu)-len(hdr)) + 2 (SW1//2) ]
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

View File

@@ -1,99 +0,0 @@
# Copyright (C) 2024 Harald Welte <laforge@gnumonks.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from typing import Optional
from osmocom.utils import h2i, i2h, Hexstr, is_hexstr
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
from pySim.transport import LinkBase
from pySim.utils import ResTuple
from pySim.wsrc import WSRC_DEFAULT_PORT_USER
from pySim.wsrc.client_blocking import WsClientBlocking
class UserWsClientBlocking(WsClientBlocking):
def __init__(self, ws_uri: str, **kwargs):
super().__init__('user', ws_uri, **kwargs)
def select_card(self, id_type:str, id_str:str):
rx = self.transceive_json('select_card', {'id_type': id_type, 'id_str': id_str},
'select_card_ack')
return rx
def reset_card(self):
self.transceive_json('reset_req', {}, 'reset_resp')
def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
rx = self.transceive_json('c_apdu', {'command': cmd}, 'r_apdu')
return rx['response'], rx['sw']
class WsrcSimLink(LinkBase):
""" pySim: WSRC (WebSocket Remote Card) reader transport link."""
name = 'WSRC'
def __init__(self, opts: argparse.Namespace, **kwargs):
super().__init__(**kwargs)
self.identities = {}
self.server_url = opts.wsrc_server_url
if opts.wsrc_eid:
self.id_type = 'eid'
self.id_str = opts.wsrc_eid
elif opts.wsrc_iccid:
self.id_type = 'iccid'
self.id_str = opts.wsrc_iccid
self.client = UserWsClientBlocking(self.server_url)
self.client.connect()
def __del__(self):
# FIXME: disconnect from server
pass
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
self.connect()
def connect(self):
rx = self.client.select_card(self.id_type, self.id_str)
self.identities = rx['identities']
def get_atr(self) -> Hexstr:
return h2i(self.identities['ATR'])
def disconnect(self):
self.__delete__()
def _reset_card(self):
self.client.reset_card()
return 1
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
return self.client.xceive_apdu_raw(pdu)
def __str__(self) -> str:
return "WSRC[%s=%s]" % (self.id_type, self.id_str)
@staticmethod
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
wsrc_group = arg_parser.add_argument_group('WebSocket Remote Card',
"""WebSocket Remote Card (WSRC) is a protoocl by which remot cards / card readers
can be accessed via a network.""")
wsrc_group.add_argument('--wsrc-server-url', default='ws://localhost:%u' % WSRC_DEFAULT_PORT_USER,
help='URI of the WSRC server to connect to')
wsrc_group.add_argument('--wsrc-iccid', type=is_hexstr,
help='ICCID of the card to open via WSRC')
wsrc_group.add_argument('--wsrc-eid', type=is_hexstr,
help='EID of the card to open via WSRC')

View File

@@ -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,18 +16,24 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from bidict import bidict
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, ValidationError
from construct import Optional as COptional, Computed
from osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import BER_TLV_IE, flatten_dict_lists
from construct import *
from construct import Optional as COptional
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from bidict import bidict
from pySim.profile import CardProfile
from pySim.profile import match_uicc
from pySim.profile import match_sim
import pySim.iso7816_4 as iso7816_4
#from pySim.filesystem import *
#from pySim.profile import CardProfile
# A UICC will usually also support 2G functionality. If this is the case, we
# need to add DF_GSM and DF_TELECOM along with the UICC related files
from pySim.ts_51_011 import DF_GSM, DF_TELECOM, AddonSIM
from pySim.gsm_r import AddonGSMR
from pySim.cdma_ruim import AddonRUIM
ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
# TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte"
@@ -74,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)
@@ -88,23 +90,27 @@ class TotalFileSize(BER_TLV_IE, tag=0x81):
# ETSI TS 102 221 11.1.1.4.3
class FileDescriptor(BER_TLV_IE, tag=0x82):
_test_decode = [
# FIXME: this doesn't work as _encode test for some strange reason.
( '82027921', { "file_descriptor_byte": { "shareable": True, "structure": "ber_tlv" }, "record_len": None, "num_of_rec": None } ),
]
_test_de_encode = [
( '82027921', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "ber_tlv" }, "record_len": None, "num_of_rec": None } ),
( '82027821', { "file_descriptor_byte": { "shareable": True, "file_type": "df", "structure": "no_info_given" }, "record_len": None, "num_of_rec": None }),
( '82024121', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "transparent" }, "record_len": None, "num_of_rec": None } ),
( '82054221006e05', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "linear_fixed" }, "record_len": 110, "num_of_rec": 5 } ),
]
class BerTlvAdapter(Adapter):
def _decode(self, obj, context, path):
if obj == 0x39:
def _parse(self, obj, context, path):
data = obj.read()
if data == b'\x01\x01\x01\x00\x00\x01':
return 'ber_tlv'
raise ValidationError
def _encode(self, obj, context, path):
def _build(self, obj, context, path):
if obj == 'ber_tlv':
return 0x39
return b'\x01\x01\x01\x00\x00\x01'
raise ValidationError
FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6))), 'file_type'/Computed('working_ef')),
FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6)))),
BitStruct(Const(0, Bit), 'shareable'/Flag, 'file_type'/Enum(BitsInteger(3), working_ef=0, internal_ef=1, df=7),
'structure'/Enum(BitsInteger(3), no_info_given=0, transparent=1, linear_fixed=2, cyclic=6))
)
@@ -125,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)
@@ -163,40 +169,17 @@ class SpecificUiccEnvironmentConditions(BER_TLV_IE, tag=0x88):
class Platform2PlatformCatSecuredApdu(BER_TLV_IE, tag=0x89):
_construct = GreedyBytes
# TS 102 222 Table 4a + 5
class SpecialFileInfo(BER_TLV_IE, tag=0xC0):
_construct = FlagsEnum(Byte, high_update_activity=0x80, readable_and_updatable_when_deactivated=0x40)
# TS 102 222 Table 4a
class FillingPattern(BER_TLV_IE, tag=0xC1):
# The first W-1 bytes of the transparent EF or the first W-1 bytes of each record of a record
# oriented EF shall be initialized with the first W-1 bytes of the Filling Pattern. All remaining
# bytes (if any) shall be initialized with the value of the last byte of the Filling Pattern. If
# the file or record length is shorter than the Filling Pattern, the Filling Pattern shall be
# truncated accordingly.
_construct = GreedyBytes
# TS 102 222 Table 4a
class RepeatPattern(BER_TLV_IE, tag=0xC2):
# The first X bytes of the transparent EF or the first X bytes of each record of a record oriented
# EF shall be initialized with the X bytes of the Repeat Pattern. This shall be repeated
# consecutively for all remaining blocks of X bytes of data in the file or in a record. If
# necessary, the Repeat Pattern shall be truncated at the end of the file or at the end of each
# record to initialize the remaining bytes.
_construct = GreedyBytes
# sysmoISIM-SJA2 specific
class ToolkitAccessConditions(BER_TLV_IE, tag=0xD2):
_construct = FlagsEnum(Byte, rfm_create=1, rfm_delete_terminate=2, other_applet_create=4,
other_applet_delete_terminate=8)
# ETSI TS 102 221 11.1.1.4.6.0 + TS 102 222 Table 4A
# ETSI TS 102 221 11.1.1.4.6.0
class ProprietaryInformation(BER_TLV_IE, tag=0xA5,
nested=[UiccCharacteristics, ApplicationPowerConsumption,
MinApplicationClockFrequency, AvailableMemory,
FileDetails, ReservedFileSize, MaximumFileSize,
SupportedFilesystemCommands, SpecificUiccEnvironmentConditions,
SpecialFileInfo, FillingPattern, RepeatPattern,
ToolkitAccessConditions]):
pass
@@ -251,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):
@@ -282,12 +266,6 @@ class FcpTemplate(BER_TLV_IE, tag=0x62, nested=[FileSize, TotalFileSize, FileDes
PinStatusTemplate_DO]):
pass
def decode_select_response(data_hex: str) -> object:
"""ETSI TS 102 221 Section 11.1.1.3"""
t = FcpTemplate()
t.from_tlv(h2b(data_hex))
d = t.to_dict()
return flatten_dict_lists(d['fcp_template'])
def tlv_key_replace(inmap, indata):
def newkey(inmap, key):
@@ -306,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):
@@ -530,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:]
@@ -568,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
@@ -634,3 +586,323 @@ NOT_DO = Nested_DO('not', 0xaf, NOT_Template)
SC_DO = DataObjectChoice('security_condition', 'Security Condition',
members=[Always_DO, Never_DO, SecCondByte_DO(), SecCondByte_DO(0x9e), CRT_DO(),
OR_DO, AND_DO, NOT_DO])
# TS 102 221 Section 13.1
class EF_DIR(LinFixedEF):
# FIXME: re-encode failure when changing to _test_de_encode
_test_decode = [
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
{ "application_label": "USim1" },
{ "discretionary_template": h2b("a00c80011781025f608203454150") } ] }
),
( '61194f10a0000000871004ffffffff890709000050054953696d31',
{ "application_template": [ { "application_id": h2b("a0000000871004ffffffff8907090000") },
{ "application_label": "ISim1" } ] }
),
]
class ApplicationLabel(BER_TLV_IE, tag=0x50):
# TODO: UCS-2 coding option as per Annex A of TS 102 221
_construct = GreedyString('ascii')
# see https://github.com/PyCQA/pylint/issues/5794
#pylint: disable=undefined-variable
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
iso7816_4.ApplicationRelatedDOSet]):
pass
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
self._tlv = EF_DIR.ApplicationTemplate
# TS 102 221 Section 13.2
class EF_ICCID(TransparentEF):
_test_de_encode = [
( '988812010000400310f0', { "iccid": "8988211000000430010" } ),
]
def __init__(self, fid='2fe2', sfid=0x02, name='EF.ICCID', desc='ICC Identification'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(10, 10))
def _decode_hex(self, raw_hex):
return {'iccid': dec_iccid(raw_hex)}
def _encode_hex(self, abstract):
return enc_iccid(abstract['iccid'])
# TS 102 221 Section 13.3
class EF_PL(TransRecEF):
_test_de_encode = [
( '6465', "de" ),
( '656e', "en" ),
( 'ffff', None ),
]
def __init__(self, fid='2f05', sfid=0x05, name='EF.PL', desc='Preferred Languages'):
super().__init__(fid, sfid=sfid, name=name,
desc=desc, rec_len=2, size=(2, None))
def _decode_record_bin(self, bin_data, **kwargs):
if bin_data == b'\xff\xff':
return None
else:
return bin_data.decode('ascii')
def _encode_record_bin(self, in_json, **kwargs):
if in_json is None:
return b'\xff\xff'
else:
return in_json.encode('ascii')
# TS 102 221 Section 13.4
class EF_ARR(LinFixedEF):
_test_de_encode = [
( '800101a40683010a950108800106900080016097008401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "control_reference_template": "ADM1" } ],
[ { "access_mode": [ "write_append", "update_erase" ] },
{ "always": None } ],
[ { "access_mode": [ "delete_file", "terminate_ef" ] },
{ "never": None } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
( '80010190008001029700800118a40683010a9501088401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "always": None } ],
[ { "access_mode": [ "update_erase" ] },
{ "never": None } ],
[ { "access_mode": [ "activate_file_or_record", "deactivate_file_or_record" ] },
{ "control_reference_template": "ADM1" } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
]
def __init__(self, fid='2f06', sfid=0x06, name='EF.ARR', desc='Access Rule Reference'):
super().__init__(fid, sfid=sfid, name=name, desc=desc)
# add those commands to the general commands of a TransparentEF
self.shell_commands += [self.AddlShellCommands()]
@staticmethod
def flatten(inp: list):
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
def sc_abbreviate(sc):
if 'always' in sc:
return 'always'
elif 'never' in sc:
return 'never'
elif 'control_reference_template' in sc:
return sc['control_reference_template']
else:
return sc
by_mode = {}
for t in inp:
am = t[0]
sc = t[1]
sc_abbr = sc_abbreviate(sc)
if 'access_mode' in am:
for m in am['access_mode']:
by_mode[m] = sc_abbr
elif 'command_header' in am:
ins = am['command_header']['INS']
if 'CLA' in am['command_header']:
cla = am['command_header']['CLA']
else:
cla = None
cmd = ts_102_22x_cmdset.lookup(ins, cla)
if cmd:
name = cmd.name.lower().replace(' ', '_')
by_mode[name] = sc_abbr
else:
raise ValueError
else:
raise ValueError
return by_mode
def _decode_record_bin(self, raw_bin_data, **kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
dec = arr_seq.decode_multi(raw_bin_data)
# we cannot pass the result through flatten() here, as we don't have a related
# 'un-flattening' decoder, and hence would be unable to encode :(
return dec[0]
def _encode_record_bin(self, in_json, **kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
return arr_seq.encode_multi(in_json)
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
def __init__(self):
super().__init__()
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
def do_read_arr_record(self, opts):
"""Read one EF.ARR record in flattened, human-friendly form."""
(data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
data = self._cmd.lchan.selected_file.flatten(data)
self._cmd.poutput_json(data, opts.oneline)
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
def do_read_arr_records(self, opts):
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
# collect all results in list so they are rendered as JSON list when printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
(data, sw) = self._cmd.lchan.read_record_dec(recnr)
data = self._cmd.lchan.selected_file.flatten(data)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
# TS 102 221 Section 13.6
class EF_UMPC(TransparentEF):
_test_de_encode = [
( '3cff02', { "max_current_mA": 60, "t_op_s": 255,
"addl_info": { "req_inc_idle_current": False, "support_uicc_suspend": True } } ),
( '320500', { "max_current_mA": 50, "t_op_s": 5, "addl_info": {"req_inc_idle_current": False,
"support_uicc_suspend": False } } ),
]
def __init__(self, fid='2f08', sfid=0x08, name='EF.UMPC', desc='UICC Maximum Power Consumption'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(5, 5))
addl_info = FlagsEnum(Byte, req_inc_idle_current=1,
support_uicc_suspend=2)
self._construct = Struct(
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
class CardProfileUICC(CardProfile):
ORDER = 1
def __init__(self, name='UICC'):
files = [
EF_DIR(),
EF_ICCID(),
EF_PL(),
EF_ARR(),
# FIXME: DF.CD
EF_UMPC(),
]
addons = [
AddonSIM,
AddonGSMR,
AddonRUIM,
]
sw = {
'Normal': {
'9000': 'Normal ending of the command',
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
},
'Postponed processing': {
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
},
'Warnings': {
'6200': 'No information given, state of non-volatile memory unchanged',
'6281': 'Part of returned data may be corrupted',
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
'6283': 'Selected file invalidated',
'6284': 'Selected file in termination state',
'62f1': 'More data available',
'62f2': 'More data available and proactive command pending',
'62f3': 'Response data available',
'63f1': 'More data expected',
'63f2': 'More data expected and proactive command pending',
'63cx': 'Command successful but after using an internal update retry routine X times',
},
'Execution errors': {
'6400': 'No information given, state of non-volatile memory unchanged',
'6500': 'No information given, state of non-volatile memory changed',
'6581': 'Memory problem',
},
'Checking errors': {
'6700': 'Wrong length',
'67xx': 'The interpretation of this status word is command dependent',
'6b00': 'Wrong parameter(s) P1-P2',
'6d00': 'Instruction code not supported or invalid',
'6e00': 'Class not supported',
'6f00': 'Technical problem, no precise diagnosis',
'6fxx': 'The interpretation of this status word is command dependent',
},
'Functions in CLA not supported': {
'6800': 'No information given',
'6881': 'Logical channel not supported',
'6882': 'Secure messaging not supported',
},
'Command not allowed': {
'6900': 'No information given',
'6981': 'Command incompatible with file structure',
'6982': 'Security status not satisfied',
'6983': 'Authentication/PIN method blocked',
'6984': 'Referenced data invalidated',
'6985': 'Conditions of use not satisfied',
'6986': 'Command not allowed (no EF selected)',
'6989': 'Command not allowed - secure channel - security not satisfied',
},
'Wrong parameters': {
'6a80': 'Incorrect parameters in the data field',
'6a81': 'Function not supported',
'6a82': 'File not found',
'6a83': 'Record not found',
'6a84': 'Not enough memory space',
'6a86': 'Incorrect parameters P1 to P2',
'6a87': 'Lc inconsistent with P1 to P2',
'6a88': 'Referenced data not found',
},
'Application errors': {
'9850': 'INCREASE cannot be performed, max value reached',
'9862': 'Authentication error, application specific',
'9863': 'Security session or association expired',
'9864': 'Minimum UICC suspension time is too long',
},
}
super().__init__(name, desc='ETSI TS 102 221', cla="00",
sel_ctrl="0004", files_in_mf=files, sw=sw,
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
@staticmethod
def decode_select_response(resp_hex: str) -> object:
"""ETSI TS 102 221 Section 11.1.1.3"""
t = FcpTemplate()
t.from_tlv(h2b(resp_hex))
d = t.to_dict()
return flatten_dict_lists(d['fcp_template'])
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_uicc(scc)
@with_default_category('TS 102 221 Specific Commands')
class AddlShellCommands(CommandSet):
suspend_uicc_parser = argparse.ArgumentParser()
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
help='Proposed minimum duration of suspension')
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
help='Proposed maximum duration of suspension')
# not ISO7816-4 but TS 102 221
@cmd2.with_argparser(suspend_uicc_parser)
def do_suspend_uicc(self, opts):
"""Perform the SUSPEND UICC command. Only supported on some UICC (check EF.UMPC)."""
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
max_len_secs=opts.max_duration_secs)
self._cmd.poutput(
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
resume_uicc_parser = argparse.ArgumentParser()
resume_uicc_parser.add_argument('token', type=str, help='Token provided during SUSPEND')
@cmd2.with_argparser(resume_uicc_parser)
def do_resume_uicc(self, opts):
"""Perform the REUSME UICC operation. Only supported on some UICC. Also: A power-cycle
of the card is required between SUSPEND and RESUME, and only very few non-RESUME
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
self._cmd.card._scc.resume_uicc(opts.token)

View File

@@ -18,10 +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 osmocom.utils import b2h, auto_uint8, auto_uint16, is_hexstr
from cmd2 import CommandSet, with_default_category, with_argparser
import argparse
from pySim.exceptions import *
from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
from pySim.ts_102_221 import *
@@ -29,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!')
@@ -42,8 +48,8 @@ class Ts102222Commands(CommandSet):
if not opts.force_delete:
self._cmd.perror("Refusing to permanently delete the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
(_data, _sw) = self._cmd.lchan.scc.delete_file(f.fid)
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.lchan.scc.delete_file(f.fid)
def complete_delete_file(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for DELETE FILE"""
@@ -63,8 +69,8 @@ class Ts102222Commands(CommandSet):
if not opts.force:
self._cmd.perror("Refusing to terminate the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
(_data, _sw) = self._cmd.lchan.scc.terminate_df(f.fid)
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.lchan.scc.terminate_df(f.fid)
def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE DF"""
@@ -79,8 +85,8 @@ class Ts102222Commands(CommandSet):
if not opts.force:
self._cmd.perror("Refusing to terminate the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
(_data, _sw) = self._cmd.lchan.scc.terminate_ef(f.fid)
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.lchan.scc.terminate_ef(f.fid)
def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE EF"""
@@ -98,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=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_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
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):
@@ -143,26 +149,26 @@ class Ts102222Commands(CommandSet):
ShortFileIdentifier(decoded=opts.short_file_id),
]
fcp = FcpTemplate(children=ies)
(_data, _sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
(data, sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
createdf_parser = argparse.ArgumentParser()
createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
createdf_parser._action_groups.pop()
createdf_required = createdf_parser.add_argument_group('required arguments')
createdf_optional = createdf_parser.add_argument_group('optional arguments')
createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments')
createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
createdf_required.add_argument('--ef-arr-record-nr', required=True, type=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')
createdf_sja_optional.add_argument('--permit-other-applet-delete-terminate', action='store_true')
createdf_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
@cmd2.with_argparser(createdf_parser)
def do_create_df(self, opts):
@@ -186,32 +192,32 @@ class Ts102222Commands(CommandSet):
ies.append(TotalFileSize(decoded=opts.total_file_size))
# TODO: Spec states PIN Status Template DO is mandatory
if opts.permit_rfm_create or opts.permit_rfm_delete_terminate or opts.permit_other_applet_create or opts.permit_other_applet_delete_terminate:
toolkit_ac = {
'rfm_create': opts.permit_rfm_create,
'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
'other_applet_create': opts.permit_other_applet_create,
'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
}
ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
toolkit_ac = {
'rfm_create': opts.permit_rfm_create,
'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
'other_applet_create': opts.permit_other_applet_create,
'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
}
ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
fcp = FcpTemplate(children=ies)
(_data, _sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
(data, sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
resize_ef_parser = argparse.ArgumentParser()
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
resize_ef_parser._action_groups.pop()
resize_ef_required = resize_ef_parser.add_argument_group('required arguments')
resize_ef_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
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):
"""Resize an existing EF below the currently selected DF. Requires related privileges."""
f = self._cmd.lchan.get_file_for_filename(opts.NAME)
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
ies = [FileIdentifier(decoded=f.fid),
FileSize(decoded=opts.file_size)]
fcp = FcpTemplate(children=ies)
(_data, _sw) = self._cmd.lchan.scc.resize_file(b2h(fcp.to_tlv()))
(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)

View File

@@ -17,12 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pySim.construct import *
from construct import *
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.tlv import BER_TLV_IE, TLV_IE_Collection
#from pySim.utils import *
from pySim.filesystem import CardDF, TransparentEF
from pySim.tlv import BER_TLV_IE, TLV_IE_Collection
# TS102 310 Section 7.1
class EF_EAPKEYS(TransparentEF):

View 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,30 +26,25 @@ 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, Int24sb, PaddedString, PrefixedArray, If
from osmocom.utils import is_hexstr
from osmocom.tlv import *
from osmocom.construct import *
from pySim.profile.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
from pySim.profile.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
from pySim.profile.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
from pySim.profile.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
from pySim.profile.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
from pySim.profile.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
# Mapping between USIM Service Number and its description
import pySim.ts_102_221
from pySim.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
from pySim.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
from pySim.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
from pySim.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
from pySim.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
from pySim.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
from pySim.ts_102_221 import EF_ARR
from pySim.tlv import *
from pySim.filesystem import *
from pySim.ts_31_102_telecom import DF_PHONEBOOK, EF_UServiceTable
from pySim.ts_31_103_shared import EF_IMSConfigData, EF_XCAPConfigData, EF_MuDMiDConfigData
from pySim.ts_31_103_shared import EF_AC_GBAUAPI, EF_IMSDCI
from pySim.construct import *
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)',
@@ -343,7 +338,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
out.append({k: v})
return out
def _encode_hex(self, in_json, **kwargs):
def _encode_hex(self, in_json):
out_bytes = self._encode_prot_scheme_id_list(
in_json['prot_scheme_id_list'])
d = self._expand_pubkey_list(in_json['hnet_pubkey_list'])
@@ -395,8 +390,8 @@ class EF_SUCI_Calc_Info(TransparentEF):
'hnet_pubkey_list': hnet_pubkey_list
}
def _encode_bin(self, in_json, **kwargs):
return h2b(self._encode_hex(in_json, **kwargs))
def _encode_bin(self, in_json):
return h2b(self._encode_hex(in_json))
class EF_LI(TransRecEF):
@@ -412,7 +407,7 @@ class EF_LI(TransRecEF):
return in_bin.decode('ascii')
def _encode_record_bin(self, in_json, **kwargs):
if in_json is None:
if in_json == None:
return b'\xff\xff'
else:
# officially this is 7-bit GSM alphabet with one padding bit in each byte
@@ -442,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
@@ -452,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
@@ -639,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
@@ -852,7 +853,7 @@ class EF_IPS(CyclicEF):
self._construct = Struct('status'/PaddedString(2, 'ascii'),
'link_to_ef_ipd'/Int8ub, 'rfu'/Byte)
# TS 31.102 Section 4.2.103 (Rel 13)
# TS 31.102 Section 4.2.103
class EF_ePDGId(TransparentEF):
_test_de_encode = [
( '801100657064672e6f736d6f636f6d2e6f7267', {'e_pdg_id': {"type_of_ePDG_address": "FQDN", "ePDG_address" : "epdg.osmocom.org" } } ),
@@ -870,7 +871,7 @@ class EF_ePDGId(TransparentEF):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_ePDGId.ePDGId
# TS 31.102 Section 4.2.104 (Rel 13)
# TS 31.102 Section 4.2.104
class EF_ePDGSelection(TransparentEF):
_test_de_encode = [
( '800600f110000100', {'e_pdg_selection': [{'plmn': '001-01', 'epdg_priority': 1, 'epdg_fqdn_format': 'operator_identified' }] }),
@@ -885,76 +886,20 @@ class EF_ePDGSelection(TransparentEF):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_ePDGSelection.ePDGSelection
# TS 31.102 Section 4.2.106 (Rel 14)
# TS 31.102 Section 4.2.106
class EF_FromPreferred(TransparentEF):
def __init__(self, fid='6ff7', sfid=None, name='EF.FromPreferred', size=(1, 1),
desc='From Preferred', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = BitStruct('rfu'/BitsRFU(7), 'from_preferred'/Flag)
# TS 31.102 Section 4.2.112 + TS 23.032 Section 6.1
GadPoint = Struct('latitude'/Int24sb, 'longitude'/Int24sb)
# TS 31.102 Section 4.2.112 (Rel ??)
class EF_EARFCNList(TransparentEF):
_test_de_encode = [
# single data object with one EARFCN + one area of 3 points
('a01a8004000100008112000001100001000002100002000003100003',
[{'earfcn_list_tlv': [{'earfcn': 65536},
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
{'latitude': 2, 'longitude': 1048578},
{'latitude': 3, 'longitude': 1048579}] }]}] ),
# single data object with one EARFCN + two areas of 3 + 4 points
('a03480040001000081120000011000010000021000020000031000038118000001100001000002100002000003100003000004100004',
[{'earfcn_list_tlv': [{'earfcn': 65536},
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
{'latitude': 2, 'longitude': 1048578},
{'latitude': 3, 'longitude': 1048579}] },
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
{'latitude': 2, 'longitude': 1048578},
{'latitude': 3, 'longitude': 1048579},
{'latitude': 4, 'longitude': 1048580}] }
] }] ),
# two concatenated data objects with 3 points each
('a01a8004000100008112000001100001000002100002000003100003a01a8004000200008112000011100011000012100012000013100013',
[{'earfcn_list_tlv': [{'earfcn': 65536},
{'geographical_area': [{'latitude': 1, 'longitude': 1048577},
{'latitude': 2, 'longitude': 1048578},
{'latitude': 3, 'longitude': 1048579}] }]},
{'earfcn_list_tlv': [{'earfcn': 131072},
{'geographical_area': [{'latitude': 17, 'longitude': 1048593},
{'latitude': 18, 'longitude': 1048594},
{'latitude': 19, 'longitude': 1048595}] }]} ]),
]
class Earfcn(BER_TLV_IE, tag=0x80):
_construct = Int32ub
class GeographicalArea(BER_TLV_IE, tag=0x81):
_construct = GreedyRange(GadPoint)
class EarfcnListTlv(BER_TLV_IE, tag=0xa0, nested=[Earfcn,GeographicalArea]):
pass
# we need a collection as there might be multiple concatenated instances
class EarfcnListTlvCollection(TLV_IE_Collection, nested=[EarfcnListTlv]):
pass
def __init__(self, fid='6ffd', sfid=None, name='EF.EARFCNList', size=(30, 100),
desc='EARFCN list for MTC/NB-IOT UEs', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._tlv = self.EarfcnListTlvCollection
# TS 31.102 Section 4.2.114 (Rel 18)
# TS 31.102 Section 4.2.114
class EF_eAKA(TransparentEF):
def __init__(self, fid='6f01', sfid=None, name='EF.eAKA', size=(1, 1),
desc='enhanced AKA support', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = BitStruct('rfu'/BitsRFU(7), 'enhanced_sqn_calculation_supported'/Flag)
# TS 31.102 Section 4.2.115 (Rel 18)
class EF_OCST(TransparentEF):
def __init__(self, fid='6f02', sfid=None, name='EF.OCST', size=(2, 100),
desc='Operator controlled signal threshold per access technology', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct('sense'/FlagsEnum(Byte, sense_enabled=1),
'ocst_tlv'/GreedyBytes)
######################################################################
# DF.GSM-ACCESS
@@ -1050,10 +995,10 @@ class EF_OCSGL(LinFixedEF):
######################################################################
# DF.5GS (Rel 15)
# DF.5GS
######################################################################
# TS 31.102 Section 4.4.11.2 (Rel 15)
# TS 31.102 Section 4.4.11.2
class EF_5GS3GPPLOCI(TransparentEF):
def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20),
desc='5S 3GP location information', **kwargs):
@@ -1064,7 +1009,7 @@ class EF_5GS3GPPLOCI(TransparentEF):
'last_visited_registered_tai_in_5gs'/HexAdapter(Bytes(6)),
'5gs_update_status'/upd_status_constr)
# TS 31.102 Section 4.4.11.7 (Rel 15)
# TS 31.102 Section 4.4.11.7
class EF_UAC_AIC(TransparentEF):
_test_de_encode = [
( '03', { "uac_access_id_config": { "multimedia_priority_service": True,
@@ -1077,7 +1022,7 @@ class EF_UAC_AIC(TransparentEF):
mission_critical_service=2)
self._construct = Struct('uac_access_id_config'/cfg_constr)
# TS 31.102 Section 4.4.11.9 (Rel 15)
# TS 31.102 Section 4.4.11.9
class EF_OPL5G(LinFixedEF):
def __init__(self, fid='4f08', sfid=0x08, name='EF.OPL5G', desc='5GS Operator PLMN List', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=(10, None), **kwargs)
@@ -1085,7 +1030,7 @@ class EF_OPL5G(LinFixedEF):
'tac_max'/HexAdapter(Bytes(3)))
self._construct = Struct('tai'/Tai, 'pnn_record_id'/Int8ub)
# TS 31.102 Section 4.4.11.10 (Rel 15)
# TS 31.102 Section 4.4.11.10
class EF_SUPI_NAI(TransparentEF):
class NetworkSpecificIdentifier(TLV_IE, tag=0x80):
# RFC 7542 encoded as UTF-8 string
@@ -1107,7 +1052,7 @@ class EF_SUPI_NAI(TransparentEF):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_SUPI_NAI.NAI_TLV_Collection
# TS 31.102 Section 4.4.11.11 (Rel 15)
# TS 31.102 Section 4.4.11.11
class EF_Routing_Indicator(TransparentEF):
def __init__(self, fid='4f0a', sfid=0x0a, name='EF.Routing_Indicator', desc='Routing Indicator', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -1119,7 +1064,7 @@ class EF_Routing_Indicator(TransparentEF):
self._construct = Struct('routing_indicator'/Rpad(BcdAdapter(Bytes(2)), 'f', 2),
'rfu'/HexAdapter(Bytes(2)))
# TS 31.102 Section 4.4.11.13 (Rel 16)
# TS 31.102 Section 4.4.11.13
class EF_TN3GPPSNN(TransparentEF):
class ServingNetworkName(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
@@ -1193,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
@@ -1336,56 +1284,6 @@ class EF_5G_PROSE_UIR(TransparentEF):
# contains TLV structure despite being TransparentEF, not BER-TLV ?!?
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUeToNetworkRelayUE
# TS 31.102 Section 4.4.13.8 (Rel 18)
class EF_5G_PROSE_U2URU(TransparentEF):
class ValidityTimer(BER_TLV_IE, tag=0x85):
_construct = Bytes(5)
class ServedByNGRAN(BER_TLV_IE, tag=0x80):
_construct = GreedyBytes
class NotServedByNGRAN(BER_TLV_IE, tag=0x81):
_construct = GreedyBytes
class DefaultDstL2IdsForRxDisc(BER_TLV_IE, tag=0x99):
_construct = GreedyBytes
class UserInforIdForDiscovery(BER_TLV_IE, tag=0x8e):
_construct = GreedyBytes
class RSCInfoList(BER_TLV_IE, tag=0x8b):
_construct = GreedyBytes
class DefaultDstL2IdsForTxRxDirect(BER_TLV_IE, tag=0x9a):
_construct = GreedyBytes
class ProSeConfigDataForU2URelayUE(BER_TLV_IE, tag=0xa0,
nested=[ValidityTimer, ServedByNGRAN, NotServedByNGRAN,
DefaultDstL2IdsForRxDisc, UserInforIdForDiscovery,
RSCInfoList, DefaultDstL2IdsForTxRxDirect]):
pass
def __init__(self, fid='4f07', sfid=0x07, name='EF.5G_PROSE_U2URU',
desc='5G ProSe configuration data for UE-to-UE relay UE', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_5G_PROSE_U2URU.ProSeConfigDataForU2URelayUE
# TS 31.102 Section 4.4.13.9 (Rel 18)
class EF_5G_PROSE_EU(TransparentEF):
class PKMFAddressInformation(BER_TLV_IE, tag=0x93):
Ipv4AddrList = PrefixedArray(Int8ub, Int32ub)
Ipv6AddrList = PrefixedArray(Int8ub, Bytes(16))
_construct = Struct('flags'/FlagsEnum(Byte, ipv4=1, ipv6=2, fqdn=4),
'ipv4_addr_list'/If(this.flags.ipv4, Ipv4AddrList),
'ipv6_addr_list'/If(this.flags.ipv6, Ipv6AddrList),
'fqdn'/Prefixed(Int8ub, Utf8Adapter(GreedyBytes)))
class ProSeConfigDataForEndUE(BER_TLV_IE, tag=0xa0,
nested=[EF_5G_PROSE_U2URU.ValidityTimer,
EF_5G_PROSE_U2URU.ServedByNGRAN,
EF_5G_PROSE_U2URU.NotServedByNGRAN,
EF_5G_PROSE_U2URU.DefaultDstL2IdsForRxDisc,
EF_5G_PROSE_U2URU.UserInforIdForDiscovery,
EF_5G_PROSE_U2URU.RSCInfoList,
EF_5G_PROSE_U2URU.DefaultDstL2IdsForTxRxDirect,
PKMFAddressInformation]):
pass
def __init__(self, fid='4f08', sfid=0x08, name='EF.5G_PROSE_EU',
desc='5G ProSe configuration data for end UE', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_5G_PROSE_EU.ProSeConfigDataForEndUE
# TS 31.102 Section 4.4.13 (Rel 17)
class DF_5G_ProSe(CardDF):
def __init__(self, fid='5ff0', name='DF.5G_ProSe', desc='Files for 5G ProSe purpose', **kwargs):
@@ -1397,62 +1295,9 @@ class DF_5G_ProSe(CardDF):
EF_5G_PROSE_U2NRU(service=3),
EF_5G_PROSE_RU(service=4),
EF_5G_PROSE_UIR(service=5),
# Rel 18 additions
EF_5G_PROSE_U2URU(service=6),
EF_5G_PROSE_EU(service=7),
]
self.add_files(files)
# TS 31.102 Section 4.4.14.2 (Rel 18)
class EF_5MBSUECONFIG(TransparentEF):
class Plmn(BER_TLV_IE, tag=0x80):
_construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
'nid'/COptional(Bytes(6)))
class Tmgi(BER_TLV_IE, tag=0x81):
TmgiEntry = Struct('tmgi'/Bytes(6),
'usd_fid'/HexAdapter(Bytes(2)),
'service_type'/FlagsEnum(Byte, mbs_service_announcement=1, mbs_user_service=2))
_construct = GreedyRange(TmgiEntry)
class NrArfcnList(BER_TLV_IE, tag=0x82):
_construct = GreedyRange(Bytes(4))
class DNN(BER_TLV_IE, tag=0x83):
_construct = GreedyBytes
class SNSSAI(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
class PduInfoList(BER_TLV_IE, tag=0xa1, nested=[DNN, SNSSAI]):
pass
class Plmn5mbsPreconfiguration(BER_TLV_IE, tag=0xa0,
nested=[Plmn, Tmgi, NrArfcnList, PduInfoList]):
pass
def __init__(self, fid='4f01', sfid=None, name='EF.5MBSUECONFIG',
desc='5MBS UE pre-configuration', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_5MBSUECONFIG.Plmn5mbsPreconfiguration
# TS 31.102 Section 4.4.14.3 (Rel 18)
class EF_5MBSUSD(TransparentEF):
"""There can be any number of these files with undefined FID; the FIDs are contained
in EF.5BMSUECONFIG. FID range is 4f08...4fff"""
class USD(BER_TLV_IE, tag=0x80):
_construct = GreedyBytes
def __init__(self, fid, sfid=None, name='EF.5MBSUSD',
desc='5MBS User Service Description', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = USD
# TS 31.102 Section 4.4.14 (Rel 18)
class DF_5MBSUECONFIG(CardDF):
def __init__(self, fid='5ff1', name='DF.5MBSUECONFIG', desc='', **kwargs):
super().__init__(fid=fid, name=name, desc=desc, **kwargs)
files = [
EF_5MBSUECONFIG(),
# EF_5MBSUSD() wouild have to be dynamically registered based on EF_5MBSUECONFIG content
]
self.add_files(files)
# TS 31.102 Section 4.4.11.18 (Rel 17)
class EF_5GSEDRX(TransparentEF):
def __init__(self, fid='4f10', sfid=0x10, name='EF.5GSEDRX',
@@ -1604,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'),
@@ -1687,7 +1533,7 @@ class ADF_USIM(CardADF):
EF_VGCS('6fb3', None, 'EF.VBS', desc='Voice Broadcast Service', service=58),
EF_VGCSS('6fb4', None, 'EF.VBSS', desc='Voice Broadcast Service Status', service=58),
EF_VGCSCA(service=64),
EF_VGCSCA('6fd5', None, 'EF.VBSCA', desc='Voice Broadcast Service Ciphering Algorithm', service=65),
EF_VGCSCA('6fd5', None, 'EF.VBCSCA', desc='Voice Broadcast Service Ciphering Algorithm', service=65),
EF_GBABP(service=68),
EF_MSK(service=69),
EF_MUK(service=69),
@@ -1717,17 +1563,7 @@ class ADF_USIM(CardADF):
EF_ePDGSelection('6ff6', None, 'EF.ePDGSelectionEm',
desc='ePDG Selection Information for Emergency Services', service=(110, 111)),
EF_FromPreferred(service=114),
EF_IMSConfigData(service=115),
# TODO: EF.TVCONFIG
# TODO: EF.3GPPPSDATAOFF
# TODO: EF.3GPPPSDATAOFFservicelist
EF_XCAPConfigData(service=120),
EF_EARFCNList(service=121),
EF_MuDMiDConfigData(service=134),
EF_eAKA(),
EF_OCST(service=148),
EF_AC_GBAUAPI(service=68),
EF_IMSDCI(service=150),
# FIXME: DF_SoLSA service=23
DF_PHONEBOOK(),
DF_GSM_ACCESS(),
@@ -1740,32 +1576,30 @@ class ADF_USIM(CardADF):
DF_SNPN(service=[143,146]),
DF_5G_ProSe(service=139),
DF_SAIP(),
DF_5MBSUECONFIG(service=147),
]
if has_imsi:
files.append(EF_IMSI(sfid=0x07))
self.add_files(files)
def decode_select_response(self, data_hex):
return CardProfileUICC.decode_select_response(data_hex)
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
def __init__(self):
super().__init__()
authenticate_parser = argparse.ArgumentParser()
authenticate_parser.add_argument('RAND', 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):
@@ -1779,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):
@@ -1791,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):
@@ -1809,8 +1643,7 @@ class ADF_USIM(CardADF):
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
get_id_parser = argparse.ArgumentParser()
get_id_parser.add_argument("--nswo-context", action='store_true',
help='use SUCI 5G Non-Seamless WLAN Offload context')
get_id_parser.add_argument("--nswo-context", action='store_true')
@cmd2.with_argparser(get_id_parser)
def do_get_identity(self, opts):
@@ -1820,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()
@@ -1842,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)

View File

@@ -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 osmocom.tlv import *
from osmocom.construct import *
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):
@@ -59,7 +58,7 @@ class EF_UServiceTable(TransparentEF):
ret[service_nr]['description'] = self.table[service_nr]
return ret
def _encode_bin(self, in_json, **kwargs):
def _encode_bin(self, in_json):
# compute the required binary size
bin_len = 0
for srv in in_json.keys():
@@ -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:

View File

@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
"""
Various constants from 3GPP TS 31.103 V18.1.0
Various constants from 3GPP TS 31.103 V16.1.0
"""
#
# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
# Copyright (C) 2021-2024 Harald Welte <laforge@osmocom.org>
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,16 +22,15 @@ Various constants from 3GPP TS 31.103 V18.1.0
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from construct import Struct, Switch, this, Bytes, GreedyString
from osmocom.utils import *
from osmocom.tlv import *
from osmocom.construct import *
from pySim.filesystem import *
from pySim.profile.ts_51_011 import EF_AD, EF_SMS, EF_SMSS, EF_SMSR, EF_SMSP
from pySim.utils import *
from pySim.tlv import *
from pySim.ts_51_011 import EF_AD, EF_SMS, EF_SMSS, EF_SMSR, EF_SMSP
from pySim.ts_31_102 import ADF_USIM, EF_FromPreferred
from pySim.ts_31_102_telecom import EF_UServiceTable
from pySim.ts_31_103_shared import *
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
import pySim.ts_102_221
from pySim.ts_102_221 import EF_ARR
from pySim.construct import *
# Mapping between ISIM Service Number and its description
EF_IST_map = {
@@ -56,7 +55,6 @@ EF_IST_map = {
19: 'XCAP Configuration Data',
20: 'WebRTC URI',
21: 'MuD and MiD configuration data',
22: 'IMS Data Channel indication',
}
# TS 31.103 Section 4.2.2
@@ -190,6 +188,96 @@ class EF_NAFKCA(LinFixedEF):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_NAFKCA.NafKeyCentreAddress
# TS 31.103 Section 4.2.16
class EF_UICCIARI(LinFixedEF):
class iari(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6fe7', sfid=None, name='EF.UICCIARI', desc='UICC IARI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_UICCIARI.iari
# TS 31.103 Section 4.2.18
class EF_IMSConfigData(BerTlvEF):
class ImsConfigDataEncoding(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Bytes(1))
class ImsConfigData(BER_TLV_IE, tag=0x81):
_construct = GreedyString
# pylint: disable=undefined-variable
class ImsConfigDataCollection(TLV_IE_Collection, nested=[ImsConfigDataEncoding, ImsConfigData]):
pass
def __init__(self, fid='6ff8', sfid=None, name='EF.IMSConfigData', desc='IMS Configuration Data', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_IMSConfigData.ImsConfigDataCollection
# TS 31.103 Section 4.2.19
class EF_XCAPConfigData(BerTlvEF):
class Access(BER_TLV_IE, tag=0x81):
pass
class ApplicationName(BER_TLV_IE, tag=0x82):
pass
class ProviderID(BER_TLV_IE, tag=0x83):
pass
class URI(BER_TLV_IE, tag=0x84):
pass
class XcapAuthenticationUserName(BER_TLV_IE, tag=0x85):
pass
class XcapAuthenticationPassword(BER_TLV_IE, tag=0x86):
pass
class XcapAuthenticationType(BER_TLV_IE, tag=0x87):
pass
class AddressType(BER_TLV_IE, tag=0x88):
pass
class Address(BER_TLV_IE, tag=0x89):
pass
class PDPAuthenticationType(BER_TLV_IE, tag=0x8a):
pass
class PDPAuthenticationName(BER_TLV_IE, tag=0x8b):
pass
class PDPAuthenticationSecret(BER_TLV_IE, tag=0x8c):
pass
class AccessForXCAP(BER_TLV_IE, tag=0x81):
pass
class NumberOfXcapConnParPolicy(BER_TLV_IE, tag=0x82):
_construct = Int8ub
# pylint: disable=undefined-variable
class XcapConnParamsPolicyPart(BER_TLV_IE, tag=0xa1, nested=[Access, ApplicationName, ProviderID, URI,
XcapAuthenticationUserName, XcapAuthenticationPassword,
XcapAuthenticationType, AddressType, Address, PDPAuthenticationType,
PDPAuthenticationName, PDPAuthenticationSecret]):
pass
class XcapConnParamsPolicy(BER_TLV_IE, tag=0xa0, nested=[AccessForXCAP, NumberOfXcapConnParPolicy, XcapConnParamsPolicyPart]):
pass
class XcapConnParamsPolicyDO(BER_TLV_IE, tag=0x80, nested=[XcapConnParamsPolicy]):
pass
def __init__(self, fid='6ffc', sfid=None, name='EF.XCAPConfigData', desc='XCAP Configuration Data', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_XCAPConfigData.XcapConnParamsPolicy
# TS 31.103 Section 4.2.20
class EF_WebRTCURI(TransparentEF):
class uri(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6ffa', sfid=None, name='EF.WebRTCURI', desc='WebRTC URI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_WebRTCURI.uri
# TS 31.103 Section 4.2.21
class EF_MuDMiDConfigData(BerTlvEF):
class MudMidConfigDataEncoding(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Bytes(1))
class MudMidConfigData(BER_TLV_IE, tag=0x81):
_construct = GreedyString
# pylint: disable=undefined-variable
class MudMidConfigDataCollection(TLV_IE_Collection, nested=[MudMidConfigDataEncoding, MudMidConfigData]):
pass
def __init__(self, fid='6ffe', sfid=None, name='EF.MuDMiDConfigData',
desc='MuD and MiD Configuration Data', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_MuDMiDConfigData.MudMidConfigDataCollection
class ADF_ISIM(CardADF):
def __init__(self, aid='a0000000871004', has_fs=True, name='ADF.ISIM', fid=None, sfid=None,
@@ -217,15 +305,13 @@ class ADF_ISIM(CardADF):
EF_XCAPConfigData(service=19),
EF_WebRTCURI(service=20),
EF_MuDMiDConfigData(service=21),
EF_AC_GBAUAPI(service=2),
EF_IMSDCI(service=22),
]
self.add_files(files)
# add those commands to the general commands of a TransparentEF
self.shell_commands += [ADF_USIM.AddlShellCommands()]
def decode_select_response(self, data_hex):
return CardProfileUICC.decode_select_response(data_hex)
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
# TS 31.103 Section 7.1

View File

@@ -1,138 +0,0 @@
"""
Definitions from 3GPP TS 31.103 V18.1.0 which are shared by both USIM (31.102) and ISIM (31.103) and
hence need to be in a separate python module to avoid circular dependencies.
"""
# Copyright (C) 2021-2024 Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from construct import Struct, Switch, Bytes, GreedyString, GreedyBytes, Int8ub, Prefixed, Enum, Byte
from osmocom.tlv import BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import HexAdapter, Utf8Adapter
from pySim.filesystem import *
# TS 31.103 Section 4.2.16
class EF_UICCIARI(LinFixedEF):
class iari(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6fe7', sfid=None, name='EF.UICCIARI', desc='UICC IARI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_UICCIARI.iari
# TS 31.103 Section 4.2.18
class EF_IMSConfigData(BerTlvEF):
class ImsConfigDataEncoding(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Bytes(1))
class ImsConfigData(BER_TLV_IE, tag=0x81):
_construct = GreedyString
# pylint: disable=undefined-variable
class ImsConfigDataCollection(TLV_IE_Collection, nested=[ImsConfigDataEncoding, ImsConfigData]):
pass
def __init__(self, fid='6ff8', sfid=None, name='EF.IMSConfigData', desc='IMS Configuration Data', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_IMSConfigData.ImsConfigDataCollection
# TS 31.103 Section 4.2.19
class EF_XCAPConfigData(BerTlvEF):
class Access(BER_TLV_IE, tag=0x81):
pass
class ApplicationName(BER_TLV_IE, tag=0x82):
pass
class ProviderID(BER_TLV_IE, tag=0x83):
pass
class URI(BER_TLV_IE, tag=0x84):
pass
class XcapAuthenticationUserName(BER_TLV_IE, tag=0x85):
pass
class XcapAuthenticationPassword(BER_TLV_IE, tag=0x86):
pass
class XcapAuthenticationType(BER_TLV_IE, tag=0x87):
pass
class AddressType(BER_TLV_IE, tag=0x88):
pass
class Address(BER_TLV_IE, tag=0x89):
pass
class PDPAuthenticationType(BER_TLV_IE, tag=0x8a):
pass
class PDPAuthenticationName(BER_TLV_IE, tag=0x8b):
pass
class PDPAuthenticationSecret(BER_TLV_IE, tag=0x8c):
pass
class AccessForXCAP(BER_TLV_IE, tag=0x81):
pass
class NumberOfXcapConnParPolicy(BER_TLV_IE, tag=0x82):
_construct = Int8ub
# pylint: disable=undefined-variable
class XcapConnParamsPolicyPart(BER_TLV_IE, tag=0xa1, nested=[Access, ApplicationName, ProviderID, URI,
XcapAuthenticationUserName, XcapAuthenticationPassword,
XcapAuthenticationType, AddressType, Address, PDPAuthenticationType,
PDPAuthenticationName, PDPAuthenticationSecret]):
pass
class XcapConnParamsPolicy(BER_TLV_IE, tag=0xa0, nested=[AccessForXCAP, NumberOfXcapConnParPolicy, XcapConnParamsPolicyPart]):
pass
class XcapConnParamsPolicyDO(BER_TLV_IE, tag=0x80, nested=[XcapConnParamsPolicy]):
pass
def __init__(self, fid='6ffc', sfid=None, name='EF.XCAPConfigData', desc='XCAP Configuration Data', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_XCAPConfigData.XcapConnParamsPolicy
# TS 31.103 Section 4.2.20
class EF_WebRTCURI(LinFixedEF):
class uri(BER_TLV_IE, tag=0x80):
_construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6ffa', sfid=None, name='EF.WebRTCURI', desc='WebRTC URI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_WebRTCURI.uri
# TS 31.103 Section 4.2.21
class EF_MuDMiDConfigData(BerTlvEF):
class MudMidConfigDataEncoding(BER_TLV_IE, tag=0x80):
_construct = HexAdapter(Bytes(1))
class MudMidConfigData(BER_TLV_IE, tag=0x81):
_construct = GreedyString
# pylint: disable=undefined-variable
class MudMidConfigDataCollection(TLV_IE_Collection, nested=[MudMidConfigDataEncoding, MudMidConfigData]):
pass
def __init__(self, fid='6ffe', sfid=None, name='EF.MuDMiDConfigData',
desc='MuD and MiD Configuration Data', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_MuDMiDConfigData.MudMidConfigDataCollection
# TS 31.103 Section 4.2.22
class EF_AC_GBAUAPI(LinFixedEF):
"""The use of this EF is eescribed in 3GPP TS 31.130"""
class AppletNafAccessControl(BER_TLV_IE, tag=0x80):
# the use of Int8ub as length field in Prefixed is strictly speaking incorrect, as it is a BER-TLV
# length field whihc will consume two bytes from length > 127 bytes. However, AIDs and NAF IDs can
# safely be assumed shorter than that
_construct = Struct('aid'/Prefixed(Int8ub, GreedyBytes),
'naf_id'/Prefixed(Int8ub, GreedyBytes))
def __init__(self, fid='6f0a', sfid=None, name='EF.GBAUAPI',
desc='Access Control to GBA_U_API', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_AC_GBAUAPI.AppletNafAccessControl
# TS 31.103 Section 4.2.23
class EF_IMSDCI(TransparentEF):
"""See Management object as defined in 3GPP TS 24.275."""
def __init__(self, fid='6f0b', sfid=None, name='EF.IMSDCI', desc='IMS Data Channel Indication', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._construct = Enum(Byte, ims_dc_not_allowed=0x00,
ims_dc_allowed_after_ims_session=0x01,
ims_dc_allowed_simultaneous_ims_session=0x02)

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