4 Commits

Author SHA1 Message Date
Harald Welte
3c37033eb2 WIP: saip.data_source
Change-Id: Ia6f70ff467ba58024d94742ba5cecd8141b93ad6
2024-03-10 15:26:23 +01:00
Harald Welte
1b2c35149d WIP: global_platform: LOAD and INSTALL [for load] support
Change-Id: I924aaeecbb3a72bdb65eefbff6135e4e9570579e
2024-03-10 15:26:23 +01:00
Harald Welte
e6f3e153b5 setup.py: Expose pySim.esim as package
Change-Id: I524d2b160e743e9a75d08d3bb285ed5781e65c59
2024-03-10 15:26:23 +01:00
Harald Welte
93c402f442 HACK: saip-test.py
Change-Id: I97601e758fd5c5423bb48d3849daf58681a6c5c9
2024-03-10 15:14:28 +01:00
292 changed files with 5447 additions and 54431 deletions

1
.github/FUNDING.yml vendored
View File

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

11
.gitignore vendored
View File

@@ -3,14 +3,3 @@
/docs/_*
/docs/generated
/.cache
/.local
/build
/pySim.egg-info
/smdpp-data/sm-dp-sessions*
dist
tags
smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem
smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.pem
smdpp-data/generated
smdpp-data/certs/dhparam2048.pem

View File

@@ -1,29 +1,16 @@
pySim - Tools for reading, decoding, browsing SIM/USIM/ISIM/HPSIM/eUICC Cards
=============================================================================
pySim - Read, Write and Browse Programmable SIM/USIM/ISIM/HPSIM Cards
=====================================================================
This repository contains a number of Python programs related to working with
subscriber identity modules of cellular networks, including but not limited
to SIM, UICC, USIM, ISIM, HPSIMs and eUICCs.
* `pySim-shell.py` can be used to interactively explore, read and decode contents
of any of the supported card models / card applications. Furthermore, if
you have the credentials to your card (ADM PIN), you can also write to the card,
i.e. edit its contents.
* `pySim-read.py` and `pySim-prog.py` are _legacy_ tools for batch programming
some very common parameters to an entire batch of programmable cards
* `pySim-trace.py` is a tool to do an in-depth decode of SIM card protocol traces
such as those obtained by [Osmocom SIMtrace2](https://osmocom.org/projects/simtrace2/wiki)
or [osmo-qcdiag](https://osmocom.org/projects/osmo-qcdiag/wiki).
* `osmo-smdpp.py` is a proof-of-concept GSMA SGP.22 Consumer eSIM SM-DP+ for lab/research
* there are more related tools, particularly in the `contrib` directory.
This repository contains a number of Python programs that can be used
to read, program (write) and browse all fields/parameters/files on
SIM/USIM/ISIM/HPSIM cards used in 3GPP cellular networks from 2G to 5G.
Note that the access control configuration of normal production cards
issue by operators will restrict significantly which files a normal
user can read, and particularly write to.
The full functionality of pySim hence can only be used with on so-called
programmable SIM/USIM/ISIM/HPSIM cards, such as the various
[sysmocom programmable card products](https://shop.sysmocom.de/SIM/).
programmable SIM/USIM/ISIM/HPSIM cards.
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
issued by regular commercial operators - come with the kind of keys that
@@ -62,9 +49,9 @@ pySim-shell vs. legacy tools
----------------------------
While you will find a lot of online resources still describing the use of
`pySim-prog.py` and `pySim-read.py`, those tools are considered legacy by
pySim-prog.py and pySim-read.py, those tools are considered legacy by
now and have by far been superseded by the much more capable
`pySim-shell.py`. We strongly encourage users to adopt pySim-shell, unless
pySim-shell. We strongly encourage users to adopt pySim-shell, unless
they have very specific requirements like batch programming of large
quantities of cards, which is about the only remaining use case for the
legacy tools.
@@ -90,7 +77,7 @@ Please install the following dependencies:
- cmd2 >= 1.5.0
- colorlog
- construct >= 2.9.51
- pyosmocom
- gsm0338
- jsonpath-ng
- packaging
- pycryptodomex
@@ -136,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

@@ -17,7 +17,7 @@
import copy
import argparse
from pySim.esim import es2p, ActivationCode
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'
@@ -73,11 +73,6 @@ if __name__ == '__main__':
res = peer.call_downloadOrder(data)
elif opts.command == 'confirm-order':
res = peer.call_confirmOrder(data)
matchingId = res.get('matchingId', None)
smdpAddress = res.get('smdpAddress', None)
if matchingId:
ac = ActivationCode(smdpAddress, matchingId, cc_required=bool(opts.confirmationCode))
print("Activation Code: '%s'" % ac.to_string())
elif opts.command == 'cancel-order':
res = peer.call_cancelOrder(data)
elif opts.command == 'release-profile':

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,48 +0,0 @@
#!/usr/bin/env python3
# Small command line utility program to encode eSIM QR-Codes
# (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 sys
import argparse
from pySim.esim import ActivationCode
option_parser = argparse.ArgumentParser(description="""
eSIM QR code generator. Will encode the given hostname + activation code
into the eSIM RSP String format as specified in SGP.22 Section 4.1. If
a PNG output file is specified, it will also generate a QR code.""")
option_parser.add_argument('hostname', help='FQDN of SM-DP+')
option_parser.add_argument('token', help='MatchingID / Token')
option_parser.add_argument('--oid', help='SM-DP+ OID in CERT.DPauth.ECDSA')
option_parser.add_argument('--confirmation-code-required', action='store_true',
help='Whether a Confirmation Code is required')
option_parser.add_argument('--png', help='Output PNG file name (no PNG is written if omitted)')
if __name__ == '__main__':
opts = option_parser.parse_args()
ac = ActivationCode(opts.hostname, opts.token, opts.oid, opts.confirmation_code_required)
print(ac.to_string())
if opts.png:
with open(opts.png, 'wb') as f:
img = ac.to_qrcode()
img.save(f)
print("# generated QR code stored to '%s'" % (opts.png))

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

@@ -1,661 +0,0 @@
#!/usr/bin/env python3
"""
Faithfully reproduces the smdpp certs contained in SGP.26_v1.5_Certificates_18_07_2024.zip
available at https://www.gsma.com/solutions-and-impact/technologies/esim/gsma_resources/sgp-26-test-certificate-definition-v1-5/
Only usable for testing, it obviously uses a different CI key.
"""
import os
import binascii
from datetime import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
# Custom OIDs used in certificates
OID_CERTIFICATE_POLICIES_CI = "2.23.146.1.2.1.0" # CI cert policy
OID_CERTIFICATE_POLICIES_TLS = "2.23.146.1.2.1.3" # DPtls cert policy
OID_CERTIFICATE_POLICIES_AUTH = "2.23.146.1.2.1.4" # DPauth cert policy
OID_CERTIFICATE_POLICIES_PB = "2.23.146.1.2.1.5" # DPpb cert policy
# Subject Alternative Name OIDs
OID_CI_RID = "2.999.1" # CI Registered ID
OID_DP_RID = "2.999.10" # DP+ Registered ID
OID_DP2_RID = "2.999.12" # DP+2 Registered ID
OID_DP4_RID = "2.999.14" # DP+4 Registered ID
OID_DP8_RID = "2.999.18" # DP+8 Registered ID
class SimplifiedCertificateGenerator:
def __init__(self):
self.backend = default_backend()
# Store generated CI keys to sign other certs
self.ci_certs = {} # {"BRP": cert, "NIST": cert}
self.ci_keys = {} # {"BRP": key, "NIST": key}
def get_curve(self, curve_type):
"""Get the appropriate curve object."""
if curve_type == "BRP":
return ec.BrainpoolP256R1()
else:
return ec.SECP256R1()
def generate_key_pair(self, curve):
"""Generate a new EC key pair."""
private_key = ec.generate_private_key(curve, self.backend)
return private_key
def load_private_key_from_hex(self, hex_key, curve):
"""Load EC private key from hex string."""
key_bytes = binascii.unhexlify(hex_key.replace(":", "").replace(" ", "").replace("\n", ""))
key_int = int.from_bytes(key_bytes, 'big')
return ec.derive_private_key(key_int, curve, self.backend)
def generate_ci_cert(self, curve_type):
"""Generate CI certificate for either BRP or NIST curve."""
curve = self.get_curve(curve_type)
private_key = self.generate_key_pair(curve)
# Build subject and issuer (self-signed) - same for both
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, "Test CI"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "TESTCERT"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSPTEST"),
x509.NameAttribute(NameOID.COUNTRY_NAME, "IT"),
])
# Build certificate - all parameters same for both
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(issuer)
builder = builder.not_valid_before(datetime(2020, 4, 1, 8, 27, 51))
builder = builder.not_valid_after(datetime(2055, 4, 1, 8, 27, 51))
builder = builder.serial_number(0xb874f3abfa6c44d3)
builder = builder.public_key(private_key.public_key())
# Add extensions - all same for both
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier(OID_CERTIFICATE_POLICIES_CI),
policy_qualifiers=None
)
]),
critical=True
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=False,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.RegisteredID(x509.ObjectIdentifier(OID_CI_RID))
]),
critical=False
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
),
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
certificate = builder.sign(private_key, hashes.SHA256(), self.backend)
self.ci_keys[curve_type] = private_key
self.ci_certs[curve_type] = certificate
return certificate, private_key
def generate_dp_cert(self, curve_type, subject_cn, serial, key_hex,
cert_policy_oid, rid_oid, validity_start, validity_end):
"""Generate a DP certificate signed by CI - works for both BRP and NIST."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
ci_cert = self.ci_certs[curve_type]
ci_key = self.ci_keys[curve_type]
subject = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ACME"),
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ci_cert.subject)
builder = builder.not_valid_before(validity_start)
builder = builder.not_valid_after(validity_end)
builder = builder.serial_number(serial)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.RegisteredID(x509.ObjectIdentifier(rid_oid))
]),
critical=False
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier(cert_policy_oid),
policy_qualifiers=None
)
]),
critical=True
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
),
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
return certificate, private_key
def generate_tls_cert(self, curve_type, subject_cn, dns_name, serial, key_hex,
rid_oid, validity_start, validity_end):
"""Generate a TLS certificate signed by CI."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
ci_cert = self.ci_certs[curve_type]
ci_key = self.ci_keys[curve_type]
subject = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ACME"),
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ci_cert.subject)
builder = builder.not_valid_before(validity_start)
builder = builder.not_valid_after(validity_end)
builder = builder.serial_number(serial)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
]),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier(OID_CERTIFICATE_POLICIES_TLS),
policy_qualifiers=None
)
]),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.DNSName(dns_name),
x509.RegisteredID(x509.ObjectIdentifier(rid_oid))
]),
critical=False
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
),
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
return certificate, private_key
def generate_eum_cert(self, curve_type, key_hex):
"""Generate EUM certificate signed by CI."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
ci_cert = self.ci_certs[curve_type]
ci_key = self.ci_keys[curve_type]
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "ES"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
x509.NameAttribute(NameOID.COMMON_NAME, "EUM Test"),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ci_cert.subject)
builder = builder.not_valid_before(datetime(2020, 4, 1, 9, 28, 37))
builder = builder.not_valid_after(datetime(2054, 3, 24, 9, 28, 37))
builder = builder.serial_number(0x12345678)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=False,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier("2.23.146.1.2.1.2"), # EUM policy
policy_qualifiers=None
)
]),
critical=True
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.RegisteredID(x509.ObjectIdentifier("2.999.5"))
]),
critical=False
)
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0),
critical=True
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
# Name Constraints
constrained_name = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
x509.NameAttribute(NameOID.SERIAL_NUMBER, "89049032"),
])
name_constraints = x509.NameConstraints(
permitted_subtrees=[
x509.DirectoryName(constrained_name)
],
excluded_subtrees=None
)
builder = builder.add_extension(
name_constraints,
critical=True
)
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
return certificate, private_key
def generate_euicc_cert(self, curve_type, eum_cert, eum_key, key_hex):
"""Generate eUICC certificate signed by EUM."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "ES"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
x509.NameAttribute(NameOID.SERIAL_NUMBER, "89049032123451234512345678901235"),
x509.NameAttribute(NameOID.COMMON_NAME, "Test eUICC"),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(eum_cert.subject)
builder = builder.not_valid_before(datetime(2020, 4, 1, 9, 48, 58))
builder = builder.not_valid_after(datetime(7496, 1, 24, 9, 48, 58))
builder = builder.serial_number(0x0200000000000001)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(eum_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier("2.23.146.1.2.1.1"), # eUICC policy
policy_qualifiers=None
)
]),
critical=True
)
certificate = builder.sign(eum_key, hashes.SHA256(), self.backend)
return certificate, private_key
def save_cert_and_key(self, cert, key, cert_path_der, cert_path_pem, key_path_sk, key_path_pk):
"""Save certificate and key in various formats."""
# Create directories if needed
os.makedirs(os.path.dirname(cert_path_der), exist_ok=True)
with open(cert_path_der, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.DER))
if cert_path_pem:
with open(cert_path_pem, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
if key and key_path_sk:
with open(key_path_sk, "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
if key and key_path_pk:
with open(key_path_pk, "wb") as f:
f.write(key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
))
def main():
gen = SimplifiedCertificateGenerator()
output_dir = "smdpp-data/generated"
os.makedirs(output_dir, exist_ok=True)
print("=== Generating CI Certificates ===")
for curve_type in ["BRP", "NIST"]:
ci_cert, ci_key = gen.generate_ci_cert(curve_type)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
ci_cert, ci_key,
f"{output_dir}/CertificateIssuer/CERT_CI{suffix}.der",
f"{output_dir}/CertificateIssuer/CERT_CI{suffix}.pem",
None, None
)
print(f"Generated CI {curve_type} certificate")
print("\n=== Generating DPauth Certificates ===")
dpauth_configs = [
("BRP", "TEST SM-DP+", 256, "93:fb:33:d0:58:4f:34:9b:07:f8:b5:d2:af:93:d7:c3:e3:54:b3:49:a3:b9:13:50:2e:6a:bc:07:0e:4d:49:29", OID_DP_RID, "DPauth"),
("NIST", "TEST SM-DP+", 256, "0a:7c:c1:c2:44:e6:0c:52:cd:5b:78:07:ab:8c:36:0c:26:52:46:01:50:7d:ca:bc:5d:d5:98:b5:a6:16:d5:d5", OID_DP_RID, "DPauth"),
("BRP", "TEST SM-DP+2", 512, "0c:17:35:5c:01:1d:0f:e8:d7:da:dd:63:f1:97:85:cf:6c:51:cb:cd:46:6a:e8:8b:e8:f8:1b:c1:05:88:46:f6", OID_DP2_RID, "DP2auth"),
("NIST", "TEST SM-DP+2", 512, "9c:32:a0:95:d4:88:42:d9:ff:a4:04:f7:12:51:2a:a2:c5:42:5a:1a:26:38:6a:b6:a1:45:d5:81:1e:03:91:41", OID_DP2_RID, "DP2auth"),
]
for curve_type, cn, serial, key_hex, rid_oid, name_prefix in dpauth_configs:
cert, key = gen.generate_dp_cert(
curve_type, cn, serial, key_hex,
OID_CERTIFICATE_POLICIES_AUTH, rid_oid,
datetime(2020, 4, 1, 8, 31, 30),
datetime(2030, 3, 30, 8, 31, 30)
)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/DPauth/CERT_S_SM_{name_prefix}{suffix}.der",
None,
f"{output_dir}/DPauth/SK_S_SM_{name_prefix}{suffix}.pem",
f"{output_dir}/DPauth/PK_S_SM_{name_prefix}{suffix}.pem"
)
print(f"Generated {name_prefix} {curve_type} certificate")
print("\n=== Generating DPpb Certificates ===")
dppb_configs = [
("BRP", "TEST SM-DP+", 257, "75:ff:32:2f:41:66:16:da:e1:a4:84:ef:71:d4:87:4f:b0:df:32:95:fd:35:c2:cb:a4:89:fb:b2:bb:9c:7b:f6", OID_DP_RID, "DPpb"),
("NIST", "TEST SM-DP+", 257, "dc:d6:94:b7:78:95:7e:8e:9a:dd:bd:d9:44:33:e9:ef:8f:73:d1:1e:49:1c:48:d4:25:a3:8a:94:91:bd:3b:ed", OID_DP_RID, "DPpb"),
("BRP", "TEST SM-DP+2", 513, "9c:ae:2e:1a:56:07:a9:d5:78:38:2e:ee:93:2e:25:1f:52:30:4f:86:ee:b1:f1:70:8c:db:d3:c0:7b:e2:cd:3d", OID_DP2_RID, "DP2pb"),
("NIST", "TEST SM-DP+2", 513, "66:93:11:49:63:9d:ba:ac:1d:c3:d3:06:c5:8b:d2:df:d2:2f:73:bf:63:ac:86:31:98:32:90:b5:7f:90:93:45", OID_DP2_RID, "DP2pb"),
]
for curve_type, cn, serial, key_hex, rid_oid, name_prefix in dppb_configs:
cert, key = gen.generate_dp_cert(
curve_type, cn, serial, key_hex,
OID_CERTIFICATE_POLICIES_PB, rid_oid,
datetime(2020, 4, 1, 8, 34, 46),
datetime(2030, 3, 30, 8, 34, 46)
)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/DPpb/CERT_S_SM_{name_prefix}{suffix}.der",
None,
f"{output_dir}/DPpb/SK_S_SM_{name_prefix}{suffix}.pem",
f"{output_dir}/DPpb/PK_S_SM_{name_prefix}{suffix}.pem"
)
print(f"Generated {name_prefix} {curve_type} certificate")
print("\n=== Generating DPtls Certificates ===")
dptls_configs = [
("BRP", "testsmdpplus1.example.com", "testsmdpplus1.example.com", 9, "3f:67:15:28:02:b3:f4:c7:fa:e6:79:58:55:f6:82:54:1e:45:e3:5e:ff:f4:e8:a0:55:65:a0:f1:91:2a:78:2e", OID_DP_RID, "DP_TLS_BRP"),
("NIST", "testsmdpplus1.example.com", "testsmdpplus1.example.com", 9, "a0:3e:7c:e4:55:04:74:be:a4:b7:a8:73:99:ce:5a:8c:9f:66:1b:68:0f:94:01:39:ff:f8:4e:9d:ec:6a:4d:8c", OID_DP_RID, "DP_TLS_NIST"),
("NIST", "testsmdpplus2.example.com", "testsmdpplus2.example.com", 12, "4e:65:61:c6:40:88:f6:69:90:7a:db:e3:94:b1:1a:84:24:2e:03:3a:82:a8:84:02:31:63:6d:c9:1b:4e:e3:f5", OID_DP2_RID, "DP2_TLS"),
("NIST", "testsmdpplus4.example.com", "testsmdpplus4.example.com", 14, "f2:65:9d:2f:52:8f:4b:11:37:40:d5:8a:0d:2a:f3:eb:2b:48:e1:22:c2:b6:0a:6a:f6:fc:96:ad:86:be:6f:a4", OID_DP4_RID, "DP4_TLS"),
("NIST", "testsmdpplus8.example.com", "testsmdpplus8.example.com", 18, "ff:6e:4a:50:9b:ad:db:38:10:88:31:c2:3c:cc:2d:44:30:7a:f2:81:e9:25:96:7f:8c:df:1d:95:54:a0:28:8d", OID_DP8_RID, "DP8_TLS"),
]
for curve_type, cn, dns, serial, key_hex, rid_oid, name_prefix in dptls_configs:
cert, key = gen.generate_tls_cert(
curve_type, cn, dns, serial, key_hex, rid_oid,
datetime(2024, 7, 9, 15, 29, 36),
datetime(2025, 8, 11, 15, 29, 36)
)
gen.save_cert_and_key(
cert, key,
f"{output_dir}/DPtls/CERT_S_SM_{name_prefix}.der",
None,
f"{output_dir}/DPtls/SK_S_SM_{name_prefix.replace('_BRP', '_BRP').replace('_NIST', '_NIST')}.pem",
f"{output_dir}/DPtls/PK_S_SM_{name_prefix.replace('_BRP', '_BRP').replace('_NIST', '_NIST')}.pem"
)
print(f"Generated {name_prefix} certificate")
print("\n=== Generating EUM Certificates ===")
eum_configs = [
("BRP", "12:9b:0a:b1:3f:17:e1:4a:40:b6:fa:4e:d8:23:e0:cf:46:5b:7b:3d:73:24:05:e6:29:5d:3b:23:b0:45:c9:9a"),
("NIST", "25:e6:75:77:28:e1:e9:51:13:51:9c:dc:34:55:5c:29:ba:ed:23:77:3a:c5:af:dd:dc:da:d9:84:89:8a:52:f0"),
]
eum_certs = {}
eum_keys = {}
for curve_type, key_hex in eum_configs:
cert, key = gen.generate_eum_cert(curve_type, key_hex)
eum_certs[curve_type] = cert
eum_keys[curve_type] = key
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/EUM/CERT_EUM{suffix}.der",
None,
f"{output_dir}/EUM/SK_EUM{suffix}.pem",
f"{output_dir}/EUM/PK_EUM{suffix}.pem"
)
print(f"Generated EUM {curve_type} certificate")
print("\n=== Generating eUICC Certificates ===")
euicc_configs = [
("BRP", "8d:c3:47:a7:6d:b7:bd:d6:22:2d:d7:5e:a1:a1:68:8a:ca:81:1e:4c:bc:6a:7f:6a:ef:a4:b2:64:19:62:0b:90"),
("NIST", "11:e1:54:67:dc:19:4f:33:71:83:e4:60:c9:f6:32:60:09:1e:12:e8:10:26:cd:65:61:e1:7c:6d:85:39:cc:9c"),
]
for curve_type, key_hex in euicc_configs:
cert, key = gen.generate_euicc_cert(curve_type, eum_certs[curve_type], eum_keys[curve_type], key_hex)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/eUICC/CERT_EUICC{suffix}.der",
None,
f"{output_dir}/eUICC/SK_EUICC{suffix}.pem",
f"{output_dir}/eUICC/PK_EUICC{suffix}.pem"
)
print(f"Generated eUICC {curve_type} certificate")
print("\n=== Certificate generation complete! ===")
print(f"All certificates saved to: {output_dir}/")
if __name__ == "__main__":
main()

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,19 +45,10 @@ case "$JOB_TYPE" in
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim tests/unittests/*.py *.py \
contrib/*.py
pySim tests/*.py *.py \
contrib/es2p_client.py
;;
"docs")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt
# XXX: workaround for https://github.com/python-cmd2/cmd2/issues/1414
# 2.4.3 was the last stable release not affected by this bug (OS#6776)
pip install cmd2==2.4.3
rm -rf docs/_build
make -C "docs" html latexpdf
@@ -98,5 +61,3 @@ case "$JOB_TYPE" in
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
exit 1
esac
osmo-clean-workspace.sh

View File

@@ -1,489 +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
from pathlib import Path as PlPath
from typing import List
from osmocom.utils import h2b, b2h, swap_nibbles
from osmocom.construct import GreedyBytes, StripHeaderAdapter
from pySim.esim.saip import *
from pySim.esim.saip.validation import CheckBasicStructure
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('extract-pe', help='Extract specified PE to (DER encoded) file')
parser_rpe.add_argument('--pe-file', required=True, help='PE file name')
parser_rpe.add_argument('--identification', type=int, help='Extract PE matching specified identification')
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', default=[], type=int, action='append', help='Remove PEs matching specified identification')
parser_rpe.add_argument('--type', default=[], action='append', help='Remove PEs matching specified type')
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_info.add_argument('--apps', action='store_true', help='List applications and their related instances')
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_aapp = subparsers.add_parser('add-app', help='Add application to PE-Sequence')
parser_aapp.add_argument('--output-file', required=True, help='Output file name')
parser_aapp.add_argument('--applet-file', required=True, help='Applet file name')
parser_aapp.add_argument('--aid', required=True, help='Load package AID')
parser_aapp.add_argument('--sd-aid', default=None, help='Security Domain AID')
parser_aapp.add_argument('--non-volatile-code-limit', default=None, type=int, help='Non volatile code limit (C6)')
parser_aapp.add_argument('--volatile-data-limit', default=None, type=int, help='Volatile data limit (C7)')
parser_aapp.add_argument('--non-volatile-data-limit', default=None, type=int, help='Non volatile data limit (C8)')
parser_aapp.add_argument('--hash-value', default=None, help='Hash value')
parser_rapp = subparsers.add_parser('remove-app', help='Remove application from PE-Sequence')
parser_rapp.add_argument('--output-file', required=True, help='Output file name')
parser_rapp.add_argument('--aid', required=True, help='Load package AID')
parser_aappi = subparsers.add_parser('add-app-inst', help='Add application instance to Application PE')
parser_aappi.add_argument('--output-file', required=True, help='Output file name')
parser_aappi.add_argument('--aid', required=True, help='Load package AID')
parser_aappi.add_argument('--class-aid', required=True, help='Class AID')
parser_aappi.add_argument('--inst-aid', required=True, help='Instance AID (must match Load package AID)')
parser_aappi.add_argument('--app-privileges', default='000000', help='Application privileges')
parser_aappi.add_argument('--volatile-memory-quota', default=None, type=int, help='Volatile memory quota (C7)')
parser_aappi.add_argument('--non-volatile-memory-quota', default=None, type=int, help='Non volatile memory quota (C8)')
parser_aappi.add_argument('--app-spec-pars', default='00', help='Application specific parameters (C9)')
parser_aappi.add_argument('--uicc-toolkit-app-spec-pars', help='UICC toolkit application specific parameters field')
parser_aappi.add_argument('--uicc-access-app-spec-pars', help='UICC Access application specific parameters field')
parser_aappi.add_argument('--uicc-adm-access-app-spec-pars', help='UICC Administrative access application specific parameters field')
parser_aappi.add_argument('--process-data', default=[], action='append', help='Process personalization APDUs')
parser_rappi = subparsers.add_parser('remove-app-inst', help='Remove application instance from Application PE')
parser_rappi.add_argument('--output-file', required=True, help='Output file name')
parser_rappi.add_argument('--aid', required=True, help='Load package AID')
parser_rappi.add_argument('--inst-aid', required=True, help='Instance AID')
esrv_flag_choices = [t.name for t in asn1.types['ServicesList'].type.root_members]
parser_esrv = subparsers.add_parser('edit-mand-srv-list', help='Add/Remove service flag from/to mandatory services list')
parser_esrv.add_argument('--output-file', required=True, help='Output file name')
parser_esrv.add_argument('--add-flag', default=[], choices=esrv_flag_choices, action='append', help='Add flag to mandatory services list')
parser_esrv.add_argument('--remove-flag', default=[], choices=esrv_flag_choices, action='append', help='Remove flag from mandatory services list')
parser_info = subparsers.add_parser('tree', help='Display the filesystem tree')
def write_pes(pes: ProfileElementSequence, output_file:str):
"""write the PE sequence to a file"""
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), output_file))
with open(output_file, 'wb') as f:
f.write(pes.to_der())
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_extract_pe(pes: ProfileElementSequence, opts):
new_pe_list = []
for pe in pes.pe_list:
if pe.identification == opts.identification:
print("Extracting PE %s (id=%u) to file %s..." % (pe, pe.identification, opts.pe_file))
with open(opts.pe_file, 'wb') as f:
f.write(pe.to_der())
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
if pe.type in opts.type:
print("Removing PE %s (type=%s) from Sequence..." % (pe, pe.type))
continue
new_pe_list.append(pe)
pes.pe_list = new_pe_list
pes._process_pelist()
write_pes(pes, opts.output_file)
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)
write_pes(pes, opts.output_file)
def info_apps(pes:ProfileElementSequence):
def show_member(dictionary:Optional[dict], member:str, indent:str="\t", mandatory:bool = False, limit:bool = False):
if dictionary is None:
return
value = dictionary.get(member, None)
if value is None and mandatory == True:
print("%s%s: (missing!)" % (indent, member))
return
elif value is None:
return
if limit and len(value) > 40:
print("%s%s: '%s...%s' (%u bytes)" % (indent, member, b2h(value[:20]), b2h(value[-20:]), len(value)))
else:
print("%s%s: '%s' (%u bytes)" % (indent, member, b2h(value), len(value)))
apps = pes.pe_by_type.get('application', [])
if len(apps) == 0:
print("No Application PE present!")
return;
for app_pe in enumerate(apps):
print("Application #%u:" % app_pe[0])
print("\tloadBlock:")
load_block = app_pe[1].decoded['loadBlock']
show_member(load_block, 'loadPackageAID', "\t\t", True)
show_member(load_block, 'securityDomainAID', "\t\t")
show_member(load_block, 'nonVolatileCodeLimitC6', "\t\t")
show_member(load_block, 'volatileDataLimitC7', "\t\t")
show_member(load_block, 'nonVolatileDataLimitC8', "\t\t")
show_member(load_block, 'hashValue', "\t\t")
show_member(load_block, 'loadBlockObject', "\t\t", True, True)
for inst in enumerate(app_pe[1].decoded.get('instanceList', [])):
print("\tinstanceList[%u]:" % inst[0])
show_member(inst[1], 'applicationLoadPackageAID', "\t\t", True)
if inst[1].get('applicationLoadPackageAID', None) != load_block.get('loadPackageAID', None):
print("\t\t(applicationLoadPackageAID should be the same as loadPackageAID!)")
show_member(inst[1], 'classAID', "\t\t", True)
show_member(inst[1], 'instanceAID', "\t\t", True)
show_member(inst[1], 'extraditeSecurityDomainAID', "\t\t")
show_member(inst[1], 'applicationPrivileges', "\t\t", True)
show_member(inst[1], 'lifeCycleState', "\t\t", True)
show_member(inst[1], 'applicationSpecificParametersC9', "\t\t", True)
sys_specific_pars = inst[1].get('systemSpecificParameters', None)
if sys_specific_pars:
print("\t\tsystemSpecificParameters:")
show_member(sys_specific_pars, 'volatileMemoryQuotaC7', "\t\t\t")
show_member(sys_specific_pars, 'nonVolatileMemoryQuotaC8', "\t\t\t")
show_member(sys_specific_pars, 'globalServiceParameters', "\t\t\t")
show_member(sys_specific_pars, 'implicitSelectionParameter', "\t\t\t")
show_member(sys_specific_pars, 'volatileReservedMemory', "\t\t\t")
show_member(sys_specific_pars, 'nonVolatileReservedMemory', "\t\t\t")
show_member(sys_specific_pars, 'ts102226SIMFileAccessToolkitParameter', "\t\t\t")
additional_cl_pars = inst.get('ts102226AdditionalContactlessParameters', None)
if additional_cl_pars:
print("\t\t\tts102226AdditionalContactlessParameters:")
show_member(additional_cl_pars, 'protocolParameterData', "\t\t\t\t")
show_member(sys_specific_pars, 'userInteractionContactlessParameters', "\t\t\t")
show_member(sys_specific_pars, 'cumulativeGrantedVolatileMemory', "\t\t\t")
show_member(sys_specific_pars, 'cumulativeGrantedNonVolatileMemory', "\t\t\t")
app_pars = inst[1].get('applicationParameters', None)
if app_pars:
print("\t\tapplicationParameters:")
show_member(app_pars, 'uiccToolkitApplicationSpecificParametersField', "\t\t\t")
show_member(app_pars, 'uiccAccessApplicationSpecificParametersField', "\t\t\t")
show_member(app_pars, 'uiccAdministrativeAccessApplicationSpecificParametersField', "\t\t\t")
ctrl_ref_tp = inst[1].get('controlReferenceTemplate', None)
if ctrl_ref_tp:
print("\t\tcontrolReferenceTemplate:")
show_member(ctrl_ref_tp, 'applicationProviderIdentifier', "\t\t\t", True)
process_data = inst[1].get('processData', None)
if process_data:
print("\t\tprocessData:")
for proc in process_data:
print("\t\t\t" + b2h(proc))
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
if opts.apps:
info_apps(pes)
return;
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))
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
app_pe.to_file(fname)
def do_add_app(pes:ProfileElementSequence, opts):
print("Applying applet file: '%s'..." % opts.applet_file)
app_pe = ProfileElementApplication.from_file(opts.applet_file,
opts.aid,
opts.sd_aid,
opts.non_volatile_code_limit,
opts.volatile_data_limit,
opts.non_volatile_data_limit,
opts.hash_value)
security_domain = pes.pe_by_type.get('securityDomain', [])
if len(security_domain) == 0:
print("profile package does not contain a securityDomain, please add a securityDomain PE first!")
elif len(security_domain) > 1:
print("adding an application PE to profiles with multiple securityDomain is not supported yet!")
else:
pes.insert_after_pe(security_domain[0], app_pe)
print("application PE inserted into PE Sequence after securityDomain PE AID: %s" %
b2h(security_domain[0].decoded['instance']['instanceAID']))
write_pes(pes, opts.output_file)
def do_remove_app(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
if opts.aid == package_aid:
identification = app_pe.identification
opts_remove_pe = argparse.Namespace()
opts_remove_pe.identification = [app_pe.identification]
opts_remove_pe.type = []
opts_remove_pe.output_file = opts.output_file
print("Found Load Package AID: %s, removing related PE (id=%u) from Sequence..." %
(package_aid, identification))
do_remove_pe(pes, opts_remove_pe)
return
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
def do_add_app_inst(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
if opts.aid == package_aid:
print("Found Load Package AID: %s, adding new instance AID: %s to Application PE..." %
(opts.aid, opts.inst_aid))
app_pe.add_instance(opts.aid,
opts.class_aid,
opts.inst_aid,
opts.app_privileges,
opts.app_spec_pars,
opts.uicc_toolkit_app_spec_pars,
opts.uicc_access_app_spec_pars,
opts.uicc_adm_access_app_spec_pars,
opts.volatile_memory_quota,
opts.non_volatile_memory_quota,
opts.process_data)
write_pes(pes, opts.output_file)
return
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
def do_remove_app_inst(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
if opts.aid == b2h(app_pe.decoded['loadBlock']['loadPackageAID']):
print("Found Load Package AID: %s, removing instance AID: %s from Application PE..." %
(opts.aid, opts.inst_aid))
app_pe.remove_instance(opts.inst_aid)
write_pes(pes, opts.output_file)
return
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
def do_edit_mand_srv_list(pes: ProfileElementSequence, opts):
header = pes.pe_by_type.get('header', [])[0]
for s in opts.add_flag:
print("Adding service '%s' to mandatory services list..." % s)
header.mandatory_service_add(s)
for s in opts.remove_flag:
if s in header.decoded['eUICC-Mandatory-services'].keys():
print("Removing service '%s' from mandatory services list..." % s)
header.mandatory_service_remove(s)
else:
print("Service '%s' not present in mandatory services list, cannot remove!" % s)
print("The following services are now set mandatory:")
for s in header.decoded['eUICC-Mandatory-services'].keys():
print("\t%s" % s)
write_pes(pes, opts.output_file)
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 == 'extract-pe':
do_extract_pe(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 == 'add-app':
do_add_app(pes, opts)
elif opts.command == 'remove-app':
do_remove_app(pes, opts)
elif opts.command == 'add-app-inst':
do_add_app_inst(pes, opts)
elif opts.command == 'remove-app-inst':
do_remove_app_inst(pes, opts)
elif opts.command == 'edit-mand-srv-list':
do_edit_mand_srv_list(pes, opts)
elif opts.command == 'tree':
do_tree(pes, opts)

View File

@@ -1,31 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to add JAVA card applets to an existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE.der
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
APPPATH=./HelloSTK_09122024.cap
# Download example applet (see also https://gitea.osmocom.org/sim-card/hello-stk):
if ! [ -f $APPPATH ]; then
wget https://osmocom.org/attachments/download/8931/HelloSTK_09122024.cap
fi
# Step #1: Create the application PE and load the ijc contents from the .cap file:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH add-app \
--output-file $OUTPATH --applet-file $APPPATH --aid 'D07002CA44'
# Step #2: Create the application instance inside the application PE created in step #1:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH add-app-inst --output-file $OUTPATH \
--aid 'D07002CA44' \
--class-aid 'D07002CA44900101' \
--inst-aid 'D07002CA44900101' \
--app-privileges '00' \
--app-spec-pars '00' \
--uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
# Display the contents of the resulting application PE:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps
# For an explaination of --uicc-toolkit-app-spec-pars, see:
# ETSI TS 102 226, section 8.2.1.3.2.2.1

View File

@@ -1,8 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to extract JAVA card applets from an existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
OUTPATH=./
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH extract-apps --output-dir ./ --format ijc

View File

@@ -1,14 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to remove a JAVA card applet instance from an application PE inside an
# existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello-no-inst.der
# Remove application PE entirely
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH remove-app-inst \
--output-file $OUTPATH --aid 'd07002ca44' --inst-aid 'd07002ca44900101'
# Display the contents of the resulting application PE:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps

View File

@@ -1,13 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to remove a JAVA card applet from an existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-no-hello.der
# Remove application PE entirely
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH remove-app \
--output-file $OUTPATH --aid 'D07002CA44'
# Display the contents of the resulting application PE:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps

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

@@ -1,52 +0,0 @@
#!/usr/bin/env python3
# small utility program to deal with 5G SUCI key material, at least for the ECIES Protection Scheme
# Profile A (curve25519) and B (secp256r1)
# (C) 2024 by Harald Welte <laforge@osmocom.org>
# SPDX-License-Identifier: GPL-2.0+
import argparse
from osmocom.utils import b2h
from Cryptodome.PublicKey import ECC
# if used with pycryptodome < v3.21.0 you will get the following error when using curve25519:
# "Cryptodome.PublicKey.ECC.UnsupportedEccFeature: Unsupported ECC purpose (OID: 1.3.101.110)"
def gen_key(opts):
# FIXME: avoid overwriting key files
mykey = ECC.generate(curve=opts.curve)
data = mykey.export_key(format='PEM')
with open(opts.key_file, "wt") as f:
f.write(data)
def dump_pkey(opts):
#with open("curve25519-1.key", "r") as f:
with open(opts.key_file, "r") as f:
data = f.read()
mykey = ECC.import_key(data)
der = mykey.public_key().export_key(format='raw', compress=opts.compressed)
print(b2h(der))
arg_parser = argparse.ArgumentParser(description="""Generate or export SUCI keys for 5G SA networks""")
arg_parser.add_argument('--key-file', help='The key file to use', required=True)
subparsers = arg_parser.add_subparsers(dest='command', help="The command to perform", required=True)
parser_genkey = subparsers.add_parser('generate-key', help='Generate a new key pair')
parser_genkey.add_argument('--curve', help='The ECC curve to use', choices=['secp256r1','curve25519'], required=True)
parser_dump_pkey = subparsers.add_parser('dump-pub-key', help='Dump the public key')
parser_dump_pkey.add_argument('--compressed', help='Use point compression', action='store_true')
if __name__ == '__main__':
opts = arg_parser.parse_args()
if opts.command == 'generate-key':
gen_key(opts)
elif opts.command == 'dump-pub-key':
dump_pkey(opts)

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

@@ -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,103 +0,0 @@
Guide: Installing JAVA-card applets
===================================
Almost all modern-day UICC cards have some form of JAVA-card / Sim-Toolkit support, which allows the installation
of customer specific JAVA-card applets. The installation of JAVA-card applets is usually done via the standardized
GlobalPlatform (GPC_SPE_034) ISD (Issuer Security Domain) application interface during the card provisioning process.
(it is also possible to load JAVA-card applets in field via OTA-SMS, but that is beyond the scope of this guide). In
this guide we will go through the individual steps that are required to load JAVA-card applet onto an UICC card.
Preparation
~~~~~~~~~~~
In this example we will install the CAP file HelloSTK_09122024.cap [1] on an sysmoISIM-SJA2 card. Since the interface
is standardized, the exact card model does not matter.
The example applet makes use of the STK (Sim-Toolkit), so we must supply STK installation parameters. Those
parameters are supplied in the form of a hexstring and should be provided by the applet manufacturer. The available
parameters and their exact encoding is specified in ETSI TS 102 226, section 8.2.1.3.2.1. The installation of
HelloSTK_09122024.cap [1], will require the following STK installation parameters: "010001001505000000000000000000000000"
During the installation, we also have to set a memory quota for the volatile and for the non volatile card memory.
Those values also should be provided by the applet manufacturer. In this example, we will allow 255 bytes of volatile
memory and 255 bytes of non volatile memory to be consumed by the applet.
To install JAVA-card applets, one must be in the possession of the key material belonging to the card. The keys are
usually provided by the card manufacturer. The following example will use the following keyset:
+---------+----------------------------------+
| Keyname | Keyvalue |
+=========+==================================+
| DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B |
+---------+----------------------------------+
| ENC/KIC | 542C37A6043679F2F9F71116418B1CD5 |
+---------+----------------------------------+
| MAC/KID | 34F11BAC8E5390B57F4E601372339E3C |
+---------+----------------------------------+
[1] https://osmocom.org/projects/cellular-infrastructure/wiki/HelloSTK
Applet Installation
~~~~~~~~~~~~~~~~~~~
To prepare the installation, a secure channel to the ISD must be established first:
::
pySIM-shell (00:MF)> select ADF.ISD
{
"application_id": "a000000003000000",
"proprietary_data": {
"maximum_length_of_data_field_in_command_message": 255
}
}
pySIM-shell (00:MF/ADF.ISD)> establish_scp02 --key-dek 5524F4BECFE96FB63FC29D6BAAC6058B --key-enc 542C37A6043679F2F9F71116418B1CD5 --key-mac 34F11BAC8E5390B57F4E601372339E3C --security-level 1
Successfully established a SCP02[01] secure channel
.. warning:: In case you get an "EXCEPTION of type 'ValueError' occurred with message: card cryptogram doesn't match" error message, it is very likely that there is a problem with the key material. The card may lock the ISD access after a certain amount of failed tries. Carefully check the key material any try again.
When the secure channel is established, we are ready to install the applet. The installation normally is a multi step
procedure, where the loading of an executable load file is announced first, then loaded and then installed in a final
step. The pySim-shell command ``install_cap`` automatically takes care of those three steps.
::
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_cap /home/user/HelloSTK_09122024.cap --install-parameters-non-volatile-memory-quota 255 --install-parameters-volatile-memory-quota 255 --install-parameters-stk 010001001505000000000000000000000000
loading cap file: /home/user/HelloSTK_09122024.cap ...
parameters:
security-domain-aid: a000000003000000
load-file: 569 bytes
load-file-aid: d07002ca44
module-aid: d07002ca44900101
application-aid: d07002ca44900101
install-parameters: c900ef1cc80200ffc70200ffca12010001001505000000000000000000000000
step #1: install for load...
step #2: load...
Loaded a total of 573 bytes in 3 blocks. Don't forget install_for_install (and make selectable) now!
step #3: install_for_install (and make selectable)...
done.
The applet is now installed on the card. We can now quit pySim-shell and remove the card from the reader and test the
applet in a mobile phone. There should be a new STK application with one menu entry shown, that will greet the user
when pressed.
Applet Removal
~~~~~~~~~~~~~~
To remove the applet, we must establish a secure channel to the ISD (see above). Then we can delete the applet using the
``delete_card_content`` command.
::
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> delete_card_content D07002CA44 --delete-related-objects
The parameter "D07002CA44" is the load-file-AID of the applet. The load-file-AID is encoded in the .cap file and also
displayed during the installation process. It is also important to note that when the applet is installed, it cannot
be installed (under the same AID) again until it is removed.

View File

@@ -1,125 +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

@@ -41,13 +41,8 @@ pySim consists of several parts:
shell
trace
legacy
smpp2sim
library
library-esim
osmo-smdpp
sim-rest
suci-keytool
saip-tool
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

@@ -1,95 +0,0 @@
pySim eSIM libraries
====================
The pySim eSIM libraries implement a variety of functionality related to the GSMA eSIM universe,
including the various interfaces of SGP.21 + SGP.22, as well as Interoperable Profile decioding,
validation, personalization and encoding.
.. automodule:: pySim.esim
:members:
GSMA SGP.21/22 Remote SIM Provisioning (RSP) - High Level
---------------------------------------------------------
pySim.esim.rsp
~~~~~~~~~~~~~~
.. automodule:: pySim.esim.rsp
:members:
pySim.esim.es2p
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.es2p
:members:
pySim.esim.es8p
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.es8p
:members:
pySim.esim.es9p
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.es9p
:members:
GSMA SGP.21/22 Remote SIM Provisioning (RSP) - Low Level
--------------------------------------------------------
pySim.esim.bsp
~~~~~~~~~~~~~~
.. automodule:: pySim.esim.bsp
:members:
pySim.esim.http_json_api
~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.http_json_api
:members:
pySim.esim.x509_cert
~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.x509_cert
:members:
SIMalliance / TCA Interoperable Profile
---------------------------------------
pySim.esim.saip
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip
:members:
pySim.esim.saip.oid
~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.oid
:members:
pySim.esim.saip.personalization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.personalization
:members:
pySim.esim.saip.templates
~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.templates
:members:
pySim.esim.saip.validation
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.validation
:members:

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,16 @@ 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.
* uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your osmo-smdppp
would be running at the host name `testsmdpplus1.example.com`
* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
of the EID or whether it was donwloaded before. 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)
of the EID or whether it was donwloaded before
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical
* **is absolutely insecure**, as it
* does not perform 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,33 +74,19 @@ 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*.
osmo-smdpp
~~~~~~~~~~
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
used; they are copied from GSMA SGP.26 v2.
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
commandline options
~~~~~~~~~~~~~~~~~~~
Typically, you just run it without any arguments, and it will bind its plain-HTTP ES9+ interface to
`localhost` TCP port 8000.
osmo-smdpp currently doesn't have any configuration file.
There are command line options for binding:
Bind the HTTP ES9+ to a port other than 8000::
./osmo-smdpp.py -p 8001
Bind the HTTP ES9+ to a different local interface::
./osmo-smdpp.py -H 127.0.0.1
DNS setup for your LPA
~~~~~~~~~~~~~~~~~~~~~~

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

@@ -1,137 +0,0 @@
saip-tool
=========
eSIM profiles are stored as a sequence of profile element (PE) objects in an ASN.1 DER encoded binary file. To inspect,
verify or make changes to those files, the `saip-tool.py` utility can be used.
NOTE: The file format, eSIM SAIP (SimAlliance Interoperable Profile) is specified in `TCA eUICC Profile Package:
Interoperable Format Technical Specification`
Profile Package Examples
~~~~~~~~~~~~~~~~~~~~~~~~
pySim ships with a set of TS48 profile package examples. Those examples can be found in `pysim/smdpp-data/upp`. The
files can be used as input for `saip-tool.py`. (see also GSMA TS.48 - Generic eUICC Test Profile for Device Testing)
See also: https://github.com/GSMATerminals/Generic-eUICC-Test-Profile-for-Device-Testing-Public
JAVA card applets
~~~~~~~~~~~~~~~~~
The `saip-tool.py` can also be used to manage JAVA-card applets (Application PE) inside a profile package. The user has
the option to add, remove and inspect applications and their instances. In the following we will discuss a few JAVA-card
related use-cases of `saip-tool.py`
NOTE: see also `contrib` folder for script examples (`saip-tool_example_*.sh`)
Inserting applications
----------------------
An application is usually inserted in two steps. In the first step, the application PE is created and populated with
the executable code from a provided `.cap` or `.ijc` file. The user also has to pick a suitable load block AID.
The application instance, which exists inside the application PE, is created in a second step. Here the user must
reference the load block AID and pick, among other application related parameters, a suitable class and instance AID.
Example: Adding a JAVA-card applet to an existing profile package
::
# Step #1: Create the application PE and load the ijc contents from the .cap file:
$ ./contrib/saip-tool.py upp.der add-app --output-file upp_with_app.der --applet-file app.cap --aid '1122334455'
Read 28 PEs from file 'upp.der'
Applying applet file: 'app.cap'...
application PE inserted into PE Sequence after securityDomain PE AID: a000000151000000
Writing 29 PEs to file 'upp_with_app.der'...
# Step #2: Create the application instance inside the application PE created in step #1:
$ ./contrib/saip-tool.py upp_with_app.der add-app-inst --output-file upp_with_app_and_instance.der \
--aid '1122334455' \
--class-aid '112233445501' \
--inst-aid '112233445501' \
--app-privileges '00' \
--app-spec-pars '00' \
--uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
Read 29 PEs from file 'upp_with_app.der'
Found Load Package AID: 1122334455, adding new instance AID: 112233445501 to Application PE...
Writing 29 PEs to file 'upp_with_app_and_instance.der'...
NOTE: The parameters of the sub-commands `add-app` and `add-app-inst` are application specific. It is up to the application
developer to pick parameters that suit the application correctly. For an exact command reference see section
`saip-tool syntax`. For parameter details see `TCA eUICC Profile Package: Interoperable Format Technical Specification`,
section 8.7 and ETSI TS 102 226, section 8.2.1.3.2
Inspecting applications
-----------------------
To inspect the application PE contents of an existing profile package, sub-command `info` with parameter '--apps' can
be used. This command lists out all application and their parameters in detail. This allows an application developer
to check if the applet insertaion was carried out as expected.
Example: Listing applications and their parameters
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der info --apps
Read 29 PEs from file 'upp_with_app_and_instance.der'
Application #0:
loadBlock:
loadPackageAID: '1122334455' (5 bytes)
loadBlockObject: '01000fdecaffed010204000105d07002ca440200...681080056810a00633b44104b431066800a10231' (569 bytes)
instanceList[0]:
applicationLoadPackageAID: '1122334455' (5 bytes)
classAID: '112233445501' (8 bytes)
instanceAID: '112233445501' (8 bytes)
applicationPrivileges: '00' (1 bytes)
lifeCycleState: '07' (1 bytes)
applicationSpecificParametersC9: '00' (1 bytes)
applicationParameters:
uiccToolkitApplicationSpecificParametersField: '01001505000000000000000000000000' (16 bytes)
In case further analysis with external tools or transfer of applications from one profile package to another is
necessary, the executable code in the `loadBlockObject` field can be extracted to an `.ijc` or an `.cap` file.
Example: Extracting applications from a profile package
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der extract-apps --output-dir ./apps --format ijc
Read 29 PEs from file 'upp_with_app_and_instance.der'
Writing Load Package AID: 1122334455 to file ./apps/8949449999999990023f-1122334455.ijc
Removing applications
---------------------
An application PE can be removed using sub-command `remove-app`. The user passes the load package AID as parameter. Then
`saip-tool.py` will search for the related application PE and delete it from the PE sequence.
Example: Remove an application from a profile package
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der remove-app --output-file upp_without_app.der --aid '1122334455'
Read 29 PEs from file 'upp_with_app_and_instance.der'
Found Load Package AID: 1122334455, removing related PE (id=23) from Sequence...
Removing PE application (id=23) from Sequence...
Writing 28 PEs to file 'upp_without_app.der'...
In some cases it is useful to remove only an instance from an existing application PE. This may be the case when the
an application developer wants to modify parameters of an application by removing and re-adding the instance. The
operation basically rolls the state back to step 1 explained in section :ref:`Inserting applications`
Example: Remove an application instance from an application PE
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der remove-app-inst --output-file upp_without_app.der --aid '1122334455' --inst-aid '112233445501'
Read 29 PEs from file 'upp_with_app_and_instance.der'
Found Load Package AID: 1122334455, removing instance AID: 112233445501 from Application PE...
Removing instance from Application PE...
Writing 29 PEs to file 'upp_with_app.der'...
saip-tool syntax
~~~~~~~~~~~~~~~~
.. argparse::
:module: contrib.saip-tool
:func: parser
:prog: contrib/saip-tool.py

View File

@@ -1,4 +1,4 @@
pySim-shell
pySim-shell
===========
pySim-shell is an interactive command line shell for all kind of interactions with SIM cards,
@@ -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:
@@ -67,18 +64,8 @@ Usage Examples
:caption: Tutorials for pySIM-shell:
suci-tutorial
cap-tutorial
Advanced Topics
---------------
.. toctree::
:maxdepth: 1
:caption: Advanced pySIM-shell topics
card-key-provider
remote-access
cmd2 basics
-----------
@@ -146,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
@@ -544,9 +505,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
~~~~~~~~~~~~
@@ -561,9 +519,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
~~~~~~~~~~~~~
@@ -578,9 +533,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
~~~~~~~~~~~~~~~~~~~
@@ -599,12 +551,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
~~~~~~~~~~
@@ -633,8 +579,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
~~~~~~~~~~~~~
@@ -688,10 +632,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
@@ -705,12 +645,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
~~~~~~~~~~
@@ -1003,25 +937,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`
aram_lock
~~~~~~~~~
This command allows to lock the access to the STORE DATA command. This renders
all access rules stored within the ARA-M applet effectively read-only. The lock
can only be removed via a secure channel to the security domain and is therefore
suitable to prevent unauthorized changes to ARA-M rules.
Removal of the lock:
::
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_for_personalization A00000015141434C00
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> apdu --expect-sw 9000 80E2900001A2
NOTE: ARA-M Locking is a proprietary feature that is specific to sysmocom's
fork of Bertrand Martel's ARA-M implementation. ARA-M Locking is supported in
newer (2025) applet versions from v0.1.0 onward.
intended must be manually inserted again using `aram_store_ref_ar_do`
GlobalPlatform commands
@@ -1066,18 +982,6 @@ delete_key
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.del_key_parser
load
~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.load_parser
install_cap
~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.install_cap_parser
install_for_personalization
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. argparse::
@@ -1090,12 +994,6 @@ install_for_install
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.inst_inst_parser
install_for_load
~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.inst_load_parser
delete_card_content
~~~~~~~~~~~~~~~~~~~
.. argparse::
@@ -1155,7 +1053,7 @@ es10x_store_data
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.es10x_store_data_parser
:func: ADF_ISDR.AddlShellCommands.es10x_store_data_parser
get_euicc_configured_addresses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1174,7 +1072,7 @@ set_default_dp_address
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.set_def_dp_addr_parser
:func: ADF_ISDR.AddlShellCommands.set_def_dp_addr_parser
get_euicc_challenge
~~~~~~~~~~~~~~~~~~~
@@ -1317,7 +1215,7 @@ remove_notification_from_list
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.rem_notif_parser
:func: ADF_ISDR.AddlShellCommands.rem_notif_parser
Example::
@@ -1366,7 +1264,7 @@ enable_profile
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.en_prof_parser
:func: ADF_ISDR.AddlShellCommands.en_prof_parser
Example (successful)::
@@ -1388,7 +1286,7 @@ disable_profile
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.dis_prof_parser
:func: ADF_ISDR.AddlShellCommands.dis_prof_parser
Example (successful)::
@@ -1402,7 +1300,7 @@ delete_profile
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.del_prof_parser
:func: ADF_ISDR.AddlShellCommands.del_prof_parser
Example::
@@ -1411,13 +1309,6 @@ Example::
"delete_result": "ok"
}
euicc_memory_reset
~~~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.mem_res_parser
get_eid
~~~~~~~
@@ -1436,7 +1327,7 @@ set_nickname
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.set_nickname_parser
:func: ADF_ISDR.AddlShellCommands.set_nickname_parser
Example::

View File

@@ -1,118 +0,0 @@
sim-rest-server
===============
Sometimes there are use cases where a [remote] application will need
access to a USIM for authentication purposes. This is, for example, in
case an IMS test client needs to perform USIM based authentication
against an IMS core.
The pysim repository contains two programs: `sim-rest-server.py` and
`sim-rest-client.py` that implement a simple approach to achieve the
above:
`sim-rest-server.py` speaks to a [usually local] USIM via the PC/SC
API and provides a high-level REST API towards [local or remote]
applications that wish to perform UMTS AKA using the USIM.
`sim-rest-client.py` implements a small example client program to
illustrate how the REST API provided by `sim-rest-server.py` can be
used.
REST API Calls
--------------
POST /sim-auth-api/v1/slot/SLOT_NR
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
where SLOT_NR is the integer-encoded slot number (corresponds to PC/SC
reader number). When using a single sysmoOCTSIM board, this is in the range of 0..7
Example: `/sim-auth-api/v1/slot/0` for the first slot.
Request Body
############
The request body is a JSON document, comprising of
1. the RAND and AUTN parameters as hex-encoded string
2. the application against which to authenticate (USIM, ISIM)
Example:
::
{
"rand": "bb685a4b2fc4d697b9d6a129dd09a091",
"autn": "eea7906f8210000004faf4a7df279b56"
}
HTTP Status Codes
#################
HTTP status codes are used to represent errors within the REST server
and the SIM reader hardware. They are not used to communicate protocol
level errors reported by the SIM Card. An unsuccessful authentication
will hence have a `200 OK` HTTP Status code and then encode the SIM
specific error information in the Response Body.
====== =========== ================================
Status Code Description
------ ----------- --------------------------------
200 OK Successful execution
400 Bad Request Request body is malformed
404 Not Found Specified SIM Slot doesn't exist
410 Gone No SIM card inserted in slot
====== =========== ================================
Response Body
#############
The response body is a JSON document, either
#. a successful outcome; encoding RES, CK, IK as hex-encoded string
#. a sync failure; encoding AUTS as hex-encoded string
#. errors
#. authentication error (incorrect MAC)
#. authentication error (security context not supported)
#. key freshness failure
#. unspecified card error
Example (succcess):
::
{
"successful_3g_authentication": {
"res": "b15379540ec93985",
"ck": "713fde72c28cbd282a4cd4565f3d6381",
"ik": "2e641727c95781f1020d319a0594f31a",
"kc": "771a2c995172ac42"
}
}
Example (re-sync case):
::
{
"synchronisation_failure": {
"auts": "dc2a591fe072c92d7c46ecfe97e5"
}
}
Concrete example using the included sysmoISIM-SJA2
--------------------------------------------------
This was tested using SIMs ending in IMSI numbers 45890...45899
The following command were executed successfully:
Slot 0
::
$ /usr/local/src/pysim/contrib/sim-rest-client.py -c 1 -n 0 -k 841EAD87BC9D974ECA1C167409357601 -o 3211CACDD64F51C3FD3013ECD9A582A0
-> {'rand': 'fb195c7873b20affa278887920b9dd57', 'autn': 'd420895a6aa2000089cd016f8d8ae67c'}
<- {'successful_3g_authentication': {'res': '131004db2ff1ce8e', 'ck': 'd42eb5aa085307903271b2422b698bad', 'ik': '485f81e6fd957fe3cad374adf12fe1ca', 'kc': '64d3f2a32f801214'}}
Slot 1
::
$ /usr/local/src/pysim/contrib/sim-rest-client.py -c 1 -n 1 -k 5C2CE9633FF9B502B519A4EACD16D9DF -o 9834D619E71A02CD76F00CC7AA34FB32
-> {'rand': '433dc5553db95588f1d8b93870930b66', 'autn': '126bafdcbe9e00000026a208da61075d'}
<- {'successful_3g_authentication': {'res': '026d7ac42d379207', 'ck': '83a90ba331f47a95c27a550b174c4a1f', 'ik': '31e1d10329ffaf0ca1684a1bf0b0a14a', 'kc': 'd15ac5b0fff73ecc'}}

View File

@@ -1,57 +0,0 @@
pySim-smpp2sim
==============
This is a program to emulate the entire communication path SMSC-CN-RAN-ME
that is usually between an OTA backend and the SIM card. This allows
to play with SIM OTA technology without using a mobile network or even
a mobile phone.
An external application can act as SMPP ESME and must encode (and
encrypt/sign) the OTA SMS and submit them via SMPP to this program, just
like it would submit it normally to a SMSC (SMS Service Centre). The
program then re-formats the SMPP-SUBMIT into a SMS DELIVER TPDU and
passes it via an ENVELOPE APDU to the SIM card that is locally inserted
into a smart card reader.
The path from SIM to external OTA application works the opposite way.
The default SMPP system_id is `test`. Likewise, the default SMPP
password is `test`
Running pySim-smpp2sim
----------------------
The command accepts the same command line arguments for smart card interface device selection as pySim-shell,
as well as a few SMPP specific arguments:
.. argparse::
:module: pySim-smpp2sim
:func: option_parser
:prog: pySim-smpp2sim.py
Example execution with sample output
------------------------------------
So for a simple system with a single PC/SC device, you would typically use something like
`./pySim-smpp2sim.py -p0` to start the program. You will see output like this at start-up
::
Using reader PCSC[HID Global OMNIKEY 3x21 Smart Card Reader [OMNIKEY 3x21 Smart Card Reader] 00 00]
INFO root: Binding Virtual SMSC to TCP Port 2775 at ::
The application has hence bound to local TCP port 2775 and expects your SMS-sending applications to send their
SMS there. Once you do, you will see log output like below:
::
WARNING smpp.twisted.protocol: SMPP connection established from ::ffff:127.0.0.1 to port 2775
INFO smpp.twisted.server: Added CommandId.bind_transceiver bind for 'test'. Active binds: CommandId.bind_transceiver: 1, CommandId.bind_transmitter: 0, CommandId.bind_receiver: 0. Max binds: 2
INFO smpp.twisted.protocol: Bind request succeeded for test. 1 active binds
And once your external program is sending SMS to the simulated SMSC, it will log something like
::
INFO root: SMS_DELIVER(MTI=0, MMS=False, LP=False, RP=False, UDHI=True, SRI=False, OA=AddressField(TON=international, NPI=unknown, 12), PID=7f, DCS=f6, SCTS=bytearray(b'"pR\x00\x00\x00\x00'), UDL=45, UD=b"\x02p\x00\x00(\x15\x16\x19\x12\x12\xb0\x00\x01'\xfa(\xa5\xba\xc6\x9d<^\x9d\xf2\xc7\x15]\xfd\xdeD\x9c\x82k#b\x15Ve0x{0\xe8\xbe]")
SMSPPDownload(DeviceIdentities({'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),Address({'ton_npi': 0, 'call_number': '0123456'}),SMS_TPDU({'tpdu': '400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c

View File

@@ -1,58 +0,0 @@
suci-keytool
============
Subscriber concealment is an important feature of the 5G SA architecture: It avoids the many privacy
issues associated with having a permanent identifier (SUPI, traditionally the IMSI) transmitted in plain text
over the air interface. Using SUCI solves this issue not just for the air interface; it even ensures the SUPI/IMSI
is not known to the visited network (VPLMN) at all.
In principle, the SUCI mechanism works by encrypting the SUPI by asymmetric (public key) cryptography:
Only the HPLMN is in possession of the private key and hence can decrypt the SUCI to the SUPI, while
each subscriber has the public key in order to encrypt their SUPI into the SUCI. In reality, the
details are more complex, as there are ephemeral keys and cryptographic MAC involved.
In any case, in order to operate a SUCI-enabled 5G SA network, you will have to
#. generate a ECC key pair of public + private key
#. deploy the public key on your USIMs
#. deploy the private key on your 5GC, specifically the UDM function
pysim contains (int its `contrib` directory) a small utility program that can make it easy to generate
such keys: `suci-keytool.py`
Generating keys
~~~~~~~~~~~~~~~
Example: Generating a *secp256r1* ECC public key pair and storing it to `/tmp/suci.key`:
::
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key generate-key --curve secp256r1
Dumping public keys
~~~~~~~~~~~~~~~~~~~
In order to store the key to SIM cards as part of `ADF.USIM/DF.5GS/EF.SUCI_Calc_Info`, you will need
a hexadecimal representation of the public key. You can achieve that using the `dump-pub-key` operation
of suci-keytool:
Example: Dumping the public key part from a previously generated key file:
::
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key dump-pub-key
0473152f32523725f5175d255da2bd909de97b1d06449a9277bc629fe42112f8643e6b69aa6dce6c86714ccbe6f2e0f4f4898d102e2b3f0c18ce26626f052539bb
If you want the point-compressed representation, you can use the `--compressed` option:
::
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key dump-pub-key --compressed
0373152f32523725f5175d255da2bd909de97b1d06449a9277bc629fe42112f864
suci-keytool syntax
~~~~~~~~~~~~~~~~~~~
.. argparse::
:module: contrib.suci-keytool
:func: arg_parser
:prog: contrib/suci-keytool.py

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

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
#
import hashlib
import argparse
from optparse import OptionParser
import os
import random
import re
@@ -33,12 +33,11 @@ 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.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 *
@@ -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,8 +29,6 @@ import random
import re
import sys
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
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
@@ -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,17 +47,14 @@ 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
@@ -114,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))
@@ -123,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):
@@ -155,7 +147,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
# When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
# needed to operate on cards.
if self.card and self.rs:
self.rs.reset()
self.lchan = self.rs.lchan[0]
self._onchange_conserve_write(
'conserve_write', False, self.conserve_write)
@@ -169,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
@@ -203,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,23 +203,18 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
self.cmd2.poutput("<- %s: %s" % (sw, resp))
def update_prompt(self):
if self.rs and self.rs.adm_verified:
prompt_char = '#'
else:
prompt_char = '>'
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)%c ' % (str(scp), self.lchan.lchan_nr, path_str, prompt_char)
self.prompt = 'pySIM-shell (%s:%02u:%s)> ' % (str(scp), self.lchan.lchan_nr, path_str)
else:
self.prompt = 'pySIM-shell (%02u:%s)%c ' % (self.lchan.lchan_nr, path_str, prompt_char)
self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
else:
if self.card:
self.prompt = 'pySIM-shell (no card profile)%c ' % prompt_char
self.prompt = 'pySIM-shell (no card profile)> '
else:
self.prompt = 'pySIM-shell (no card)%c ' % prompt_char
self.prompt = 'pySIM-shell (no card)> '
@cmd2.with_category(CUSTOM_CATEGORY)
def do_intro(self, _):
@@ -256,10 +235,9 @@ 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)
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
@cmd2.with_argparser(apdu_cmd_parser)
def do_apdu(self, opts):
@@ -273,9 +251,9 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
# 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)
data, sw = self.card._scc.send_apdu(opts.APDU)
else:
data, sw = self.lchan.scc.send_apdu(opts.APDU, apply_lchan = False)
data, sw = self.lchan.scc.send_apdu(opts.APDU)
if data:
self.poutput("SW: %s, RESP: %s" % (sw, data))
else:
@@ -283,21 +261,14 @@ 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 = self.card._scc.get_atr()
else:
atr = self.rs.reset(self)
self.poutput('Card ATR: %s' % atr)
atr = self.card.reset()
if self.lchan and self.lchan.scc.scp:
self.lchan.scc.scp = None
self.poutput('Card ATR: %s' % i2h(atr))
self.update_prompt()
class InterceptStderr(list):
@@ -376,7 +347,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,
@@ -392,7 +364,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
@@ -402,7 +374,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.
@@ -417,7 +389,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()
@@ -469,13 +441,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):
@@ -524,25 +496,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'])
@@ -575,45 +534,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()
@@ -631,10 +688,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("#")
@@ -662,222 +719,55 @@ 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_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!")
self._cmd.rs.adm_verified = True
self._cmd.update_prompt()
def do_cardinfo(self, opts):
"""Display information about the currently inserted card"""
self._cmd.poutput("Card info:")
self._cmd.poutput(" Name: %s" % self._cmd.card.name)
self._cmd.poutput(" ATR: %s" % 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):
@@ -902,64 +792,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")
@@ -967,26 +862,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")
@@ -999,14 +894,14 @@ class Iso7816Commands(CommandSet):
@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).
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.
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!"""
Note that for *deactivation* the to-be-deactivated EF must be selected, but for *activation*, the DF
above the to-be-activated EF must be selected!"""
(data, sw) = self._cmd.lchan.activate_file(opts.NAME)
def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
@@ -1016,7 +911,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):
@@ -1028,7 +923,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):
@@ -1040,14 +935,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):
@@ -1073,14 +968,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,
@@ -1088,8 +977,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,
@@ -1098,20 +985,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())
@@ -1126,19 +1015,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
@@ -1150,44 +1040,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:
stop = app.onecmd_plus_hooks(c)
if stop == True:
sys.exit(0)
else:
print("Errors during startup, refusing to execute command (%s)" % c)
# Run optional command
if opts.command:
if not startup_errors:
stop = app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
if stop == True:
sys.exit(0)
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:
stop = app.onecmd_plus_hooks('{} {}'.format('run_script', opts.script), add_to_history = False)
if stop == True:
sys.exit(0)
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

@@ -1,428 +0,0 @@
#!/usr/bin/env python3
#
# Program to emulate the entire communication path SMSC-MSC-BSC-BTS-ME
# that is usually between an OTA backend and the SIM card. This allows
# to play with SIM OTA technology without using a mobile network or even
# a mobile phone.
#
# An external application must encode (and encrypt/sign) the OTA SMS
# and submit them via SMPP to this program, just like it would submit
# it normally to a SMSC (SMS Service Centre). The program then re-formats
# the SMPP-SUBMIT into a SMS DELIVER TPDU and passes it via an ENVELOPE
# APDU to the SIM card that is locally inserted into a smart card reader.
#
# The path from SIM to external OTA application works the opposite way.
# (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 argparse
import logging
import colorlog
from twisted.protocols import basic
from twisted.internet import defer, endpoints, protocol, reactor, task
from twisted.cred.portal import IRealm
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
from twisted.cred.portal import Portal
from zope.interface import implementer
from smpp.twisted.config import SMPPServerConfig
from smpp.twisted.server import SMPPServerFactory, SMPPBindManager
from smpp.twisted.protocol import SMPPSessionStates, DataHandlerResponse
from smpp.pdu import pdu_types, operations, pdu_encoding
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer
from pySim.commands import SimCardCommands
from pySim.cards import UiccCardBase
from pySim.exceptions import *
from pySim.cat import ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload, BearerDescription
from pySim.cat import DeviceIdentities, Address, OtherAddress, UiccTransportLevel, BufferSize
from pySim.cat import ChannelStatus, ChannelData, ChannelDataLength
from pySim.utils import b2h, h2b
logger = logging.getLogger(__name__)
# MSISDNs to use when generating proactive SMS messages
SIM_MSISDN='23'
ESME_MSISDN='12'
# HACK: we need some kind of mapping table between system_id and card-reader
# or actually route based on MSISDNs
hackish_global_smpp = None
class MyApduTracer(ApduTracer):
def trace_response(self, cmd, sw, resp):
print("-> %s %s" % (cmd[:10], cmd[10:]))
print("<- %s: %s" % (sw, resp))
class TcpProtocol(protocol.Protocol):
def dataReceived(self, data):
pass
def connectionLost(self, reason):
pass
def tcp_connected_callback(p: protocol.Protocol):
"""called by twisted TCP client."""
logger.error("%s: connected!" % p)
class ProactChannel:
"""Representation of a single proective channel."""
def __init__(self, channels: 'ProactChannels', chan_nr: int):
self.channels = channels
self.chan_nr = chan_nr
self.ep = None
def close(self):
"""Close the channel."""
if self.ep:
self.ep.disconnect()
self.channels.channel_delete(self.chan_nr)
class ProactChannels:
"""Wrapper class for maintaining state of proactive channels."""
def __init__(self):
self.channels = {}
def channel_create(self) -> ProactChannel:
"""Create a new proactive channel, allocating its integer number."""
for i in range(1, 9):
if not i in self.channels:
self.channels[i] = ProactChannel(self, i)
return self.channels[i]
raise ValueError('Cannot allocate another channel: All channels active')
def channel_delete(self, chan_nr: int):
del self.channels[chan_nr]
class Proact(ProactiveHandler):
#def __init__(self, smpp_factory):
# self.smpp_factory = smpp_factory
def __init__(self):
self.channels = ProactChannels()
@staticmethod
def _find_first_element_of_type(instlist, cls):
for i in instlist:
if isinstance(i, cls):
return i
return None
"""Call-back which the pySim transport core calls whenever it receives a
proactive command from the SIM."""
def handle_SendShortMessage(self, pcmd: ProactiveCommand):
# {'smspp_download': [{'device_identities': {'source_dev_id': 'network',
# 'dest_dev_id': 'uicc'}},
# {'address': {'ton_npi': {'ext': True,
# 'type_of_number': 'international',
# 'numbering_plan_id': 'isdn_e164'},
# 'call_number': '79'}},
# {'sms_tpdu': {'tpdu': '40048111227ff6407070611535004d02700000481516011212000001fe4c0943aea42e45021c078ae06c66afc09303608874b72f58bacadb0dcf665c29349c799fbb522e61709c9baf1890015e8e8e196e36153106c8b92f95153774'}}
# ]}
"""Card requests sending a SMS. We need to pass it on to the ESME via SMPP."""
logger.info("SendShortMessage")
logger.info(pcmd)
# Relevant parts in pcmd: Address, SMS_TPDU
addr_ie = Proact._find_first_element_of_type(pcmd.children, Address)
sms_tpdu_ie = Proact._find_first_element_of_type(pcmd.children, SMS_TPDU)
raw_tpdu = sms_tpdu_ie.decoded['tpdu']
submit = SMS_SUBMIT.from_bytes(raw_tpdu)
submit.tp_da = AddressField(addr_ie.decoded['call_number'], addr_ie.decoded['ton_npi']['type_of_number'],
addr_ie.decoded['ton_npi']['numbering_plan_id'])
logger.info(submit)
self.send_sms_via_smpp(submit)
def handle_OpenChannel(self, pcmd: ProactiveCommand):
"""Card requests opening a new channel via a UDP/TCP socket."""
# {'open_channel': [{'command_details': {'command_number': 1,
# 'type_of_command': 'open_channel',
# 'command_qualifier': 3}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'terminal'}},
# {'bearer_description': {'bearer_type': 'default',
# 'bearer_parameters': ''}},
# {'buffer_size': 1024},
# {'uicc_transport_level': {'protocol_type': 'tcp_uicc_client_remote',
# 'port_number': 32768}},
# {'other_address': {'type_of_address': 'ipv4',
# 'address': '01020304'}}
# ]}
logger.info("OpenChannel")
logger.info(pcmd)
transp_lvl_ie = Proact._find_first_element_of_type(pcmd.children, UiccTransportLevel)
other_addr_ie = Proact._find_first_element_of_type(pcmd.children, OtherAddress)
bearer_desc_ie = Proact._find_first_element_of_type(pcmd.children, BearerDescription)
buffer_size_ie = Proact._find_first_element_of_type(pcmd.children, BufferSize)
if transp_lvl_ie.decoded['protocol_type'] != 'tcp_uicc_client_remote':
raise ValueError('Unsupported protocol_type')
if other_addr_ie.decoded.get('type_of_address', None) != 'ipv4':
raise ValueError('Unsupported type_of_address')
ipv4_bytes = h2b(other_addr_ie.decoded['address'])
ipv4_str = '%u.%u.%u.%u' % (ipv4_bytes[0], ipv4_bytes[1], ipv4_bytes[2], ipv4_bytes[3])
port_nr = transp_lvl_ie.decoded['port_number']
print("%s:%u" % (ipv4_str, port_nr))
channel = self.channels.channel_create()
channel.ep = endpoints.TCP4ClientEndpoint(reactor, ipv4_str, port_nr)
channel.prot = TcpProtocol()
d = endpoints.connectProtocol(channel.ep, channel.prot)
# FIXME: why is this never called despite the client showing the inbound connection?
d.addCallback(tcp_connected_callback)
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'open_channel',
# 'command_qualifier': 3}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
# {'channel_status': '8100'},
# {'bearer_description': {'bearer_type': 'default', 'bearer_parameters': ''}},
# {'buffer_size': 1024}
# ]
return self.prepare_response(pcmd) + [ChannelStatus(decoded='8100'), bearer_desc_ie, buffer_size_ie]
def handle_CloseChannel(self, pcmd: ProactiveCommand):
"""Close a channel."""
logger.info("CloseChannel")
logger.info(pcmd)
def handle_ReceiveData(self, pcmd: ProactiveCommand):
"""Receive/read data from the socket."""
# {'receive_data': [{'command_details': {'command_number': 1,
# 'type_of_command': 'receive_data',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'channel_1'}},
# {'channel_data_length': 9}
# ]}
logger.info("ReceiveData")
logger.info(pcmd)
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'receive_data',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
# {'channel_data': '16030100040e000000'},
# {'channel_data_length': 0}
# ]
return self.prepare_response(pcmd) + []
def handle_SendData(self, pcmd: ProactiveCommand):
"""Send/write data received from the SIM to the socket."""
# {'send_data': [{'command_details': {'command_number': 1,
# 'type_of_command': 'send_data',
# 'command_qualifier': 1}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'channel_1'}},
# {'channel_data': '160301003c010000380303d0f45e12b52ce5bb522750dd037738195334c87a46a847fe2b6886cada9ea6bf00000a00ae008c008b00b0002c010000050001000101'}
# ]}
logger.info("SendData")
logger.info(pcmd)
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
chan_data_ie = Proact._find_first_element_of_type(pcmd.children, ChannelData)
chan_str = dev_id_ie.decoded['dest_dev_id']
chan_nr = 1 # FIXME
chan = self.channels.channels.get(chan_nr, None)
# FIXME chan.prot.transport.write(h2b(chan_data_ie.decoded))
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'send_data',
# 'command_qualifier': 1}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
# {'channel_data_length': 255}
# ]
return self.prepare_response(pcmd) + [ChannelDataLength(decoded=255)]
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
# {'set_up_event_list': [{'command_details': {'command_number': 1,
# 'type_of_command': 'set_up_event_list',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'terminal'}},
# {'event_list': ['data_available', 'channel_status']}
# ]}
logger.info("SetUpEventList")
logger.info(pcmd)
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'set_up_event_list',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}}
# ]
return self.prepare_response(pcmd)
def getChannelStatus(self, pcmd: ProactiveCommand):
logger.info("GetChannelStatus")
logger.info(pcmd)
return self.prepare_response(pcmd) + []
def send_sms_via_smpp(self, submit: SMS_SUBMIT):
# while in a normal network the phone/ME would *submit* a message to the SMSC,
# we are actually emulating the SMSC itself, so we must *deliver* the message
# to the ESME
deliver = SMS_DELIVER.from_submit(submit)
deliver_smpp = deliver.to_smpp()
hackish_global_smpp.sendDataRequest(deliver_smpp)
# # obtain the connection/binding of system_id to be used for delivering MO-SMS to the ESME
# connection = smpp_server.getBoundConnections[system_id].getNextBindingForDelivery()
# connection.sendDataRequest(deliver_smpp)
def dcs_is_8bit(dcs):
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
return True
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED_COMMON):
return True
# pySim-smpp2sim.py:150:21: E1101: Instance of 'DataCodingScheme' has no 'GSM_MESSAGE_CLASS' member (no-member)
# pylint: disable=no-member
if dcs.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS and dcs.schemeData['msgCoding'] == pdu_types.DataCodingGsmMsgCoding.DATA_8BIT:
return True
else:
return False
class MyServer:
@implementer(IRealm)
class SmppRealm:
def requestAvatar(self, avatarId, mind, *interfaces):
return ('SMPP', avatarId, lambda: None)
def __init__(self, tcp_port:int = 2775, bind_ip = '::', system_id:str = 'test', password:str = 'test'):
smpp_config = SMPPServerConfig(msgHandler=self._msgHandler,
systems={system_id: {'max_bindings': 2}})
portal = Portal(self.SmppRealm())
credential_checker = InMemoryUsernamePasswordDatabaseDontUse()
credential_checker.addUser(system_id, password)
portal.registerChecker(credential_checker)
self.factory = SMPPServerFactory(smpp_config, auth_portal=portal)
logger.info('Binding Virtual SMSC to TCP Port %u at %s' % (tcp_port, bind_ip))
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
smppEndpoint.listen(self.factory)
self.tp = self.scc = self.card = None
def connect_to_card(self, tp: LinkBase):
self.tp = tp
self.scc = SimCardCommands(self.tp)
self.card = UiccCardBase(self.scc)
# this should be part of UiccCardBase, but FairewavesSIM breaks with that :/
self.scc.cla_byte = "00"
self.scc.sel_ctrl = "0004"
self.card.read_aids()
self.card.select_adf_by_aid(adf='usim')
# FIXME: create a more realistic profile than ffffff
self.scc.terminal_profile('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
def _msgHandler(self, system_id, smpp, pdu):
"""Handler for incoming messages received via SMPP from ESME."""
# HACK: we need some kind of mapping table between system_id and card-reader
# or actually route based on MSISDNs
global hackish_global_smpp
hackish_global_smpp = smpp
if pdu.id == pdu_types.CommandId.submit_sm:
return self.handle_submit_sm(system_id, smpp, pdu)
else:
logger.warning('Rejecting non-SUBMIT commandID')
return pdu_types.CommandStatus.ESME_RINVCMDID
def handle_submit_sm(self, system_id, smpp, pdu):
"""SUBMIT-SM was received via SMPP from ESME. We need to deliver it to the SIM."""
# check for valid data coding scheme + PID
if not dcs_is_8bit(pdu.params['data_coding']):
logger.warning('Rejecting non-8bit DCS')
return pdu_types.CommandStatus.ESME_RINVDCS
if pdu.params['protocol_id'] != 0x7f:
logger.warning('Rejecting non-SIM PID')
return pdu_types.CommandStatus.ESME_RINVDCS
# 1) build a SMS-DELIVER (!) from the SMPP-SUBMIT
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
logger.info(tpdu)
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
addr_ie = Address(decoded={'ton_npi': {'ext':False, 'type_of_number':'unknown', 'numbering_plan_id':'unknown'}, 'call_number': '0123456'})
dev_ids = DeviceIdentities(decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'})
sms_dl = SMSPPDownload(children=[dev_ids, addr_ie, tpdu_ie])
# 3) send to the card
envelope_hex = b2h(sms_dl.to_tlv())
logger.info("ENVELOPE: %s" % envelope_hex)
(data, sw) = self.scc.envelope(envelope_hex)
logger.info("SW %s: %s" % (sw, data))
if sw in ['9200', '9300']:
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
elif sw == '9000' or sw[0:2] in ['6f', '62', '63'] and len(data):
# data something like 027100000e0ab000110000000000000001612f or
# 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
# which is the user-data portion of the SMS starting with the UDH (027100)
# TODO: return the response back to the sender in an RP-ACK; PID/DCS like in CMD
deliver = operations.DeliverSM(service_type=pdu.params['service_type'],
source_addr_ton=pdu.params['dest_addr_ton'],
source_addr_npi=pdu.params['dest_addr_npi'],
source_addr=pdu.params['destination_addr'],
dest_addr_ton=pdu.params['source_addr_ton'],
dest_addr_npi=pdu.params['source_addr_npi'],
destination_addr=pdu.params['source_addr'],
esm_class=pdu.params['esm_class'],
protocol_id=pdu.params['protocol_id'],
priority_flag=pdu.params['priority_flag'],
data_coding=pdu.params['data_coding'],
short_message=h2b(data))
smpp.sendDataRequest(deliver)
return pdu_types.CommandStatus.ESME_ROK
else:
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
option_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
smpp_group = option_parser.add_argument_group('SMPP Options')
smpp_group.add_argument('--smpp-bind-port', type=int, default=2775,
help='TCP Port to bind the SMPP socket to')
smpp_group.add_argument('--smpp-bind-ip', default='::',
help='IPv4/IPv6 address to bind the SMPP socket to')
smpp_group.add_argument('--smpp-system-id', default='test',
help='SMPP System-ID used by ESME to bind')
smpp_group.add_argument('--smpp-password', default='test',
help='SMPP Password used by ESME to bind')
if __name__ == '__main__':
log_format='%(log_color)s%(levelname)-8s%(reset)s %(name)s: %(message)s'
colorlog.basicConfig(level=logging.INFO, format = log_format)
logger = colorlog.getLogger()
opts = option_parser.parse_args()
tp = init_reader(opts, proactive_handler = Proact())
if tp is None:
exit(1)
tp.connect()
global g_ms
g_ms = MyServer(opts.smpp_bind_port, opts.smpp_bind_ip, opts.smpp_system_id, opts.smpp_password)
g_ms.connect_to_card(tp)
reactor.run()

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

@@ -9,7 +9,7 @@ is far too simplistic, while this decoder can utilize all of the information
we already know in pySim about the filesystem structure, file encoding, etc.
"""
# (C) 2022-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
@@ -29,11 +29,12 @@ import abc
import typing
from typing import List, Dict, Optional
from termcolor import colored
from construct import Byte
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.utils import *
from construct import Byte, GreedyBytes
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
@@ -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."""
@@ -271,7 +270,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 +280,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 +290,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 +310,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

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
@@ -22,13 +22,11 @@ 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 pySim.utils import i2h
logger = logging.getLogger(__name__)
@@ -100,7 +98,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:
@@ -116,7 +114,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 +127,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 = {}
@@ -460,17 +458,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,11 +9,11 @@ APDU commands of 3GPP TS 31.102 V16.6.0
"""
from typing import Dict
from construct import BitStruct, Enum, BitsInteger, Int8ub, this, Struct, If, Switch, Const
from construct import BitStruct, Enum, BitsInteger, Int8ub, Bytes, this, Struct, If, Switch, Const
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
@@ -42,28 +42,28 @@ class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '
BitsInteger(4),
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
vgcs_vbs=2, gba=4))
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/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

@@ -16,16 +16,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from osmocom.gsmtap import GsmtapReceiver
from pySim.gsmtap import GsmtapSource
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
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
class GsmtapApduSource(ApduSource):
"""ApduSource for handling GSMTAP-SIM messages received via UDP, such as
@@ -41,7 +40,7 @@ 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()

View File

@@ -19,17 +19,17 @@
import logging
from typing import Tuple
import pyshark
from osmocom.gsmtap import GsmtapMessage
from pySim.utils import h2b
from pySim.gsmtap import GsmtapMessage
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
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
logger = logging.getLogger(__name__)

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,12 @@ 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.runtime import RuntimeState
from pySim.profile import CardProfile
from pySim.cdma_ruim import CardProfileRUIM
from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import all_subclasses
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
@@ -42,7 +41,7 @@ import pySim.ara_m
import pySim.global_platform
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 +56,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 +106,4 @@ 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,29 +26,29 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
#
from construct import GreedyString, Struct, Enum, Int8ub, Int16ub
from construct import GreedyBytes, GreedyString, Struct, Enum, Int8ub, Int16ub
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 *
from pySim.tlv import *
from pySim.utils import Hexstr
import pySim.global_platform
# various BER-TLV encoded Data Objects (DOs)
class AidRefDO(BER_TLV_IE, tag=0x4f):
# GPD_SPE_013 v1.1 Table 6-3
# SEID v1.1 Table 6-3
_construct = HexAdapter(GreedyBytes)
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
# GPD_SPE_013 v1.1 Table 6-3
# SEID v1.1 Table 6-3
pass
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
# GPD_SPE_013 v1.1 Table 6-4
# SEID v1.1 Table 6-4
_construct = HexAdapter(GreedyBytes)
@@ -58,12 +58,12 @@ class PkgRefDO(BER_TLV_IE, tag=0xca):
class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO, AidRefEmptyDO, DevAppIdRefDO, PkgRefDO]):
# GPD_SPE_013 v1.1 Table 6-5
# SEID v1.1 Table 6-5
pass
class ApduArDO(BER_TLV_IE, tag=0xd0):
# GPD_SPE_013 v1.1 Table 6-8
# SEID v1.1 Table 6-8
def _from_bytes(self, do: bytes):
if len(do) == 1:
if do[0] == 0x00:
@@ -76,13 +76,13 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
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:
@@ -108,7 +108,7 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
class NfcArDO(BER_TLV_IE, tag=0xd1):
# GPD_SPE_013 v1.1 Table 6-9
# SEID v1.1 Table 6-9
_construct = Struct('nfc_event_access_rule' /
Enum(Int8ub, never=0, always=1))
@@ -120,122 +120,124 @@ class PermArDO(BER_TLV_IE, tag=0xdb):
class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
# GPD_SPE_013 v1.1 Table 6-7
# SEID v1.1 Table 6-7
pass
class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
# GPD_SPE_013 v1.1 Table 6-6
# SEID v1.1 Table 6-6
pass
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
# GPD_SPE_013 v1.1 Table 4-2
# SEID v1.1 Table 4-2
pass
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
# GPD_SPE_013 v1.1 Table 4-3
# SEID v1.1 Table 4-3
pass
class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
# GPD_SPE_013 v1.1 Table 4-4
# SEID v1.1 Table 4-4
_construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
# GPD_SPE_013 v1.1 Table 6-12
# SEID v1.1 Table 6-12
_construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
# GPD_SPE_013 v1.1 Table 6-10
# SEID v1.1 Table 6-10
pass
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
# GPD_SPE_013 v1.1 Table 5-14
# SEID v1.1 Table 5-14
pass
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
# GPD_SPE_013 v1.1 Table 6-11
# SEID v1.1 Table 6-11
pass
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
# GPD_SPE_013 v1.1 Table 4-5
# SEID v1.1 Table 4-5
pass
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
# GPD_SPE_013 v1.1 Table 5-2
# SEID v1.1 Table 5-2
pass
class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
# GPD_SPE_013 v1.1 Table 5-4
# SEID v1.1 Table 5-4
pass
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
# GPD_SPE_013 V1.1 Table 5-6
# SEID V1.1 Table 5-6
pass
class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
# GPD_SPE_013 v1.1 Table 5-7
# SEID v1.1 Table 5-7
pass
class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
# GPD_SPE_013 v1.1 Table 5-8
# SEID v1.1 Table 5-8
pass
class CommandGetAll(BER_TLV_IE, tag=0xf4):
# GPD_SPE_013 v1.1 Table 5-9
# SEID v1.1 Table 5-9
pass
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
# GPD_SPE_013 v1.1 Table 5-10
# SEID v1.1 Table 5-10
pass
class CommandGetNext(BER_TLV_IE, tag=0xf5):
# GPD_SPE_013 v1.1 Table 5-11
# SEID v1.1 Table 5-11
pass
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
# GPD_SPE_013 v1.1 Table 5-12
# SEID v1.1 Table 5-12
pass
class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
# GPD_SPE_013 v1.1 Table 5-13
# SEID v1.1 Table 5-13
pass
class BlockDO(BER_TLV_IE, tag=0xe7):
# GPD_SPE_013 v1.1 Table 6-13
# SEID v1.1 Table 6-13
_construct = Struct('offset'/Int16ub, 'length'/Int8ub)
# GPD_SPE_013 v1.1 Table 4-1
# SEID v1.1 Table 4-1
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
pass
# SEID v1.1 Table 4-2
# GPD_SPE_013 v1.1 Table 4-2
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
ResponseRefreshTagDO, ResponseAramConfigDO]):
pass
# SEID v1.1 Table 5-1
# GPD_SPE_013 v1.1 Table 5-1
class StoreCommandDoCollection(TLV_IE_Collection,
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
@@ -244,7 +246,7 @@ class StoreCommandDoCollection(TLV_IE_Collection,
pass
# GPD_SPE_013 v1.1 Section 5.1.2
# SEID v1.1 Section 5.1.2
class StoreResponseDoCollection(TLV_IE_Collection,
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
pass
@@ -262,7 +264,7 @@ class ADF_ARAM(CardADF):
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,7 +276,7 @@ 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()
@@ -285,32 +287,32 @@ class ADF_ARAM(CardADF):
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):
"""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):
"""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())
@@ -320,9 +322,9 @@ class ADF_ARAM(CardADF):
'--device-app-id', required=True, help='Identifies the specific device application that the rule appplies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
aid_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
aid_grp.add_argument(
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 or 0 hex bytes)')
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 hex bytes)')
aid_grp.add_argument('--aid-empty', action='store_true',
help='No specific SE application, applies to implicitly selected application (all channels)')
help='No specific SE application, applies to all applications')
store_ref_ar_do_parse.add_argument(
'--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
# AR-DO
@@ -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')
@@ -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,23 +372,18 @@ 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):
"""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())
def do_aram_lock(self, opts):
"""Lock STORE DATA command to prevent unauthorized changes
(Proprietary feature that is specific to sysmocom's fork of Bertrand Martels ARA-M implementation.)"""
self._cmd.lchan.scc.send_apdu_checksw('80e2900001A1', '9000')
# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
sw_aram = {
@@ -420,84 +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
for d in dictlist:
if key in d:
obj = d.get(key)
if obj is None:
return ""
return obj
return None
@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)
aid_ref_empty_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_empty_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 is not None and len(aid_ref_do) > 0:
export_str += (" --aid %s" % aid_ref_do)
elif aid_ref_do is not None:
export_str += " --aid \"\""
if aid_ref_empty_do is not None:
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

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

@@ -23,11 +23,10 @@
#
from typing import Optional, Tuple
from osmocom.utils import *
from pySim.ts_102_221 import EF_DIR, CardProfileUICC
from pySim.ts_102_221 import EF_DIR
from pySim.ts_51_011 import DF_GSM
from pySim.utils import SwHexstr
from pySim.utils import *
from pySim.commands import Path, SimCardCommands
class CardBase:
@@ -55,17 +54,10 @@ class CardBase:
print("warning: erasing is not supported for specified card type!")
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]:
@@ -73,16 +65,6 @@ class CardBase:
# callers having to do hasattr('read_aids') ahead of every call.
return []
def adf_present(self, adf: str = "usim") -> bool:
# a non-UICC doesn't have any applications. Convenience helper to avoid
# callers having to do hasattr('adf_present') ahead of every call.
return False
def select_adf_by_aid(self, adf: str = "usim", scc: Optional[SimCardCommands] = None) -> Tuple[Optional[Hexstr], Optional[SwHexstr]]:
# a non-UICC doesn't have any applications. Convenience helper to avoid
# callers having to do hasattr('select_adf_by_aid') ahead of every call.
return (None, None)
class SimCardBase(CardBase):
"""Here we only add methods for commands specified in TS 51.011, without

View File

@@ -20,56 +20,41 @@ as described in 3GPP TS 31.111."""
from typing import List
from bidict import bidict
from construct import Int8ub, Int16ub, Byte, BitsInteger
from construct import Int8ub, Int16ub, Byte, Bytes, BitsInteger
from construct import Struct, Enum, BitStruct, this
from construct import Switch, GreedyRange, FlagsEnum
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
from osmocom.utils import b2h, h2b
from pySim.utils import dec_xplmn_w_act
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from pySim.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
from pySim.utils import b2h, dec_xplmn_w_act
# Tag values as per TS 101 220 Table 7.23
# TS 102 223 Section 8.1
class Address(COMPR_TLV_IE, tag=0x86):
_construct = Struct('ton_npi'/TonNpi,
'call_number'/BcdAdapter(GreedyBytes))
class Address(COMPR_TLV_IE, tag=0x06):
_construct = Struct('ton_npi'/Int8ub,
'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,
@@ -255,27 +240,24 @@ class Result(COMPR_TLV_IE, tag=0x83):
'launch_browser_generic_error': AddlInfoLaunchBrowser,
'bearer_independent_protocol_error': AddlInfoBip,
'frames_error': AddlInfoFrames
}, default=GreedyBytes))
}, default=HexAdapter(GreedyBytes)))
# TS 102 223 Section 8.13 + TS 31.111 Section 8.13
class SMS_TPDU(COMPR_TLV_IE, tag=0x8B):
_construct = Struct('tpdu'/GreedyBytes)
_construct = Struct('tpdu'/HexAdapter(GreedyBytes))
# TS 31.111 Section 8.14
class SsString(COMPR_TLV_IE, tag=0x89):
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/GreedyBytes)
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.15
class TextString(COMPR_TLV_IE, tag=0x8D):
_test_de_encode = [
( '8d090470617373776f7264', {'dcs': 4, 'text_string': b'password'} )
]
class TextString(COMPR_TLV_IE, tag=0x0d):
_construct = Struct('dcs'/Int8ub, # TS 03.38
'text_string'/GreedyBytes)
'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,13 +288,13 @@ 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'/GreedyBytes)
'ussd_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.18
class FileList(COMPR_TLV_IE, tag=0x92):
FileId=Bytes(2)
class FileList(COMPR_TLV_IE, tag=0x12):
FileId=HexAdapter(Bytes(2))
_construct = Struct('number_of_files'/Int8ub,
'files'/GreedyRange(FileId))
@@ -335,10 +317,10 @@ class NetworkMeasurementResults(COMPR_TLV_IE, tag=0x96):
# TS 102 223 Section 8.23
class DefaultText(COMPR_TLV_IE, tag=0x97):
_construct = Struct('dcs'/Int8ub,
'text_string'/GreedyBytes)
'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)
@@ -394,7 +376,7 @@ class ItemIconIdentifierList(COMPR_TLV_IE, tag=0x9f):
# TS 102 223 Section 8.35
class CApdu(COMPR_TLV_IE, tag=0xA2):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.37
class TimerIdentifier(COMPR_TLV_IE, tag=0xA4):
@@ -406,51 +388,28 @@ class TimerValue(COMPR_TLV_IE, tag=0xA5):
# TS 102 223 Section 8.40
class AtCommand(COMPR_TLV_IE, tag=0xA8):
_construct = GreedyBytes
_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 = GreedyBytes
_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 = 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': b'', 'bearer_type': 'default'} ),
]
# TS 31.111 Section 8.52.1
BearerParsCs = Struct('data_rate'/Int8ub,
'bearer_service'/Int8ub,
@@ -492,11 +451,11 @@ class BearerDescription(COMPR_TLV_IE, tag=0xB5):
'packet_grps_utran_eutran': BearerParsPacket,
'packet_with_extd_params': BearerParsPacketExt,
'ng_ran': BearerParsNgRan,
}, default=GreedyBytes))
}, default=HexAdapter(GreedyBytes)))
# TS 102 223 Section 8.53
class ChannelData(COMPR_TLV_IE, tag = 0xB6):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.54
class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
@@ -506,33 +465,26 @@ 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 = GreedyBytes
pass
# TS 102 223 Section 8.58
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
_test_de_encode = [
( 'be052101020304', {'address': h2b('01020304'), 'type_of_address': 'ipv4'} ),
]
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
'address'/GreedyBytes)
'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):
_construct = Struct('aid'/GreedyBytes)
class Aid(COMPR_TLV_IE, tag=0x2f):
_construct = Struct('aid'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.61
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
@@ -546,38 +498,35 @@ class ServiceRecord(COMPR_TLV_IE, tag=0xC1):
BearerTechId = Enum(Int8ub, technology_independent=0, bluetooth=1, irda=2, rs232=3, usb=4)
_construct = Struct('local_bearer_technology'/BearerTechId,
'service_identifier'/Int8ub,
'service_record'/GreedyBytes)
'service_record'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.64
class DeviceFilter(COMPR_TLV_IE, tag=0xC2):
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
'device_filter'/GreedyBytes)
'device_filter'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.65
class ServiceSearchIE(COMPR_TLV_IE, tag=0xC3):
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
'service_search'/GreedyBytes)
'service_search'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.66
class AttributeInformation(COMPR_TLV_IE, tag=0xC4):
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
'attribute_information'/GreedyBytes)
'attribute_information'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.68
class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
_construct = Struct('coding_type'/Enum(Int8ub, ieee802_16=0, irda=1),
'address'/GreedyBytes)
'address'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.70
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
_test_de_encode = [
( 'c704036e6161', h2b('036e6161') ),
]
_construct = GreedyBytes
_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,20 +562,20 @@ 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
class MultimediaMessageReference(COMPR_TLV_IE, tag=0xEA):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.83
class MultimediaMessageIdentifier(COMPR_TLV_IE, tag=0xEB):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.85
class MmContentIdentifier(COMPR_TLV_IE, tag=0xEE):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.89
class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
@@ -649,7 +598,7 @@ class ContactlessFunctionalityState(COMPR_TLV_IE, tag=0xD4):
# TS 31.111 Section 8.91
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
_construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
'lac'/Bytes(2),
'lac'/HexAdapter(Bytes(2)),
'rac'/Int8ub)
# TS 31.111 Section 8.92
@@ -709,23 +658,23 @@ class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
# TS 102 223 Section 8.99
class EncryptedTlvList(COMPR_TLV_IE, tag=0xA2):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.100
class Mac(COMPR_TLV_IE, tag=0xE0):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.101
class SaTemplate(COMPR_TLV_IE, tag=0xA3):
_construct = GreedyBytes
_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
class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.105
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
@@ -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
@@ -1240,7 +1003,7 @@ class ProactiveCommand(TLV_IE_Collection,
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()
@@ -1259,15 +1022,6 @@ class ProactiveCommand(TLV_IE_Collection,
def to_bytes(self, context: dict = {}):
return self.decoded.to_tlv()
# TS 101 223 Section 6.8.0
class TerminalResponse(TLV_IE_Collection,
nested=[CommandDetails, DeviceIdentities, Result,
Duration, TextString, ItemIdentifier,
#TODO: LocalInformation and other optional/conditional IEs
ChannelData, ChannelDataLength,
ChannelStatus, BufferSize, BearerDescription,
]):
pass
# reasonable default for playing with OTA
# 010203040506070809101112131415161718192021222324252627282930313233

View File

@@ -20,14 +20,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.ts_51_011 import CardProfileSIM
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
from pySim.ts_51_011 import EF_ServiceTable
from pySim.construct import *
# Mapping between CDMA Service Number and its description
@@ -115,7 +116,7 @@ class EF_AD(TransparentEF):
'''3.4.33 Administrative Data'''
_test_de_encode = [
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : b'\x00\x00', 'rfu' : b'' } ),
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
]
_test_no_pad = True
@@ -134,9 +135,9 @@ class EF_AD(TransparentEF):
# Byte 1: Display Condition
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information
'additional_info'/Bytes(2),
'additional_info'/HexAdapter(Bytes(2)),
# Bytes 4..: RFU
'rfu'/GreedyBytesRFU,
'rfu'/HexAdapter(GreedyBytesRFU),
)
@@ -178,7 +179,7 @@ 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",
@@ -189,12 +190,9 @@ class CardProfileRUIM(CardProfile):
# 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"])
@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

@@ -23,13 +23,12 @@
from typing import List, Tuple
import typing # construct also has a Union, so we do typing.Union below
from construct import Construct, Struct, Const, Select
from construct import Optional as COptional
from osmocom.construct import LV, filter_dict
from osmocom.utils import rpad, lpad, b2h, h2b, h2i, i2h, str_sanitize, Hexstr
from osmocom.tlv import bertlv_encode_len
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, SwMatchstr
from pySim.construct import LV, filter_dict
from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, h2i, i2h, str_sanitize, expand_hex, SwMatchstr
from pySim.utils import Hexstr, SwHexstr, ResTuple
from pySim.exceptions import SwMatchError
from pySim.transport import LinkBase
@@ -66,6 +65,7 @@ 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
@@ -75,10 +75,15 @@ class SimCardCommands:
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
ret = SimCardCommands(transport = self._tp, lchan_nr = lchan_nr)
ret.cla_byte = self.cla_byte
ret.cla_byte = self._cla_byte
ret.sel_ctrl = self.sel_ctrl
return ret
@property
def cla_byte(self) -> Hexstr:
"""Return the (cached) patched default CLA byte for this card."""
return self._cla4lchan
@property
def max_cmd_len(self) -> int:
"""Maximum length of the command apdu data section. Depends on secure channel protocol used."""
@@ -87,46 +92,58 @@ class SimCardCommands:
else:
return 255
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
@cla_byte.setter
def cla_byte(self, new_val: Hexstr):
"""Set the (raw, without lchan) default CLA value for this card."""
self._cla_byte = new_val
# compute cached result
self._cla4lchan = cla_with_lchan(self._cla_byte, self.lchan_nr)
def cla4lchan(self, cla: Hexstr) -> Hexstr:
"""Compute the lchan-patched value of the given CLA value. If no CLA
value is provided as argument, the lchan-patched version of the SimCardCommands._cla_byte
value is used. Most commands will use the latter, while some wish to override it and
can pass it as argument here."""
if not cla:
# return cached result to avoid re-computing this over and over again
return self._cla4lchan
else:
return cla_with_lchan(cla, self.lchan_nr)
def send_apdu(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if apply_lchan:
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
else:
return self._tp.send_apdu(pdu)
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000", apply_lchan:bool = True) -> ResTuple:
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
digits using a '?' to add some ambiguity if needed.
apply_lchan : apply the currently selected lchan to the CLA byte before sending
digits using a '?' to add some ambiguity if needed.
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if 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]:
cmd_data: Hexstr, resp_constr: Construct) -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
@@ -136,16 +153,14 @@ class SimCardCommands:
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
resp_cosntr : defining how to decode binary APDU response data
Returns:
Tuple of (decoded_data, sw)
"""
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
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)
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)))
@@ -155,7 +170,7 @@ class SimCardCommands:
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]:
sw_exp: SwMatchstr="9000") -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
@@ -170,8 +185,8 @@ class SimCardCommands:
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)
(rsp, sw) = self.send_apdu_constr(cla, ins,
p1, p2, cmd_constr, cmd_data, resp_constr)
if not sw_match(sw, sw_exp):
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
return (rsp, sw)
@@ -196,7 +211,6 @@ class SimCardCommands:
# 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.
# TODO: this likely just is normal BER-TLV ("All data objects are BER-TLV except if otherwise # defined.")
exp_tlv_len = int(fcp[2:4], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 4
@@ -204,7 +218,6 @@ class SimCardCommands:
exp_tlv_len = int(fcp[2:6], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 6
raise ValueError('Cannot determine length of TLV-length')
# Skip FCP tag and length
tlv = fcp[skip:]
@@ -248,7 +261,7 @@ class SimCardCommands:
if not isinstance(dir_list, 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.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
rv.append((data, sw))
if sw != '9000':
return rv
@@ -278,11 +291,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.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.send_apdu_checksw(self.cla_byte + "a4030400")
def select_adf(self, aid: Hexstr) -> ResTuple:
"""Execute SELECT a given Applicaiton ADF.
@@ -292,7 +305,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.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.
@@ -319,8 +332,8 @@ class SimCardCommands:
try:
data, sw = self.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)) from e
total_data += data
chunk_offset += chunk_len
return total_data, sw
@@ -379,8 +392,8 @@ class SimCardCommands:
try:
chunk_data, chunk_sw = self.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)) from e
total_data += data
chunk_offset += chunk_len
if verify:
@@ -495,9 +508,9 @@ 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'
pdu = self.cla4lchan('80') + 'cb000000'
return self.send_apdu_checksw(pdu)
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
@@ -527,7 +540,7 @@ class SimCardCommands:
p1 = 0x00
if isinstance(data, (bytes, bytearray)):
data = b2h(data)
pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data)
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
return self.send_apdu_checksw(pdu)
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
@@ -570,7 +583,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.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).
@@ -591,8 +604,6 @@ class SimCardCommands:
p2 = '81'
elif context == 'gsm':
p2 = '80'
else:
raise ValueError("Unsupported context '%s'" % context)
(data, sw) = self.send_apdu_constr_checksw(
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
if 'auts' in data:
@@ -603,7 +614,7 @@ 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.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."""
@@ -618,12 +629,12 @@ class SimCardCommands:
return self.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"""
"""Execute CREEATE FILE command as per TS 102 222 Section 6.3"""
return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
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.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"""
@@ -652,7 +663,7 @@ class SimCardCommands:
p1 = 0x80
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
return self.send_apdu_checksw(pdu)
def reset_card(self) -> Hexstr:
@@ -664,7 +675,7 @@ class SimCardCommands:
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)
raise SwMatchError(sw, '9000')
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
"""Verify a given CHV (Card Holder Verification == PIN)
@@ -735,7 +746,7 @@ class SimCardCommands:
Args:
payload : payload as hex string
"""
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload) + "00", apply_lchan = False)
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
def terminal_profile(self, payload: Hexstr) -> ResTuple:
"""Send TERMINAL PROFILE to card
@@ -744,7 +755,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.send_apdu(('80100000%02x' % data_length) + payload)
return (data, sw)
# ETSI TS 102 221 11.1.22
@@ -782,7 +793,7 @@ class SimCardCommands:
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.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)
@@ -792,12 +803,11 @@ 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.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.send_apdu('%02xca%04x00' % (cla, tag))
return (data, sw)
# TS 31.102 Section 7.5.2

587
pySim/construct.py Normal file
View File

@@ -0,0 +1,587 @@
"""Utility code related to the integration of the 'construct' declarative parser."""
import typing
import codecs
import ipaddress
import gsm0338
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
from construct import Adapter, Prefixed, Int8ub, GreedyBytes, Default, Flag, Byte, Construct, Enum
from construct import BitsInteger, BitStruct, Bytes, StreamError, stream_read_entire, stream_write
from construct import SizeofError, IntegerError, swapbytes
from construct.core import evaluate
from construct.lib import integertypes
from pySim.utils import b2h, h2b, swap_nibbles
# (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(instr, 'utf_16_be')
def _encode_variant2(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 2"""
codepoint_prefix = None
# second byte contains a value indicating the number of characters
hdr = b'\x81' + len(instr).to_bytes(1, byteorder='big')
chars = b''
for c in instr:
try:
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
assert codepoint_prefix == c_prefix
enc = (0x80 + (c_codepoint & 0x7f)).to_bytes(1, byteorder='big')
chars += enc
if codepoint_prefix is None:
codepoint_prefix = 0
return hdr + codepoint_prefix.to_bytes(1, byteorder='big') + chars
def _encode_variant3(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 3"""
# second byte contains a value indicating the number of characters
hdr = b'\x82' + len(instr).to_bytes(1, byteorder='big')
chars = b''
codepoint_list = codepoints_not_in_gsm338(instr)
codepoint_base = min(codepoint_list)
for c in instr:
try:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a GSM
# Default # Alphabet character
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
# if bit 8 of the byte is set to one, the remaining seven bits are an offset
# value added to the base pointer defined in bytes three and four, and the
# resultant 16 bit value is a UCS2 code point
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_codepoint_delta = c_codepoint - codepoint_base
assert c_codepoint_delta < 0x80
enc = (0x80 + c_codepoint_delta).to_bytes(1, byteorder='big')
chars += enc
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space
return hdr + codepoint_base.to_bytes(2, byteorder='big') + chars
if encodable_in_variant2(obj):
return _encode_variant2(obj)
elif encodable_in_variant3(obj):
return _encode_variant3(obj)
else:
return _encode_variant1(obj)
class BcdAdapter(Adapter):
"""convert a bytes() type to a string of BCD nibbles."""
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 is False:
obj[k] = True
elif v is 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
class StripTrailerAdapter(Adapter):
"""
Encoder removes all trailing bytes matching the default_value
Decoder pads input data up to total_length with default_value
This is used in constellations like "FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), ..."
where you have a bit-mask that may have 1, 2 or 3 bytes, depending on whether or not any
of the LSBs are actually set.
"""
def __init__(self, subcon, total_length:int, default_value=b'\x00', min_len=1):
super().__init__(subcon)
assert len(default_value) == 1
self.total_length = total_length
self.default_value = default_value
self.min_len = min_len
def _decode(self, obj, context, path):
assert isinstance(obj, bytes)
# pad with suppressed/missing bytes
if len(obj) < self.total_length:
obj += self.default_value * (self.total_length - len(obj))
return int.from_bytes(obj, 'big')
def _encode(self, obj, context, path):
assert isinstance(obj, int)
obj = obj.to_bytes(self.total_length, 'big')
# remove trailing bytes if they are zero
while len(obj) > self.min_len and obj[-1] == self.default_value[0]:
obj = obj[:-1]
return obj
def filter_dict(d, exclude_prefix='_'):
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
if not isinstance(d, dict):
return d
res = {}
for (key, value) in d.items():
if key.startswith(exclude_prefix):
continue
if isinstance(value, dict):
res[key] = filter_dict(value)
else:
res[key] = value
return res
def normalize_construct(c, exclude_prefix: str = '_'):
"""Convert a construct specific type to a related base type, mostly useful
so we can serialize it."""
# we need to include the filter_dict as we otherwise get elements like this
# in the dict: '_io': <_io.BytesIO object at 0x7fdb64e05860> which we cannot json-serialize
c = filter_dict(c, exclude_prefix)
if isinstance(c, (Container, dict)):
r = {k: normalize_construct(v) for (k, v) in c.items()}
elif isinstance(c, ListContainer):
r = [normalize_construct(x) for x in c]
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, exclude_prefix)
def build_construct(c, decoded_data, context: dict = {}):
"""Helper function to handle total_len."""
return c.build(decoded_data, total_len=None, **context)
# here we collect some shared / common definitions of data types
LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
# 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
nbytes = max(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) from e
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,53 +1,9 @@
import sys
from typing import Optional, Tuple
from typing import Optional
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,
}
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 = ''
@@ -58,11 +14,11 @@ 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)
return asn1tools.compile_string(asn_txt, codec='der')
# SGP.22 section 4.1 Activation Code
class ActivationCode:
"""SGP.22 section 4.1 Activation Code"""
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')
@@ -78,7 +34,6 @@ class ActivationCode:
@staticmethod
def decode_str(ac: str) -> dict:
"""decode an activation code from its string representation."""
if ac[0] != '1':
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
ac_elements = ac.split('$')

View File

@@ -1,9 +1,11 @@
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
where BPP is the Bound Profile Package. So the full expansion is the
"GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
Originally (SGP.22 v2.x) this was called SCP03t, but it has since been renamed to BSP."""
# Early proof-of-concept implementation of
# GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
# where BPP is the Bound Profile Package. So the full expansion is the
# "GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
#
# Originally (SGP.22 v2.x) this was called SCP03t, but it has since been
# renamed to BSP.
#
# (C) 2023 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
@@ -33,8 +35,7 @@ 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__)
@@ -43,7 +44,6 @@ logger.addHandler(logging.NullHandler())
MAX_SEGMENT_SIZE = 1020
class BspAlgo(abc.ABC):
"""Base class representing a cryptographic algorithm within the BSP (BPP Security Protocol)."""
blocksize: int
def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
@@ -61,7 +61,6 @@ class BspAlgo(abc.ABC):
return self.__class__.__name__
class BspAlgoCrypt(BspAlgo, abc.ABC):
"""Base class representing an encryption/decryption algorithm within the BSP (BPP Security Protocol)."""
def __init__(self, s_enc: bytes):
self.s_enc = s_enc
@@ -73,7 +72,7 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
block_nr = self.block_nr
ciphertext = self._encrypt(padded_data)
logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
block_nr, b2h(self.s_enc)[:20], b2h(data)[:20], b2h(padded_data)[:20], b2h(ciphertext)[:20])
block_nr, b2h(self.s_enc), b2h(data), b2h(padded_data), b2h(ciphertext))
return ciphertext
def decrypt(self, data:bytes) -> bytes:
@@ -93,7 +92,6 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
"""Actual implementation, to be implemented by derived class."""
class BspAlgoCryptAES128(BspAlgoCrypt):
"""AES-CBC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
name = 'AES-CBC-128'
blocksize = 16
@@ -134,7 +132,6 @@ class BspAlgoCryptAES128(BspAlgoCrypt):
class BspAlgoMac(BspAlgo, abc.ABC):
"""Base class representing a message authentication code algorithm within the BSP (BPP Security Protocol)."""
l_mac = 0 # must be overridden by derived class
def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
@@ -149,20 +146,10 @@ class BspAlgoMac(BspAlgo, abc.ABC):
temp_data = self.mac_chain + tag_and_length + data
old_mcv = self.mac_chain
c_mac = self._auth(temp_data)
# DEBUG: Show MAC computation details
logger.debug(f"MAC_DEBUG: tag=0x{tag:02x}, lcc={lcc}")
logger.debug(f"MAC_DEBUG: tag_and_length: {tag_and_length.hex()}")
logger.debug(f"MAC_DEBUG: mac_chain[:20]: {old_mcv[:20].hex()}")
logger.debug(f"MAC_DEBUG: temp_data[:20]: {temp_data[:20].hex()}")
logger.debug(f"MAC_DEBUG: c_mac: {c_mac.hex()}")
# The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value.
ret = tag_and_length + data + c_mac
logger.debug(f"MAC_DEBUG: final_output[:20]: {ret[:20].hex()}")
logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
tag, b2h(old_mcv)[:20], b2h(self.s_mac)[:20], b2h(data)[:20], b2h(temp_data)[:20], b2h(ret)[:20])
tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret))
return ret
def verify(self, ciphertext: bytes) -> bool:
@@ -179,7 +166,6 @@ class BspAlgoMac(BspAlgo, abc.ABC):
"""To be implemented by algorithm specific derived class."""
class BspAlgoMacAES128(BspAlgoMac):
"""AES-CMAC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
name = 'AES-CMAC-128'
l_mac = 8
@@ -214,11 +200,6 @@ def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, hos
s_enc = out[l:2*l]
s_mac = out[l*2:3*l]
logger.debug(f"BSP_KDF_DEBUG: kdf_out = {b2h(out)}")
logger.debug(f"BSP_KDF_DEBUG: initial_mcv = {b2h(initial_mac_chaining_value)}")
logger.debug(f"BSP_KDF_DEBUG: s_enc = {b2h(s_enc)}")
logger.debug(f"BSP_KDF_DEBUG: s_mac = {b2h(s_mac)}")
return s_enc, s_mac, initial_mac_chaining_value
@@ -243,24 +224,12 @@ class BspInstance:
return cls(s_enc, s_mac, initial_mcv)
def encrypt_and_mac_one(self, tag: int, plaintext:bytes) -> bytes:
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertext."""
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertex."""
assert tag <= 255
assert len(plaintext) <= self.max_payload_size
# DEBUG: Show what we're processing
logger.debug(f"BSP_DEBUG: encrypt_and_mac_one(tag=0x{tag:02x}, plaintext_len={len(plaintext)})")
logger.debug(f"BSP_DEBUG: plaintext[:20]: {plaintext[:20].hex()}")
logger.debug(f"BSP_DEBUG: s_enc[:20]: {self.c_algo.s_enc[:20].hex()}")
logger.debug(f"BSP_DEBUG: s_mac[:20]: {self.m_algo.s_mac[:20].hex()}")
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext)[:20])
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
ciphered = self.c_algo.encrypt(plaintext)
logger.debug(f"BSP_DEBUG: ciphered[:20]: {ciphered[:20].hex()}")
maced = self.m_algo.auth(tag, ciphered)
logger.debug(f"BSP_DEBUG: final_result[:20]: {maced[:20].hex()}")
logger.debug(f"BSP_DEBUG: final_result_len: {len(maced)}")
return maced
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
@@ -280,11 +249,11 @@ class BspInstance:
return result
def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
"""MAC a single plaintext TLV. Returns the protected ciphertext."""
"""MAC a single plaintext TLV. Returns the protected ciphertex."""
assert tag <= 255
assert len(plaintext) < self.max_payload_size
maced = self.m_algo.auth(tag, plaintext)
# The data block counter for ICV calculation is incremented also for each segment with C-MAC only.
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
self.c_algo.block_nr += 1
return maced
@@ -318,8 +287,6 @@ 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 calculation is incremented also for each segment with C-MAC only.
self.c_algo.block_nr += 1
return val
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:

View File

@@ -15,16 +15,96 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import requests
import logging
import json
from datetime import datetime
import time
from pySim.esim.http_json_api import *
import base64
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class ApiParam(abc.ABC):
"""A class reprsenting a single parameter in the ES2+ API."""
@classmethod
def verify_decoded(cls, data):
"""Verify the decoded reprsentation of a value. Should raise an exception if somthing is odd."""
pass
@classmethod
def verify_encoded(cls, data):
"""Verify the encoded reprsentation of a value. Should raise an exception if somthing is odd."""
pass
@classmethod
def encode(cls, data):
"""[Validate and] Encode the given value."""
cls.verify_decoded(data)
encoded = cls._encode(data)
cls.verify_decoded(encoded)
return encoded
@classmethod
def _encode(cls, data):
"""encoder function, typically [but not always] overridden by derived class."""
return data
@classmethod
def decode(cls, data):
"""[Validate and] Decode the given value."""
cls.verify_encoded(data)
decoded = cls._decode(data)
cls.verify_decoded(decoded)
return decoded
@classmethod
def _decode(cls, data):
"""decoder function, typically [but not always] overridden by derived class."""
return data
class ApiParamString(ApiParam):
"""Base class representing an API parameter of 'string' type."""
pass
class ApiParamInteger(ApiParam):
"""Base class representing an API parameter of 'integer' type."""
@classmethod
def _decode(cls, data):
return int(data)
@classmethod
def _encode(cls, data):
return str(data)
@classmethod
def verify_decoded(cls, data):
if not isinstance(data, int):
raise TypeError('Expected an integer input data type')
@classmethod
def verify_encoded(cls, data):
if not data.isdecimal():
raise ValueError('integer (%s) contains non-decimal characters' % data)
assert str(int(data)) == data
class ApiParamBoolean(ApiParam):
"""Base class representing an API parameter of 'boolean' type."""
@classmethod
def _encode(cls, data):
return bool(data)
class ApiParamFqdn(ApiParam):
"""String, as a list of domain labels concatenated using the full stop (dot, period) character as
separator between labels. Labels are restricted to the Alphanumeric mode character set defined in table 5
of ISO/IEC 18004"""
@classmethod
def verify_encoded(cls, data):
# FIXME
pass
class param:
class Iccid(ApiParamString):
"""String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
@@ -90,6 +170,9 @@ class param:
class SmdsAddress(ApiParamFqdn):
pass
class SmdpAddress(ApiParamFqdn):
pass
class ReleaseFlag(ApiParamBoolean):
pass
@@ -104,7 +187,7 @@ class param:
@classmethod
def _encode(cls, data):
return datetime.isoformat(data)
return datetime.toisoformat(data)
class NotificationPointId(ApiParamInteger):
pass
@@ -112,13 +195,149 @@ class param:
class NotificationPointStatus(ApiParam):
pass
class ResultData(ApiParamBase64):
pass
class ResultData(ApiParam):
@classmethod
def _decode(cls, data):
return base64.b64decode(data)
class Es2PlusApiFunction(JsonHttpApiFunction):
"""Base classs for representing an ES2+ API Function."""
@classmethod
def _encode(cls, data):
return base64.b64encode(data)
class JsonResponseHeader(ApiParam):
"""SGP.22 section 6.5.1.4."""
@classmethod
def verify_decoded(cls, data):
fe_status = data.get('functionExecutionStatus')
if not fe_status:
raise ValueError('Missing mandatory functionExecutionStatus in header')
status = fe_status.get('status')
if not status:
raise ValueError('Missing mandatory status in header functionExecutionStatus')
if status not in ['Executed-Success', 'Executed-WithWarning', 'Failed', 'Expired']:
raise ValueError('Unknown/unspecified status "%s"' % status)
class HttpStatusError(Exception):
pass
class HttpHeaderError(Exception):
pass
class Es2PlusApiError(Exception):
"""Exception representing an error at the ES2+ API level (status != Executed)."""
def __init__(self, func_ex_status: dict):
self.status = func_ex_status['status']
sec = {
'subjectCode': None,
'reasonCode': None,
'subjectIdentifier': None,
'message': None,
}
actual_sec = func_ex_status.get('statusCodeData', None)
sec.update(actual_sec)
self.subject_code = sec['subjectCode']
self.reason_code = sec['reasonCode']
self.subject_id = sec['subjectIdentifier']
self.message = sec['message']
def __str__(self):
return f'{self.status}("{self.subject_code}","{self.reason_code}","{self.subject_id}","{self.message}")'
class Es2PlusApiFunction(abc.ABC):
"""Base classs for representing an ES2+ API Function."""
# the below class variables are expected to be overridden in derived classes
path = None
# dictionary of input parameters. key is parameter name, value is ApiParam class
input_params = {}
# list of mandatory input parameters
input_mandatory = []
# dictionary of output parameters. key is parameter name, value is ApiParam class
output_params = {}
# list of mandatory output parameters (for successful response)
output_mandatory = []
# expected HTTP status code of the response
expected_http_status = 200
def __init__(self, url_prefix: str, func_req_id: str, session):
self.url_prefix = url_prefix
self.func_req_id = func_req_id
self.session = session
def encode(self, data: dict, func_call_id: str) -> dict:
"""Validate an encode input dict into JSON-serializable dict for request body."""
output = {
'header': {
'functionRequesterIdentifier': self.func_req_id,
'functionCallIdentifier': func_call_id
}
}
for p in self.input_mandatory:
if not p in data:
raise ValueError('Mandatory input parameter %s missing' % p)
for p, v in data.items():
p_class = self.input_params.get(p)
if not p_class:
logger.warning('Unexpected/unsupported input parameter %s=%s', p, v)
output[p] = v
else:
output[p] = p_class.encode(v)
return output
def decode(self, data: dict) -> dict:
"""[further] Decode and validate the JSON-Dict of the respnse body."""
output = {}
# let's first do the header, it's special
if not 'header' in data:
raise ValueError('Mandatory output parameter "header" missing')
hdr_class = self.output_params.get('header')
output['header'] = hdr_class.decode(data['header'])
if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
raise Es2PlusApiError(output['header']['functionExecutionStatus'])
# we can only expect mandatory parameters to be present in case of successful execution
for p in self.output_mandatory:
if p == 'header':
continue
if not p in data:
raise ValueError('Mandatory output parameter "%s" missing' % p)
for p, v in data.items():
p_class = self.output_params.get(p)
if not p_class:
logger.warning('Unexpected/unsupported output parameter "%s"="%s"', p, v)
output[p] = v
else:
output[p] = p_class.decode(v)
return output
def call(self, data: dict, func_call_id:str, timeout=10) -> dict:
"""Make an API call to the ES2+ API endpoint represented by this object.
Input data is passed in `data` as json-serializable dict. Output data
is returned as json-deserialized dict."""
url = self.url_prefix + self.path
encoded = json.dumps(self.encode(data, func_call_id))
headers = {
'Content-Type': 'application/json',
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
}
logger.debug("HTTP REQ %s - '%s'" % (url, encoded))
response = self.session.post(url, data=encoded, headers=headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
logger.debug("HTTP RSP: %s" % (response.content))
if response.status_code != self.expected_http_status:
raise HttpStatusError(response)
if not response.headers.get('Content-Type').startswith(headers['Content-Type']):
raise HttpHeaderError(response)
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
raise HttpHeaderError(response)
return self.decode(response.json())
# ES2+ DownloadOrder function (SGP.22 section 5.3.1)
class DownloadOrder(Es2PlusApiFunction):
path = '/gsma/rsp2/es2plus/downloadOrder'
@@ -128,7 +347,7 @@ class DownloadOrder(Es2PlusApiFunction):
'profileType': param.ProfileType
}
output_params = {
'header': JsonResponseHeader,
'header': param.JsonResponseHeader,
'iccid': param.Iccid,
}
output_mandatory = ['header', 'iccid']
@@ -146,10 +365,10 @@ class ConfirmOrder(Es2PlusApiFunction):
}
input_mandatory = ['iccid', 'releaseFlag']
output_params = {
'header': JsonResponseHeader,
'header': param.JsonResponseHeader,
'eid': param.Eid,
'matchingId': param.MatchingId,
'smdpAddress': SmdpAddress,
'smdpAddress': param.SmdpAddress,
}
output_mandatory = ['header', 'matchingId']
@@ -164,7 +383,7 @@ class CancelOrder(Es2PlusApiFunction):
}
input_mandatory = ['finalProfileStatusIndicator', 'iccid']
output_params = {
'header': JsonResponseHeader,
'header': param.JsonResponseHeader,
}
output_mandatory = ['header']
@@ -176,7 +395,7 @@ class ReleaseProfile(Es2PlusApiFunction):
}
input_mandatory = ['iccid']
output_params = {
'header': JsonResponseHeader,
'header': param.JsonResponseHeader,
}
output_mandatory = ['header']

View File

@@ -1,5 +1,6 @@
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+ as per SGP22 v3.0 Section 5.5"""
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+
# as per SGP22 v3.0 Section 5.5
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
@@ -16,17 +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
import logging
logger = logging.getLogger(__name__)
# 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
@@ -80,40 +74,14 @@ 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) StoreMetadataRequest DO (SGP.22 5.5.3)"""
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
smr = {
'iccid': self.iccid_bin,
'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)
@@ -199,12 +167,8 @@ class BoundProfilePackage(ProfilePackage):
# 'initialiseSecureChannelRequest'
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
# firstSequenceOf87
logger.debug(f"BPP_ENCODE_DEBUG: Encrypting ConfigureISDP with BSP keys")
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-ENC: {bsp.c_algo.s_enc.hex()}")
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-MAC: {bsp.m_algo.s_mac.hex()}")
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
# sequenceOF88
logger.debug(f"BPP_ENCODE_DEBUG: MAC-only StoreMetadata with BSP keys")
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
if self.ppp: # we have to use session keys
@@ -219,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 representing a single parameter in the API."""
@classmethod
def verify_decoded(cls, data):
"""Verify the decoded representation of a value. Should raise an exception if something is odd."""
pass
@classmethod
def verify_encoded(cls, data):
"""Verify the encoded representation of a value. Should raise an exception if something 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

@@ -1,5 +1,6 @@
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) as per SGP22 v3.0"""
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning)
# as per SGP22 v3.0
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
@@ -22,8 +23,6 @@ import shelve
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 pySim.esim import compile_asn1_subdir
@@ -37,7 +36,7 @@ class RspSessionState:
def __init__(self, transactionId: str, serverChallenge: bytes, ci_cert_id: bytes):
self.transactionId = transactionId
self.serverChallenge = serverChallenge
# used at a later point between API calls
# used at a later point between API calsl
self.ci_cert_id = ci_cert_id
self.euicc_cert: Optional[x509.Certificate] = None
self.eum_cert: Optional[x509.Certificate] = None
@@ -94,86 +93,6 @@ class RspSessionState:
self.__dict__.update(state)
class RspSessionStore:
"""A wrapper around the database-backed storage 'shelve' for storing RspSessionState objects.
Can be configured to use either file-based storage or in-memory storage.
We use it to store RspSessionState objects indexed by transactionId."""
def __init__(self, filename: Optional[str] = None, in_memory: bool = False):
self._in_memory = in_memory
if in_memory:
self._shelf = shelve.Shelf(dict())
else:
if filename is None:
raise ValueError("filename is required for file-based session store")
self._shelf = shelve.open(filename)
# dunder magic
def __getitem__(self, key):
return self._shelf[key]
def __setitem__(self, key, value):
self._shelf[key] = value
def __delitem__(self, key):
del self._shelf[key]
def __contains__(self, key):
return key in self._shelf
def __iter__(self):
return iter(self._shelf)
def __len__(self):
return len(self._shelf)
# everything else
def __getattr__(self, name):
"""Delegate attribute access to the underlying shelf object."""
return getattr(self._shelf, name)
def close(self):
"""Close the session store."""
if hasattr(self._shelf, 'close'):
self._shelf.close()
if self._in_memory:
# For in-memory store, clear the reference
self._shelf = None
def sync(self):
"""Synchronize the cache with the underlying storage."""
if hasattr(self._shelf, 'sync'):
self._shelf.sync()
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
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."""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
# Data sources: Provding data for profile personalization
#
# (C) 2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import secrets
from Cryptodome.Random import get_random_bytes
class DataSource(abc.ABC):
"""Base class for something that can provide data during a personalization process."""
@abc.abstractmethod
def generate_one(self):
pass
class DataSourceFixed(DataSource):
"""A data source that provides a fixed value (of any type).
Parameters:
fixed_value: The fixed value that shall be used during each data generation
"""
def __init__(self, fixed_value, **kwargs):
self.fixed_value = fixed_value
super().__init__(**kwargs)
def generate_one(self):
return self.fixed_value
class DataSourceIncrementing(DataSource):
"""A data source that provides incrementing integer numbers.
Parameters:
base_value: The start value (value returned during first data generation)
step_size: Increment step size (Default: 1)
"""
def __init__(self, base_value: int, **kwargs):
self.base_value = int(base_value)
self.step_size = kwargs.pop('step_size', 1)
self.i = 0
super().__init__(**kwargs)
def generate_one(self):
val = self.base_value + self.i
self.i += self.step_size
return val
class DataSourceRandomBytes(DataSource):
"""A data source that provides a configurable number of random bytes.
Parameters:
size: Number of bytes to generate each turn
"""
def __init__(self, size: int, **kwargs):
self.size = size
super().__init__(**kwargs)
def generate_one(self):
return get_random_bytes(self.size)
class DataSourceRandomUInt(DataSource):
"""A data source that provides a configurable unsigned integer value.
Parameters:
below: Number one greater than the maximum permitted random unsigned integer
"""
def __init__(self, below: int, **kwargs):
self.below = below
super().__init__(**kwargs)
def generate_one(self):
return secrets.randbelow(self.below)

View File

@@ -1,5 +1,5 @@
"""Implementation of SimAlliance/TCA Interoperable Profile OIDs"""
# 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
@@ -15,10 +15,8 @@
# 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]:
@@ -28,10 +26,6 @@ class OID:
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)
@@ -44,43 +38,6 @@ class OID:
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"""
@@ -96,18 +53,18 @@ 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")
ADF_USIM_not_by_default = eOID("2.5")
ADF_USIM_not_by_default_v2 = eOID("2.5.2")
ADF_USIM_not_by_default_v3 = eOID("2.5.3")
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
ADF_ISIM_by_default = eOID("2.8")
ADF_ISIMopt_not_by_default = eOID("2.9")
ADF_ISIMopt_not_by_default_v2 = eOID("2.9.2")
ADF_ISIM_not_by_default = eOID("2.9")
ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
ADF_CSIM_by_default = eOID("2.10")
ADF_CSIM_by_default_v2 = eOID("2.10.2")
ADF_CSIMopt_not_by_default = eOID("2.11")
ADF_CSIMopt_not_by_default_v2 = eOID("2.11.2")
ADF_CSIM_not_by_default = eOID("2.11")
ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
DF_EAP = eOID("2.12")
DF_5GS = eOID("2.13")
DF_5GS_v2 = eOID("2.13.2")
@@ -116,5 +73,5 @@ 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")
IoT_default = eOID("2.17")
IoT_default = eOID("2.18")

View File

@@ -1,5 +1,5 @@
"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile."""
# Implementation of SimAlliance/TCA Interoperable Profile handling
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
@@ -19,7 +19,7 @@ import abc
import io
from typing import List, Tuple
from osmocom.tlv import camel_to_snake
from pySim.tlv import camel_to_snake
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
from pySim.esim.saip import ProfileElement, ProfileElementSequence
@@ -67,7 +67,7 @@ class Iccid(ConfigurableParameter):
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 might be an integer
# 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')
@@ -86,7 +86,7 @@ class Imsi(ConfigurableParameter):
the last digit of the IMSI."""
def validate(self):
# convert to string as it might be an integer
# 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')
@@ -112,7 +112,7 @@ class SdKey(ConfigurableParameter, metaclass=ClassVarMeta):
key_id = None
kvn = None
key_usage_qual = None
permitted_len = []
permitted_len = None
def validate(self):
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
@@ -300,7 +300,7 @@ class Adm2(Pin, keyReference=0x0B):
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
"""Configurable Algorithm parameter."""
"""Configurable Algorithm parameter. bytes."""
key = None
def validate(self):
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):

View File

@@ -1,5 +1,5 @@
"""Implementation of SimAlliance/TCA Interoperable Profile Templates."""
# 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
@@ -17,36 +17,14 @@
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."""
"""Representation of a single file in a SimAlliance/TCA Profile Template."""
def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
pe_name:Optional[str] = None, 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.
"""
pe_name:Optional[str] = None):
# initialize from arguments
self.fid = fid
self.name = name
@@ -58,24 +36,18 @@ class FileTemplate:
if ftype in ['LF', 'CY']:
self.nb_rec = nb_rec
self.rec_len = size
elif ftype in ['TR', 'BT']:
elif ftype in ['TR']:
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)
@@ -84,122 +56,26 @@ class FileTemplate:
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
return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s)" % (self.name, self.pe_name, s_fid,
self.file_type, s_arr, s_sfi)
class ProfileTemplate:
"""Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
consists of a number of file definitions. We implement each profile template as a class derived from this
base class. Each such derived class is a singleton and has no instances."""
created_by_default: bool = False
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
files_by_pename: dict[str,FileTemplate] = {}
def __init_subclass__(cls, **kwargs):
"""This classmethod is called automatically after executing the subclass body. We use it to
initialize the cls.files_by_pename from the cls.files"""
super().__init_subclass__(**kwargs)
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."""
@@ -222,10 +98,10 @@ class ProfileTemplateRegistry:
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).
# of "Profile interoperability specification V3.1 Final" (unless other version explicitly specified).
# Section 9.2
class FilesAtMF(ProfileTemplate):
"""Files at MF as per Section 9.2"""
created_by_default = True
oid = OID.MF
files = [
@@ -238,8 +114,8 @@ class FilesAtMF(ProfileTemplate):
]
# Section 9.3
class FilesCD(ProfileTemplate):
"""Files at DF.CD as per Section 9.3"""
created_by_default = False
oid = OID.DF_CD
files = [
@@ -253,47 +129,46 @@ class FilesCD(ProfileTemplate):
# 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]),
FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size']),
]
for i in range(0x38, 0x40):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi']))
for i in range(0x40, 0x48):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
for i in range(0x48, 0x50):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
df_pb_files += [
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi'], 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]),
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi']),
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
]
for i in range(0x50, 0x58):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x58, 0x60):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x60, 0x68):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi']))
for i in range(0x68, 0x70):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x70, 0x78):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi']))
for i in range(0x78, 0x80):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x80, 0x88):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x88, 0x90):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi']))
for i in range(0x90, 0x98):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi']))
for i in range(0x98, 0xa0):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
# Section 9.4 v2.3.1
class FilesTelecom(ProfileTemplate):
"""Files at DF.TELECOM as per Section 9.4 v2.3.1"""
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(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
@@ -301,40 +176,38 @@ class FilesTelecom(ProfileTemplate):
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]),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size']),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'], ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
files += [deepcopy(x) for x in df_pb_files]
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], 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(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
]
# Section 9.4
class FilesTelecomV2(ProfileTemplate):
"""Files at DF.TELECOM as per Section 9.4"""
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(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
@@ -342,45 +215,45 @@ class FilesTelecomV2(ProfileTemplate):
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]),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size']),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'],ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
files += [deepcopy(x) for x in df_pb_files]
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], 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(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119], 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
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119]),
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119]),
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 2
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 3
]
# Section 9.5.1 v2.3.1
class FilesUsimMandatory(ProfileTemplate):
"""Mandatory Files at ADF.USIM as per Section 9.5.1 v2.3.1"""
created_by_default = True
oid = OID.ADF_USIM_by_default
files = [
@@ -410,8 +283,8 @@ class FilesUsimMandatory(ProfileTemplate):
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):
"""Mandatory Files at ADF.USIM as per Section 9.5.1"""
created_by_default = True
oid = OID.ADF_USIM_by_default_v2
files = [
@@ -442,13 +315,10 @@ class FilesUsimMandatoryV2(ProfileTemplate):
]
# Section 9.5.2 v2.3.1
class FilesUsimOptional(ProfileTemplate):
"""Optional Files at ADF.USIM as per Section 9.5.2 v2.3.1"""
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default
base_path = Path('ADF.USIM')
extends = FilesUsimMandatory
oid = OID.ADF_USIM_not_by_default
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
@@ -463,9 +333,9 @@ class FilesUsimOptional(ProfileTemplate):
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(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]),
@@ -529,12 +399,8 @@ class FilesUsimOptional(ProfileTemplate):
# Section 9.5.2
class FilesUsimOptionalV2(ProfileTemplate):
"""Optional Files at ADF.USIM as per Section 9.5.2"""
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default_v2
base_path = Path('ADF.USIM')
extends = FilesUsimMandatoryV2
oid = OID.ADF_USIM_not_by_default_v2
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
@@ -623,31 +489,18 @@ class FilesUsimOptionalV2(ProfileTemplate):
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
]
class FilesUsimOptionalV3(ProfileTemplate):
"""Optional Files at ADF.USIM as per Section 9.5.2.3 v3.3.1"""
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):
"""DF.PHONEBOOK Files at ADF.USIM as per Section 9.5.3"""
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):
"""DF.GSM-ACCESS Files at ADF.USIM as per Section 9.5.4"""
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),
@@ -657,12 +510,10 @@ class FilesUsimDfGsmAccess(ProfileTemplate):
]
# Section 9.5.11 v2.3.1
class FilesUsimDf5GS(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11 v2.3.1"""
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),
@@ -678,12 +529,10 @@ class FilesUsimDf5GS(ProfileTemplate):
]
# Section 9.5.11.2
class FilesUsimDf5GSv2(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.2"""
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),
@@ -701,12 +550,10 @@ class FilesUsimDf5GSv2(ProfileTemplate):
]
# Section 9.5.11.3
class FilesUsimDf5GSv3(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.3"""
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),
@@ -725,78 +572,19 @@ class FilesUsimDf5GSv3(ProfileTemplate):
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
]
class FilesUsimDf5GSv4(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.4"""
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):
"""DF.SAIP Files at ADF.USIM as per Section 9.5.12"""
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'),
FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF..FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
]
class FilesDfSnpn(ProfileTemplate):
"""DF.SNPN Files at ADF.USIM as per Section 9.5.13"""
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]),
]
class FilesDf5GProSe(ProfileTemplate):
"""DF.ProSe Files at ADF.USIM as per Section 9.5.14"""
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):
"""Mandatory Files at ADF.ISIM as per Section 9.6.1"""
created_by_default = True
oid = OID.ADF_ISIM_by_default
files = [
@@ -810,13 +598,10 @@ class FilesIsimMandatory(ProfileTemplate):
]
# Section 9.6.2 v2.3.1
class FilesIsimOptional(ProfileTemplate):
"""Optional Files at ADF.ISIM as per Section 9.6.2 of v2.3.1"""
created_by_default = False
optional = True
oid = OID.ADF_ISIMopt_not_by_default
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
oid = OID.ADF_ISIM_not_by_default
files = [
FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
@@ -830,13 +615,10 @@ class FilesIsimOptional(ProfileTemplate):
]
# Section 9.6.2
class FilesIsimOptionalv2(ProfileTemplate):
"""Optional Files at ADF.ISIM as per Section 9.6.2"""
created_by_default = False
optional = True
oid = OID.ADF_ISIMopt_not_by_default_v2
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
oid = OID.ADF_ISIM_not_by_default_v2
files = [
FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
@@ -858,8 +640,8 @@ class FilesIsimOptionalv2(ProfileTemplate):
# TODO: CSIM
# Section 9.8
class FilesEap(ProfileTemplate):
"""Files at DF.EAP as per Section 9.8"""
created_by_default = False
oid = OID.DF_EAP
files = [
@@ -867,8 +649,8 @@ class FilesEap(ProfileTemplate):
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(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']),
]
@@ -891,85 +673,3 @@ ARR_DEFINITION = {
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

@@ -1,5 +1,5 @@
"""Implementation of SimAlliance/TCA Interoperable Profile validation."""
# Implementation of SimAlliance/TCA Interoperable Profile handling
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
@@ -19,21 +19,16 @@
from pySim.esim.saip import *
class ProfileError(Exception):
"""Raised when a ProfileConstraintChecker finds an error in a file [structure]."""
pass
class ProfileConstraintChecker:
"""Base class of a constraint checker for a ProfileElementSequence."""
def check(self, pes: ProfileElementSequence):
"""Execute all the check_* methods of the ProfileConstraintChecker against the given
ProfileElementSequence"""
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(pes)
class CheckBasicStructure(ProfileConstraintChecker):
"""ProfileConstraintChecker for the basic profile structure constraints."""
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
opt_pe = pes.get_pe_for_type(opt)
if opt_pe:
@@ -43,7 +38,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
# FIXME: check order
def check_start_and_end(self, pes: ProfileElementSequence):
"""Check for mandatory header and end ProfileElements at the right position."""
if pes.pe_list[0].type != 'header':
raise ProfileError('first element is not header')
if pes.pe_list[1].type != 'mf':
@@ -53,7 +47,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
raise ProfileError('last element is not end')
def check_number_of_occurrence(self, pes: ProfileElementSequence):
"""Check The number of occurrence of various ProfileElements."""
# check for invalid number of occurrences
if len(pes.get_pes_for_type('header')) != 1:
raise ProfileError('multiple ProfileHeader')
@@ -67,7 +60,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
raise ProfileError('multiple PE-%s' % tn.upper())
def check_optional_ordering(self, pes: ProfileElementSequence):
"""Check the ordering of optional PEs following the respective mandatory ones."""
# ordering and required depenencies
self._is_after_if_exists(pes,'opt-usim', 'usim')
self._is_after_if_exists(pes,'opt-isim', 'isim')
@@ -103,30 +95,20 @@ class CheckBasicStructure(ProfileConstraintChecker):
if 'profile-a-p256' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
def check_identification_unique(self, pes: ProfileElementSequence):
"""Ensure that each PE has a unique identification value."""
id_list = [pe.header['identification'] for pe in pes.pe_list if pe.header]
if len(id_list) != len(set(id_list)):
raise ProfileError('PE identification values are not unique')
FileChoiceList = List[Tuple]
class FileError(ProfileError):
"""Raised when a FileConstraintChecker finds an error in a file [structure]."""
pass
class FileConstraintChecker:
def check(self, l: FileChoiceList):
"""Execute all the check_* methods of the FileConstraintChecker against the given FileChoiceList"""
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(l)
class FileCheckBasicStructure(FileConstraintChecker):
"""Validator for the basic structure of a decoded file."""
def check_seqence(self, l: FileChoiceList):
"""Check the sequence/ordering."""
by_type = {}
for k, v in l:
if k in by_type:

View File

@@ -1,5 +1,6 @@
"""Implementation of X.509 certificate handling in GSMA eSIM as per SGP22 v3.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
@@ -25,13 +26,12 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key, E
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from pySim.utils import b2h
from . import x509_err
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 cryptography 41.0.0 :(
# 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:
@@ -65,6 +65,9 @@ class oid:
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
@@ -133,7 +136,7 @@ class CertificateSet:
# we cannot check if there's no CRL
return
if self.crl.get_revoked_certificate_by_serial_number(cert.serial_nr):
raise x509_err.CertificateRevoked()
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
@@ -147,14 +150,14 @@ class CertificateSet:
check_signed(c, self.root_cert)
return
parent_cert = self.intermediate_certs.get(aki, None)
if not parent_cert:
raise x509_err.MissingIntermediateCert(b2h(aki))
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 x509_err.MaxDepthExceeded(max_depth, depth)
raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
def ecdsa_dss_to_tr03111(sig: bytes) -> bytes:
@@ -186,7 +189,7 @@ class CertAndPrivkey:
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 "concatenated raw R + S values". """
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)

View File

@@ -1,58 +0,0 @@
"""X.509 certificate verification exceptions for GSMA eSIM."""
class VerifyError(Exception):
"""Base class for certificate verification errors."""
pass
class MissingIntermediateCert(VerifyError):
"""Raised when an intermediate certificate in the chain cannot be found."""
def __init__(self, auth_key_id: str):
self.auth_key_id = auth_key_id
super().__init__(f'Could not find intermediate certificate for AuthKeyId {auth_key_id}')
class CertificateRevoked(VerifyError):
"""Raised when a certificate is found in the CRL."""
def __init__(self, cert_serial: str = None):
self.cert_serial = cert_serial
msg = 'Certificate is present in CRL, verification failed'
if cert_serial:
msg += f' (serial: {cert_serial})'
super().__init__(msg)
class MaxDepthExceeded(VerifyError):
"""Raised when certificate chain depth exceeds the maximum allowed."""
def __init__(self, max_depth: int, actual_depth: int):
self.max_depth = max_depth
self.actual_depth = actual_depth
super().__init__(f'Maximum depth {max_depth} exceeded while verifying certificate chain (actual: {actual_depth})')
class SignatureVerification(VerifyError):
"""Raised when certificate signature verification fails."""
def __init__(self, cert_subject: str = None, signer_subject: str = None):
self.cert_subject = cert_subject
self.signer_subject = signer_subject
msg = 'Certificate signature verification failed'
if cert_subject and signer_subject:
msg += f': {cert_subject} not signed by {signer_subject}'
super().__init__(msg)
class InvalidCertificate(VerifyError):
"""Raised when a certificate is invalid (missing required fields, wrong type, etc)."""
def __init__(self, reason: str):
self.reason = reason
super().__init__(f'Invalid certificate: {reason}')
class CertificateExpired(VerifyError):
"""Raised when a certificate has expired."""
def __init__(self, cert_subject: str = None):
self.cert_subject = cert_subject
msg = 'Certificate has expired'
if cert_subject:
msg += f': {cert_subject}'
super().__init__(msg)

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>
@@ -27,23 +25,13 @@ 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.tlv import *
from pySim.construct import *
from pySim.commands import SimCardCommands
from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
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):
@@ -120,7 +108,7 @@ class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetD
# SGP.22 Section 5.7.7: GetEUICCChallenge
class EuiccChallenge(BER_TLV_IE, tag=0x80):
_construct = Bytes(16)
_construct = HexAdapter(Bytes(16))
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
pass
@@ -128,7 +116,7 @@ class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
class SVN(BER_TLV_IE, tag=0x82):
_construct = VersionType
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
pass
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
@@ -140,15 +128,15 @@ class ProfileVersion(BER_TLV_IE, tag=0x81):
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
_construct = VersionType
class ExtCardResource(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class UiccCapability(BER_TLV_IE, tag=0x85):
_construct = GreedyBytes # FIXME
_construct = HexAdapter(GreedyBytes) # FIXME
class TS102241Version(BER_TLV_IE, tag=0x86):
_construct = VersionType
class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
_construct = VersionType
class RspCapability(BER_TLV_IE, tag=0x88):
_construct = GreedyBytes # FIXME
_construct = HexAdapter(GreedyBytes) # FIXME
class EuiccCategory(BER_TLV_IE, tag=0x8b):
_construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
class PpVersion(BER_TLV_IE, tag=0x04):
@@ -177,7 +165,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):
@@ -211,7 +199,7 @@ class TagList(BER_TLV_IE, tag=0x5c):
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
pass
class IsdpAid(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class ProfileState(BER_TLV_IE, tag=0x9f70):
_construct = Enum(Int8ub, disabled=0, enabled=1)
class ProfileNickname(BER_TLV_IE, tag=0x90):
@@ -268,20 +256,9 @@ class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
pass
# SGP.22 Section 5.7.19: EuiccMemoryReset
class ResetOptions(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Byte, deleteOperationalProfiles=0x80, deleteFieldLoadedTestProfiles=0x40,
resetDefaultSmdpAddress=0x20)
class ResetResult(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, nothingToDelete=1, undefinedError=127)
class EuiccMemoryResetReq(BER_TLV_IE, tag=0xbf34, nested=[ResetOptions]):
pass
class EuiccMemoryResetResp(BER_TLV_IE, tag=0xbf34, nested=[ResetResult]):
pass
# SGP.22 Section 5.7.20 GetEID
class EidValue(BER_TLV_IE, tag=0x5a):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
pass
@@ -302,7 +279,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
@@ -315,9 +292,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)
@@ -336,14 +313,12 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
desc='ISD-R (Issuer Security Domain Root) Application')
self.adf.decode_select_response = self.decode_select_response
self.adf.shell_commands += [self.AddlShellCommands()]
# we attempt to retrieve ISD-R key material from CardKeyProvider identified by EID
self.adf.scp_key_identity = 'EID'
@staticmethod
def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
Only single-block store supported for now."""
capdu = '80E29100%02x%s00' % (len(tx_do)//2, tx_do)
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
return scc.send_apdu_checksw(capdu, exp_sw)
@staticmethod
@@ -368,13 +343,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 b2h(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))
@@ -461,11 +429,8 @@ 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)
@@ -483,11 +448,8 @@ 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)
@@ -504,52 +466,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)
d = dp.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['delete_profile_resp']))
mem_res_parser = argparse.ArgumentParser()
mem_res_parser.add_argument('--delete-operational', action='store_true',
help='Delete all operational profiles')
mem_res_parser.add_argument('--delete-test-field-installed', action='store_true',
help='Delete all test profiles, except pre-installed ones')
mem_res_parser.add_argument('--reset-smdp-address', action='store_true',
help='Reset the SM-DP+ address')
@cmd2.with_argparser(mem_res_parser)
def do_euicc_memory_reset(self, opts):
"""Perform an ES10c eUICCMemoryReset function. This will permanently delete the selected subset of
profiles from the eUICC."""
flags = {}
if opts.delete_operational:
flags['deleteOperationalProfiles'] = True
if opts.delete_test_field_installed:
flags['deleteFieldLoadedTestProfiles'] = True
if opts.reset_smdp_address:
flags['resetDefaultSmdpAddress'] = True
mr_cmd = EuiccMemoryResetReq(children=[ResetOptions(decoded=flags)])
mr = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, mr_cmd, EuiccMemoryResetResp)
d = mr.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['euicc_memory_reset_resp']))
def do_get_eid(self, _opts):
"""Perform an ES10c GetEID function."""
(_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
ged = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
d = ged.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_data']))
set_nickname_parser = argparse.ArgumentParser()
set_nickname_parser.add_argument('--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):
@@ -565,7 +501,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c GetCerts() function on an IoT eUICC."""
gc = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
d = gc.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['get_certs_resp']))
self._cmd.poutput_json(flatten_dict_lists(d['get_certficiates_resp']))
def do_get_eim_configuration_data(self, _opts):
"""Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
@@ -586,49 +522,7 @@ class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
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 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

@@ -49,18 +49,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
@@ -35,14 +35,10 @@ import cmd2
from cmd2 import CommandSet, with_default_category
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.utils import sw_match, h2b, b2h, is_hex, auto_int, auto_uint8, auto_uint16, is_hexstr
from pySim.construct import filter_dict, parse_construct, build_construct
from pySim.jsonpath import js_path_modify
from pySim.commands import SimCardCommands
from pySim.exceptions import SwMatchError
# int: a single service is associated with this file
# list: any of the listed services requires this file
@@ -65,7 +61,7 @@ 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
@@ -179,7 +175,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:
@@ -299,17 +295,6 @@ class CardFile:
return True
raise ValueError("self.service must be either int or list or tuple")
@staticmethod
def export(as_json: bool, lchan):
r"""
Export file contents in the form of commandline script. This method is meant to be overloaded by a subclass in
case any exportable contents are present. The generated script may contain multiple command lines separated by
line breaks ("\n"), where the last commandline shall have no line break at the end
(e.g. "update_record 1 112233\nupdate_record 1 445566"). Naturally this export method will always refer to the
currently selected file of the presented lchan.
"""
return "# %s has no exportable contents" % str(lchan.selected_file)
class CardDF(CardFile):
"""DF (Dedicated File) in the smart card filesystem. Those are basically sub-directories."""
@@ -527,7 +512,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 +527,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"""
@@ -627,19 +603,19 @@ class TransparentEF(CardEF):
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')
upd_bin_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(upd_bin_parser)
def do_update_binary(self, opts):
"""Update (Write) data of a transparent EF"""
(data, _sw) = self._cmd.lchan.update_binary(opts.DATA, opts.offset)
(data, _sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
if data:
self._cmd.poutput(data)
upd_bin_dec_parser = argparse.ArgumentParser()
upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
upd_bin_dec_parser.add_argument('--json-path', type=str,
help='JSON path to modify specific element of file only')
upd_bin_dec_parser.add_argument('DATA', help='Abstract data (JSON format) to write')
@cmd2.with_argparser(upd_bin_dec_parser)
def do_update_binary_decoded(self, opts):
@@ -647,9 +623,9 @@ class TransparentEF(CardEF):
if opts.json_path:
(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_json = json.loads(opts.data)
(data, _sw) = self._cmd.lchan.update_binary_dec(data_json)
if data:
self._cmd.poutput_json(data)
@@ -661,7 +637,7 @@ class TransparentEF(CardEF):
filename = '%s/file' % dirname
# write existing data as JSON to file
with open(filename, 'w') as text_file:
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
json.dump(orig_json, text_file, indent=4)
# run a text editor
self._cmd.run_editor(filename)
with open(filename, 'r') as text_file:
@@ -743,26 +719,7 @@ class TransparentEF(CardEF):
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,18 +728,17 @@ 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)})
return build_construct(self._construct, abstract_data)
if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
@@ -790,7 +746,7 @@ class TransparentEF(CardEF):
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,19 +755,18 @@ 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)}))
return b2h(build_construct(self._construct, abstract_data))
if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
@@ -819,25 +774,6 @@ class TransparentEF(CardEF):
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.
@@ -861,15 +797,15 @@ 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=auto_uint8, 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=auto_uint8, 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
recnr = opts.record_nr + r
(data, _sw) = self._cmd.lchan.read_record(recnr)
if len(data) > 0:
recstr = str(data)
@@ -878,15 +814,15 @@ class LinFixedEF(CardEF):
self._cmd.poutput("%03d %s" % (recnr, recstr))
read_rec_dec_parser = argparse.ArgumentParser()
read_rec_dec_parser.add_argument(
'record_nr', type=auto_uint8, 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()
@@ -920,50 +856,50 @@ class LinFixedEF(CardEF):
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=auto_uint8, help='Number of record to be read')
upd_rec_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(upd_rec_parser)
def do_update_record(self, opts):
"""Update (write) data to a record-oriented EF"""
(data, _sw) = self._cmd.lchan.update_record(opts.RECORD_NR, opts.DATA)
(data, _sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
if data:
self._cmd.poutput(data)
upd_rec_dec_parser = argparse.ArgumentParser()
upd_rec_dec_parser.add_argument(
'record_nr', type=auto_uint8, 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)
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=auto_uint8, help='Number of record to be edited')
@cmd2.with_argparser(edit_rec_dec_parser)
def do_edit_record_decoded(self, opts):
"""Edit the JSON representation of one record in an editor."""
(orig_json, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
(orig_json, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
with open(filename, 'w') as text_file:
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
json.dump(orig_json, text_file, indent=4)
# run a text editor
self._cmd.run_editor(filename)
with open(filename, 'r') as text_file:
@@ -972,7 +908,7 @@ class LinFixedEF(CardEF):
self._cmd.poutput("Data not modified, skipping write")
else:
(data, _sw) = self._cmd.lchan.update_record_dec(
opts.RECORD_NR, edited_json)
opts.record_nr, edited_json)
if data:
self._cmd.poutput_json(data)
@@ -1051,26 +987,7 @@ class LinFixedEF(CardEF):
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,19 +997,18 @@ 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)}))
return b2h(build_construct(self._construct, abstract_data))
if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
@@ -1100,7 +1016,7 @@ class LinFixedEF(CardEF):
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,18 +1026,17 @@ 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)})
return build_construct(self._construct, abstract_data)
if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
@@ -1129,54 +1044,6 @@ class LinFixedEF(CardEF):
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"""
@@ -1224,13 +1091,6 @@ class TransRecEF(TransparentEF):
Returns:
abstract_data; dict representing the decoded data
"""
# The record data length should always be equal or at least greater than the record length defined for the
# TransRecEF. Short records may be occur when the length of the underlying TransparentEF is not a multiple
# of the TransRecEF record length.
if len(raw_hex_data) // 2 < self.__get_rec_len():
return {'raw': raw_hex_data}
method = getattr(self, '_decode_record_hex', None)
if callable(method):
return method(raw_hex_data)
@@ -1258,11 +1118,6 @@ class TransRecEF(TransparentEF):
Returns:
abstract_data; dict representing the decoded data
"""
# See comment in decode_record_hex (above)
if len(raw_bin_data) < self.__get_rec_len():
return {'raw': b2h(raw_bin_data)}
method = getattr(self, '_decode_record_bin', None)
if callable(method):
return method(raw_bin_data)
@@ -1278,20 +1133,7 @@ class TransRecEF(TransparentEF):
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()
@@ -1300,19 +1142,17 @@ 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)})))
return b2h(filter_dict(build_construct(self._construct, abstract_data)))
if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
@@ -1320,7 +1160,7 @@ class TransRecEF(TransparentEF):
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()
@@ -1329,19 +1169,17 @@ 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)}))
return filter_dict(build_construct(self._construct, abstract_data))
if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
@@ -1354,8 +1192,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)
@@ -1374,12 +1212,12 @@ class BerTlvEF(CardEF):
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):
@@ -1389,33 +1227,27 @@ class BerTlvEF(CardEF):
set_data_parser = argparse.ArgumentParser()
set_data_parser.add_argument(
'TAG', type=auto_int, help='BER-TLV Tag of value to set')
'tag', type=auto_int, help='BER-TLV Tag of value to set')
set_data_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(set_data_parser)
def do_set_data(self, opts):
"""Set (Write) data for a given tag in a BER-TLV EF"""
(data, _sw) = self._cmd.lchan.set_data(opts.TAG, opts.data)
(data, _sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
if data:
self._cmd.poutput(data)
del_data_parser = argparse.ArgumentParser()
del_data_parser.add_argument(
'TAG', type=auto_int, help='BER-TLV Tag of value to set')
'tag', type=auto_int, help='BER-TLV Tag of value to set')
@cmd2.with_argparser(del_data_parser)
def do_delete_data(self, opts):
"""Delete data for a given tag in a BER-TLV EF"""
(data, _sw) = self._cmd.lchan.set_data(opts.TAG, None)
"""Delete data for a given tag in a BER-TLV EF"""
(data, _sw) = self._cmd.lchan.set_data(opts.tag, None)
if data:
self._cmd.poutput(data)
def do_delete_all(self, opts):
"""Delete all data from a BER-TLV EF"""
tags = self._cmd.lchan.retrieve_tags()
for tag in tags:
self._cmd.lchan.set_data(tag, None)
def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None, parent: CardDF = None,
size: Size = (1, None), **kwargs):
"""
@@ -1432,34 +1264,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.
@@ -1490,8 +1294,6 @@ 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 {}
@@ -1516,15 +1318,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
@@ -1542,7 +1335,8 @@ class CardModel(abc.ABC):
"""Test if given card matches this model."""
card_atr = scc.get_atr()
for atr in cls._atrs:
if atr == card_atr:
atr_bin = toBytes(atr)
if atr_bin == card_atr:
print("Detected CardModel:", cls.__name__)
return True
return False
@@ -1555,53 +1349,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

View File

@@ -24,90 +24,12 @@ 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.global_platform.install_param import gen_install_parameters
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from pySim.profile import CardProfile
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
from pySim.javacard import CapFile
# 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': {
@@ -318,7 +240,7 @@ class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
class ApplicationAID(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
pass
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
@@ -425,10 +347,10 @@ class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
pass
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class CardImageNumber(BER_TLV_IE, tag=0x45):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
_construct = GreedyInteger()
@@ -467,7 +389,8 @@ class LifeCycleState(BER_TLV_IE, tag=0x9f70):
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
class Privileges(BER_TLV_IE, tag=0xc5):
_construct = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3, steps = [1, 3]),
# 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,
@@ -487,7 +410,7 @@ class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
@@ -495,15 +418,15 @@ class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
# 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 = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
@@ -512,6 +435,23 @@ class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCy
ExecutableModuleAID, AssociatedSecurityDomainAID]):
pass
# Section 11.6.2.3 / Table 11-58
class SecurityDomainAid(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
class LoadFileDataBlockSignature(BER_TLV_IE, tag=0xc3):
_construct = GreedyBytes
class DapBlock(BER_TLV_IE, tag=0xe2, nested=[SecurityDomainAid, LoadFileDataBlockSignature]):
pass
class LoadFileDataBlock(BER_TLV_IE, tag=0xc4):
_construct = GreedyBytes
class Icv(BER_TLV_IE, tag=0xd3):
_construct = GreedyBytes
class CipheredLoadFileDataBlock(BER_TLV_IE, tag=0xd4):
_construct = GreedyBytes
class LoadFile(TLV_IE_Collection, nested=[DapBlock, LoadFileDataBlock, Icv, CipheredLoadFileDataBlock]):
pass
# Application Dedicated File of a Security Domain
class ADF_SD(CardADF):
StoreData = BitStruct('last_block'/Flag,
@@ -582,7 +522,7 @@ class ADF_SD(CardADF):
{'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")
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk))
block_nr += 1
response += data
return data
@@ -591,7 +531,7 @@ class ADF_SD(CardADF):
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-type', choices=KeyType.ksymapping.values(), action='append', required=True, help='Key Type')
put_key_parser.add_argument('--key-data', type=is_hexstr, action='append', required=True, help='Key Data Block')
put_key_parser.add_argument('--key-check', type=is_hexstr, action='append', help='Key Check Value')
put_key_parser.add_argument('--suppress-key-check', action='store_true', help='Suppress generation of Key Check Values')
@@ -628,7 +568,7 @@ class ADF_SD(CardADF):
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])))
kcb = b2h(self._cmd.lchan.scc.scp.card_keys.encrypt_key(h2b(opts.key_data[i])))
else:
# (for example) during personalization, DEK might not be required)
kcb = opts.key_data[i]
@@ -640,19 +580,19 @@ class ADF_SD(CardADF):
# Table 11-68: Key Data Field - Format 1 (Basic Format)
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
'kcb'/Prefixed(Int8ub, GreedyBytes),
'kcv'/Prefixed(Int8ub, GreedyBytes)))
'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")
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data))
return data
get_status_parser = argparse.ArgumentParser()
get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()),
get_status_parser.add_argument('subset', choices=StatusSubset.ksymapping.values(),
help='Subset of statuses to be included in the response')
get_status_parser.add_argument('--aid', type=is_hexstr, default='',
help='AID Search Qualifier (search only for given AID)')
@@ -673,7 +613,7 @@ class ADF_SD(CardADF):
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")
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data))
remainder = h2b(data)
while len(remainder):
# tlv sequence, each element is one GpRegistryRelatedData()
@@ -687,9 +627,9 @@ class ADF_SD(CardADF):
return grd_list
set_status_parser = argparse.ArgumentParser()
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
set_status_parser.add_argument('scope', choices=SetStatusScope.ksymapping.values(),
help='Defines the scope of the requested status change')
set_status_parser.add_argument('status', choices=list(CLifeCycleState.ksymapping.values()),
set_status_parser.add_argument('status', choices=CLifeCycleState.ksymapping.values(),
help='Specify the new intended status')
set_status_parser.add_argument('--aid', type=is_hexstr,
help='AID of the target Application or Security Domain')
@@ -704,12 +644,12 @@ class ADF_SD(CardADF):
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
'scope'/SetStatusScope, 'status'/CLifeCycleState,
'aid'/Prefixed(Int8ub, COptional(GreedyBytes)))
'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')
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):
@@ -726,9 +666,9 @@ class ADF_SD(CardADF):
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 (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
help='Install Parameters')
inst_inst_parser.add_argument('--privilege', action='append', dest='privileges', default=[],
choices=list(Privileges._construct.flags.keys()),
choices=Privileges._construct.flags.keys(),
help='Privilege granted to newly installed Application')
inst_inst_parser.add_argument('--install-token', type=is_hexstr, default='',
help='Install Token (Section GPCS C.4.2/C.4.7)')
@@ -738,12 +678,12 @@ class ADF_SD(CardADF):
@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'/Prefixed(Int8ub, GreedyBytes),
'module_aid'/Prefixed(Int8ub, GreedyBytes),
'application_aid'/Prefixed(Int8ub, GreedyBytes),
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'/Prefixed(Int8ub, GreedyBytes),
'install_token'/Prefixed(Int8ub, GreedyBytes))
'install_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'install_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
p1 = 0x04
if opts.make_selectable:
p1 |= 0x08
@@ -759,27 +699,27 @@ class ADF_SD(CardADF):
inst_load_parser.add_argument('--security-domain-aid', type=is_hexstr, default='',
help='AID of the Security Domain into which the file shalle be added')
inst_load_parser.add_argument('--load-file-hash', type=is_hexstr, default='',
help='Load File Data Block Hash (GPC_SPE_034, section C.2)')
inst_load_parser.add_argument('--load-parameters', type=is_hexstr, default='',
help='Load Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
inst_load_parser.add_argument('--load-token', type=is_hexstr, default='',
help='Load Token (GPC_SPE_034, section C.4.1)')
help='Load File Data Block Hash')
inst_inst_parser.add_argument('--load-parameters', type=is_hexstr, default='',
help='Load Token (Section GPCS C.4.1)')
inst_inst_parser.add_argument('--load-token', type=is_hexstr, default='',
help='Load Token (Section GPCS C.4.1)')
@cmd2.with_argparser(inst_load_parser)
def do_install_for_load(self, opts):
"""Perform GlobalPlatform INSTALL [for load] command in order to prepare to load an application."""
"""Perform GlobalPlatform INSTALL [for load] command."""
if opts.load_token != '' and opts.load_file_hash == '':
raise ValueError('Load File Data Block Hash is mandatory if a Load Token is present')
InstallForLoadCD = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
'security_domain_aid'/Prefixed(Int8ub, GreedyBytes),
'load_file_hash'/Prefixed(Int8ub, GreedyBytes),
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
'load_token'/Prefixed(Int8ub, GreedyBytes))
InstallForLoadCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'security_domain_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'load_file_hash'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'load_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
'load_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
self.install(0x02, 0x00, b2h(ifl_bytes))
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
del_cc_parser = argparse.ArgumentParser()
@@ -818,32 +758,24 @@ class ADF_SD(CardADF):
self.delete(0x00, p2, cmd)
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
load_parser = argparse.ArgumentParser()
load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True)
load_parser_from_grp.add_argument('--from-hex', type=is_hexstr, help='load from hex string')
load_parser_from_grp.add_argument('--from-file', type=argparse.FileType('rb', 0), help='load from binary file')
load_parser_from_grp.add_argument('--from-cap-file', type=argparse.FileType('rb', 0), help='load from JAVA-card CAP file')
# we make this a required --optional argument now, so we can later have other sources for load data
load_parser.add_argument('--from-file', required=True)
@cmd2.with_argparser(load_parser)
def do_load(self, opts):
"""Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and
without ciphering.)"""
if opts.from_hex is not None:
self.load(h2b(opts.from_hex))
elif opts.from_file is not None:
self.load(opts.from_file.read())
elif opts.from_cap_file is not None:
cap = CapFile(opts.from_cap_file)
self.load(cap.get_loadfile())
else:
raise ValueError('load source not specified!')
"""Perform a GlobalPlatform LOAD command. We currently only support loading without DAP and
without ciphering."""
with open(opts.from_file, 'rb') as f:
self.load(f)
def load(self, contents:bytes, chunk_len:int = 240):
# TODO:tune chunk_len based on the overhead of the used SCP?
# build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case
def load(self, stream: io.RawIOBase, chunk_len:int = 240):
# we might want to tune chunk_len based on the overhead of the used SCP?
contents = stream.readall()
# build TLV according to 11.6.2.3 / Table 11-58 for unencrypted case
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
# transfer this in vaious chunks to the card
total_size = len(remainder)
@@ -851,94 +783,33 @@ class ADF_SD(CardADF):
while len(remainder):
block = remainder[:chunk_len]
remainder = remainder[chunk_len:]
# build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56
p1 = 0x00 if len(remainder) else 0x80
# build LOAD command APDU according to 11.6.2 / Table 11-56
p1 = 0x80 if len(remainder) else 0x00
p2 = block_nr % 256
block_nr += 1
cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block))
cmd_hex = "80E8%02x%02x%02x%s" % (p1, p2, len(block), b2h(block))
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!" % (total_size, block_nr))
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_load now!" % (total_size, block_nr))
install_cap_parser = argparse.ArgumentParser()
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
help='JAVA-CARD CAP file to install')
install_cap_parser_inst_prm_g = install_cap_parser.add_mutually_exclusive_group()
install_cap_parser_inst_prm_g.add_argument('--install-parameters', type=is_hexstr, default=None,
help='install Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_g_grp = install_cap_parser_inst_prm_g.add_argument_group()
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-volatile-memory-quota',
type=int, default=None,
help='volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-non-volatile-memory-quota',
type=int, default=None,
help='non volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-stk',
type=is_hexstr, default=None,
help='Load Parameters (ETSI TS 102 226, section 8.2.1.3.2.1)')
@cmd2.with_argparser(install_cap_parser)
def do_install_cap(self, opts):
"""Perform a .cap file installation using GlobalPlatform LOAD and INSTALL commands."""
self._cmd.poutput("loading cap file: %s ..." % opts.cap_file)
cap = CapFile(opts.cap_file)
security_domain_aid = self._cmd.lchan.selected_file.aid
load_file = cap.get_loadfile()
load_file_aid = cap.get_loadfile_aid()
module_aid = cap.get_applet_aid()
application_aid = module_aid
if opts.install_parameters:
install_parameters = opts.install_parameters;
else:
install_parameters = gen_install_parameters(opts.install_parameters_non_volatile_memory_quota,
opts.install_parameters_volatile_memory_quota,
opts.install_parameters_stk)
self._cmd.poutput("parameters:")
self._cmd.poutput(" security-domain-aid: %s" % security_domain_aid)
self._cmd.poutput(" load-file: %u bytes" % len(load_file))
self._cmd.poutput(" load-file-aid: %s" % load_file_aid)
self._cmd.poutput(" module-aid: %s" % module_aid)
self._cmd.poutput(" application-aid: %s" % application_aid)
self._cmd.poutput(" install-parameters: %s" % install_parameters)
self._cmd.poutput("step #1: install for load...")
self.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid))
self._cmd.poutput("step #2: load...")
self.load(load_file)
self._cmd.poutput("step #3: install_for_install (and make selectable)...")
self.do_install_for_install("--load-file-aid %s --module-aid %s --application-aid %s --install-parameters %s --make-selectable" %
(load_file_aid, module_aid, application_aid, install_parameters))
self._cmd.poutput("done.")
est_scp02_parser = argparse.ArgumentParser()
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, default=0, help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True,
help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--key-enc', type=is_hexstr, required=True,
help='Secure Channel Encryption Key')
est_scp02_parser.add_argument('--key-mac', type=is_hexstr, required=True,
help='Secure Channel MAC Key')
est_scp02_parser.add_argument('--key-dek', type=is_hexstr, required=True,
help='Data Encryption Key')
est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
help='Hard-code the host challenge; default: random')
est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
help='Security Level. Default: 0x01 (C-MAC only)')
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
@@ -948,24 +819,12 @@ class ADF_SD(CardADF):
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
@@ -1002,9 +861,6 @@ 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):
@@ -1012,14 +868,18 @@ class CardApplicationISD(CardApplicationSD):
# 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):
# The Key Version Number is an 8 bit integer number, where 0 refers to the first available key,
# see also: GPC_SPE_034, section E.5.1.3
assert 0 <= kvn < 256
assert 0 < kvn < 256
assert len(enc) == len(mac) == len(dek)
self.kvn = kvn
self.enc = enc

View File

@@ -1,94 +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, GreedyString, BytesInteger
from construct import this, len_, Rebuild, Const
from construct import Optional as COptional
from osmocom.construct import Bytes, GreedyBytes
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,72 +0,0 @@
# GlobalPlatform install parameter generator
#
# (C) 2024 by Sysmocom s.f.m.c. GmbH
# 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 osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import *
class AppSpecificParams(BER_TLV_IE, tag=0xC9):
# GPD_SPE_013, table 11-49
_construct = GreedyBytes
class VolatileMemoryQuota(BER_TLV_IE, tag=0xC7):
# GPD_SPE_013, table 11-49
_construct = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
class NonVolatileMemoryQuota(BER_TLV_IE, tag=0xC8):
# GPD_SPE_013, table 11-49
_construct = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
class StkParameter(BER_TLV_IE, tag=0xCA):
# GPD_SPE_013, table 11-49
# ETSI TS 102 226, section 8.2.1.3.2.1
_construct = GreedyBytes
class SystemSpecificParams(BER_TLV_IE, tag=0xEF, nested=[VolatileMemoryQuota, NonVolatileMemoryQuota, StkParameter]):
# GPD_SPE_013 v1.1 Table 6-5
pass
class InstallParams(TLV_IE_Collection, nested=[AppSpecificParams, SystemSpecificParams]):
# GPD_SPE_013, table 11-49
pass
def gen_install_parameters(non_volatile_memory_quota:int, volatile_memory_quota:int, stk_parameter:str):
# GPD_SPE_013, table 11-49
#Mandatory
install_params = InstallParams()
install_params_dict = [{'app_specific_params': None}]
#Conditional
if non_volatile_memory_quota and volatile_memory_quota and stk_parameter:
system_specific_params = []
#Optional
if non_volatile_memory_quota:
system_specific_params += [{'non_volatile_memory_quota': non_volatile_memory_quota}]
#Optional
if volatile_memory_quota:
system_specific_params += [{'volatile_memory_quota': volatile_memory_quota}]
#Optional
if stk_parameter:
system_specific_params += [{'stk_parameter': stk_parameter}]
install_params_dict += [{'system_specific_params': system_specific_params}]
install_params.from_dict(install_params_dict)
return b2h(install_params.to_bytes())

View File

@@ -20,12 +20,9 @@ import logging
from typing import Optional
from Cryptodome.Cipher import DES3, DES
from Cryptodome.Util.strxor import strxor
from construct import Struct, Int8ub, Int16ub, Const
from construct import Struct, Bytes, Int8ub, Int16ub, Const
from construct import Optional as COptional
from osmocom.construct import Bytes
from osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
from pySim.utils import parse_command_apdu
from pySim.utils import b2h, bertlv_parse_len, bertlv_encode_len
from pySim.secure_channel import SecureChannel
logger = logging.getLogger(__name__)
@@ -114,48 +111,10 @@ 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):
# Spec references that explain KVN ranges:
# TS 102 225 Annex A.1 states KVN 0x01..0x0F shall be used for SCP80
# GPC_GUI_003 states
# * For the Issuer Security Domain, this is initially Key Version Number 'FF' which has been deliberately
# chosen to be outside of the allowable range ('01' to '7F') for a Key Version Number.
# * It is logical that the initial keys in the Issuer Security Domain be replaced by an initial issuer Key
# Version Number in the range '01' to '6F'.
# * Key Version Numbers '70' to '72' and '74' to '7F' are reserved for future use.
# * On an implementation supporting Supplementary Security Domains, the RSA public key with a Key Version
# Number '73' and a Key Identifier of '01' has the following functionality in a Supplementary Security
# Domain with the DAP Verification privilege [...]
# GPC_GUI_010 V1.0.1 Section 6 states
# * Key Version number range ('20' to '2F') is reserved for SCP02
# * Key Version 'FF' is reserved for use by an Issuer Security Domain supporting SCP02, and cannot be used
# for SCP80. This initial key set shall be replaced by a key set with a Key Version Number in the
# ('20' to '2F') range.
# * Key Version number range ('01' to '0F') is reserved for SCP80
# * Key Version number '70' with Key Identifier '01' is reserved for the Token Key, which is either a RSA
# public key or a DES key
# * Key Version number '71' with Key Identifier '01' is reserved for the Receipt Key, which is a DES key
# * Key Version Number '11' is reserved for DAP as specified in ETSI TS 102 226 [2]
# * Key Version Number '73' with Key Identifier '01' is reserved for the DAP verification key as specified
# in sections 3.3.3 and 4 of [4], which is either an RSA public key or DES key
# * Key Version Number '74' is reserved for the CASD Keys (cf. section 9.2)
# * Key Version Number '75' with Key Identifier '01' is reserved for the key used to decipher the Ciphered
# Load File Data Block described in section 4.8 of [5].
if card_keys.kvn == 0:
# Key Version Number 0x00 refers to the first available key, so we won't carry out
# a range check in this case. See also: GPC_SPE_034, section E.5.1.3
pass
elif hasattr(self, 'kvn_range'):
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
@@ -257,20 +216,18 @@ class SCP02(SCP):
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))
# Key Version Number 0x70 is a non-spec special-case of sysmoISIM-SJA2/SJA5 and possibly more sysmocom products
# Key Version Number 0x01 is a non-spec special-case of sysmoUSIM-SJS1
kvn_ranges = [[0x01, 0x01], [0x20, 0x2f], [0x70, 0x70]]
kvn_range = [0x20, 0x2f]
def __init__(self, *args, **kwargs):
self.overhead = 8
super().__init__(*args, **kwargs)
def dek_encrypt(self, plaintext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
return cipher.encrypt(plaintext)
def dek_decrypt(self, ciphertext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
return cipher.decrypt(ciphertext)
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
@@ -282,7 +239,7 @@ class SCP02(SCP):
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'
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALZIE UPDATE."""
@@ -309,52 +266,34 @@ class SCP02(SCP):
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
lc = len(apdu) - 5
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
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'
# CLA without log. channel can be 80 or 00 only
if self.do_cmac:
if self.mac_on_unmodified:
mlc = lc
clac = cla
else: # CMAC on modified APDU
mlc = lc + 8
clac = cla | CLA_SM
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
if self.do_cenc:
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
data = k.encrypt(pad80(apdu[5:], 8))
lc = len(data)
else:
data = apdu[5:]
lc += 8
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
@@ -501,7 +440,7 @@ class SCP03(SCP):
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'
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALIZE UPDATE."""
@@ -526,26 +465,18 @@ class SCP03(SCP):
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
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
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
lc = apdu[4]
assert lc == len(apdu) - 5
cmd_data = apdu[5:]
if self.do_cenc and not skip_cenc:
if case <= 2:
assert self.do_cmac
if lc == 0:
# No encryption shall be applied to a command where there is no command data field. In this
# case, the encryption counter shall still be incremented
self.sk.block_nr += 1
@@ -558,24 +489,21 @@ class SCP03(SCP):
# 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]
if self.do_cmac:
# The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
# mode) to indicate the inclusion of the C-MAC in the data field of the command message.
mlc = lc + self.s_mode
if mlc >= 256:
raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
# The class byte shall be modified for the generation or verification of the C-MAC: The logical
# channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
# GlobalPlatform proprietary secure messaging.
mcla = (cla & 0xF0) | CLA_SM
mapdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
cmac = self.sk.calc_cmac(mapdu)
mapdu += cmac[:self.s_mode]
# See comment in SCP03._wrap_cmd_apdu()
if case == 4 or case == 2:
apdu += b'\x00'
return apdu
return mapdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
# No R-MAC shall be generated and no protection shall be applied to a response that includes an error

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

@@ -26,10 +26,10 @@ order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for
from pySim.utils import *
from struct import pack, unpack
from construct import Struct, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
from construct import Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
from construct import Optional as COptional
from osmocom.construct import *
from pySim.construct import *
from pySim.profile import CardProfileAddon
from pySim.filesystem import *
@@ -184,13 +184,13 @@ class EF_CallconfI(LinFixedEF):
class EF_Shunting(TransparentEF):
"""Section 7.6"""
_test_de_encode = [
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": h2b("f8ffffff000000") } ),
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
]
def __init__(self):
super().__init__(fid='6ff4', sfid=None,
name='EF.Shunting', desc='Shunting', size=(8, 8))
self._construct = Struct('common_gid'/Int8ub,
'shunting_gid'/Bytes(7))
'shunting_gid'/HexAdapter(Bytes(7)))
class EF_GsmrPLMN(LinFixedEF):
@@ -199,13 +199,13 @@ class EF_GsmrPLMN(LinFixedEF):
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
"fn": True, "eirene": True }, "preference": 0 },
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
"ic_table_ref": h2b("01") } ),
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
"ic_table_ref": "01" } ),
( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
"fn": True, "eirene": False }, "preference": 1 },
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
"ic_table_ref": h2b("02") } ),
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
"ic_table_ref": "02" } ),
]
def __init__(self):
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
@@ -213,24 +213,24 @@ class EF_GsmrPLMN(LinFixedEF):
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
'preference'/BitsInteger(3)),
'ic_incoming_ref_tbl'/Bytes(2),
'outgoing_ref_tbl'/Bytes(2),
'ic_table_ref'/Bytes(1))
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
'outgoing_ref_tbl'/HexAdapter(Bytes(2)),
'ic_table_ref'/HexAdapter(Bytes(1)))
class EF_IC(LinFixedEF):
"""Section 7.8"""
_test_de_encode = [
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": h2b("6f8e"),
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": "6f8e",
"ic_decision_value": "041f", "network_string_table_index": 1 } ),
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"),
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": "ffff",
"ic_decision_value": "ffff", "network_string_table_index": 65535 } ),
]
def __init__(self):
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
desc='International Code', rec_len=(7, 7))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/Bytes(2),
'id_of_next_table'/HexAdapter(Bytes(2)),
'ic_decision_value'/BcdAdapter(Bytes(2)),
'network_string_table_index'/Int16ub)
@@ -252,18 +252,18 @@ class EF_NW(LinFixedEF):
class EF_Switching(LinFixedEF):
"""Section 8.4"""
_test_de_encode = [
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": h2b("6f87"),
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": "6f87",
"decision_value": "0fff", "string_table_index": 0 } ),
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": h2b("6f8f"),
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": "6f8f",
"decision_value": "1fff", "string_table_index": 1 } ),
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": h2b("6f89"),
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": "6f89",
"decision_value": "5fff", "string_table_index": 5 } ),
]
def __init__(self, fid='1234', name='Switching', desc=None):
super().__init__(fid=fid, sfid=None,
name=name, desc=desc, rec_len=(6, 6))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/Bytes(2),
'id_of_next_table'/HexAdapter(Bytes(2)),
'decision_value'/BcdAdapter(Bytes(2)),
'string_table_index'/Int8ub)
@@ -271,12 +271,12 @@ class EF_Switching(LinFixedEF):
class EF_Predefined(LinFixedEF):
"""Section 8.5"""
_test_de_encode = [
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": h2b("6f85") } ),
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": "6f85" } ),
( "f0ffc8", 2, { "predefined_value1": "0fff", "string_table_index1": 200 } ),
]
# header and other records have different structure. WTF !?!
construct_first = Struct('next_table_type'/NextTableType,
'id_of_next_table'/Bytes(2))
'id_of_next_table'/HexAdapter(Bytes(2)))
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
'string_table_index1'/Int8ub)
@@ -290,7 +290,7 @@ class EF_Predefined(LinFixedEF):
else:
return parse_construct(self.construct_others, raw_bin_data)
def _encode_record_bin(self, abstract_data : dict, record_nr : int, **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)
@@ -301,13 +301,13 @@ class EF_Predefined(LinFixedEF):
class EF_DialledVals(TransparentEF):
"""Section 8.6"""
_test_de_encode = [
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"), "dialed_digits": "22" } ),
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": h2b("6f88"), "dialed_digits": "58" }),
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": "ffff", "dialed_digits": "22" } ),
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": "6f88", "dialed_digits": "58" }),
]
def __init__(self, fid='1234', name='DialledVals', desc=None):
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/Bytes(2),
'id_of_next_table'/HexAdapter(Bytes(2)),
'dialed_digits'/BcdAdapter(Bytes(1)))

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 construct import Optional as COptional
from construct import Int8ub, Int8sb, Int32ub, BitStruct, Enum, GreedyBytes, Struct, Switch
from construct import this, PaddedString
from pySim.construct import *
# The root definition of GSMTAP can be found at
# 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 GreedyString
from osmocom.tlv import *
from osmocom.construct import *
from construct import GreedyBytes, GreedyString
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,142 +0,0 @@
# JavaCard related utilities
#
# (C) 2024 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import zipfile
import struct
import sys
import io
from osmocom.utils import b2h, Hexstr
from construct import Struct, Array, this, Int32ub, Int16ub, Int8ub
from osmocom.construct import *
from osmocom.tlv import *
from construct import Optional as COptional
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.
example usage:
with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
ijc_to_cap(f, z)
"""
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:]
class CapFile():
# Java Card Platform Virtual Machine Specification, v3.2, section 6.4
__header_component_compact = Struct('tag'/Int8ub,
'size'/Int16ub,
'magic'/Int32ub,
'minor_version'/Int8ub,
'major_version'/Int8ub,
'flags'/Int8ub,
'package'/Struct('minor_version'/Int8ub,
'major_version'/Int8ub,
'AID'/LV),
'package_name'/COptional(LV)) #since CAP format 2.2
# Java Card Platform Virtual Machine Specification, v3.2, section 6.6
__applet_component_compact = Struct('tag'/Int8ub,
'size'/Int16ub,
'count'/Int8ub,
'applets'/Array(this.count, Struct('AID'/LV,
'install_method_offset'/Int16ub)),
)
def __init__(self, filename:str):
# In this dictionary we will keep all nested .cap file components by their file names (without .cap suffix)
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1
self.__component = {}
# Extract the nested .cap components from the .cap file
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1
cap = zipfile.ZipFile(filename)
cap_namelist = cap.namelist()
for i, filename in enumerate(cap_namelist):
if filename.lower().endswith('.capx') and not filename.lower().endswith('.capx'):
#TODO: At the moment we only support the compact .cap format, add support for the extended .cap format.
raise ValueError("incompatible .cap file, extended .cap format not supported!")
if filename.lower().endswith('.cap'):
key = filename.split('/')[-1].removesuffix('.cap')
self.__component[key] = cap.read(filename)
# Make sure that all mandatory components are present
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2
required_components = {'Header' : 'COMPONENT_Header',
'Directory' : 'COMPONENT_Directory',
'Import' : 'COMPONENT_Import',
'ConstantPool' : 'COMPONENT_ConstantPool',
'Class' : 'COMPONENT_Class',
'Method' : 'COMPONENT_Method',
'StaticField' : 'COMPONENT_StaticField',
'RefLocation' : 'COMPONENT_ReferenceLocation',
'Descriptor' : 'COMPONENT_Descriptor'}
for component in required_components:
if component not in self.__component.keys():
raise ValueError("invalid cap file, %s missing!" % required_components[component])
def get_loadfile(self) -> bytes:
"""Get the executeable loadfile as hexstring"""
# Concatenate all cap file components in the specified order
# see also: Java Card Platform Virtual Machine Specification, v3.2, section 6.3
loadfile = self.__component['Header']
loadfile += self.__component['Directory']
loadfile += self.__component['Import']
if 'Applet' in self.__component.keys():
loadfile += self.__component['Applet']
loadfile += self.__component['Class']
loadfile += self.__component['Method']
loadfile += self.__component['StaticField']
if 'Export' in self.__component.keys():
loadfile += self.__component['Export']
loadfile += self.__component['ConstantPool']
loadfile += self.__component['RefLocation']
if 'Descriptor' in self.__component.keys():
loadfile += self.__component['Descriptor']
return loadfile
def get_loadfile_aid(self) -> Hexstr:
"""Get the loadfile AID as hexstring"""
header = self.__header_component_compact.parse(self.__component['Header'])
magic = header['magic'] or 0
if magic != 0xDECAFFED:
raise ValueError("invalid cap file, COMPONENT_Header lacks magic number (0x%08X!=0xDECAFFED)!" % magic)
#TODO: check cap version and make sure we are compatible with it
return header['package']['AID']
def get_applet_aid(self, index:int = 0) -> Hexstr:
"""Get the applet AID as hexstring"""
#To get the module AID, we must look into COMPONENT_Applet. Unfortunately, even though this component should
#be present in any .cap file, it is defined as an optional component.
if 'Applet' not in self.__component.keys():
raise ValueError("can't get the AID, this cap file lacks the optional COMPONENT_Applet component!")
applet = self.__applet_component_compact.parse(self.__component['Applet'])
if index > applet['count']:
raise ValueError("can't get the AID for applet with index=%u, this .cap file only has %u applets!" %
(index, applet['count']))
return applet['applets'][index]['AID']

View File

@@ -3,14 +3,15 @@
################################################################################
import abc
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
@@ -780,7 +781,7 @@ class SysmoSIMgr1(GrcardSim):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b991800118822334455667760":
if scc.get_atr() == toBytes("3B 99 18 00 11 88 22 33 44 55 66 77 60"):
return kls(scc)
except:
return None
@@ -825,7 +826,7 @@ class SysmoSIMgr2(SimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b7d9400005555530a7486930b247c4d5468":
if scc.get_atr() == toBytes("3B 7D 94 00 00 55 55 53 0A 74 86 93 0B 24 7C 4D 54 68"):
return kls(scc)
except:
return None
@@ -903,7 +904,7 @@ class SysmoUSIMSJS1(UsimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f96801fc78031a073be21136743200718000001a5":
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5"):
return kls(scc)
except:
return None
@@ -1031,7 +1032,7 @@ class FairwavesSIM(UsimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f96801fc78031a073be21136744220610000001a9":
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 44 22 06 10 00 00 01 A9"):
return kls(scc)
except:
return None
@@ -1165,7 +1166,7 @@ class OpenCellsSim(SimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f95801fc38031e073fe21135786810286984418a8":
if scc.get_atr() == toBytes("3B 9F 95 80 1F C3 80 31 E0 73 FE 21 13 57 86 81 02 86 98 44 18 A8"):
return kls(scc)
except:
return None
@@ -1214,7 +1215,7 @@ class WavemobileSim(UsimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f95801fc78031e073f62113674d4516004301008f":
if scc.get_atr() == toBytes("3B 9F 95 80 1F C7 80 31 E0 73 F6 21 13 67 4D 45 16 00 43 01 00 8F"):
return kls(scc)
except:
return None
@@ -1304,18 +1305,18 @@ class SysmoISIMSJA2(UsimCard, IsimCard):
def autodetect(kls, scc):
try:
# Try card model #1
atr = "3b9f96801f878031e073fe211b674a4c753034054ba9"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #2
atr = "3b9f96801f878031e073fe211b674a4c7531330251b2"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #3
atr = "3b9f96801f878031e073fe211b674a4c5275310451d5"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"
if scc.get_atr() == toBytes(atr):
return kls(scc)
except:
return None
@@ -1553,16 +1554,16 @@ class SysmoISIMSJA5(SysmoISIMSJA2):
def autodetect(kls, scc):
try:
# Try card model #1 (9FJ)
atr = "3b9f96801f878031e073fe211b674a357530350251cc"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #2 (SLM17)
atr = "3b9f96801f878031e073fe211b674a357530350265f8"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #3 (9FV)
atr = "3b9f96801f878031e073fe211b674a357530350259c4"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"
if scc.get_atr() == toBytes(atr):
return kls(scc)
except:
return None
@@ -1591,7 +1592,7 @@ class GialerSim(UsimCard):
def autodetect(cls, scc):
try:
# Look for ATR
if scc.get_atr() == '3b9f95801fc78031a073b6a10067cf3215ca9cd70920':
if scc.get_atr() == toBytes('3B 9F 95 80 1F C7 80 31 A0 73 B6 A1 00 67 CF 32 15 CA 9C D7 09 20'):
return cls(scc)
except:
return None

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))]
@@ -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
@@ -18,12 +18,12 @@
import zlib
import abc
import struct
from typing import Optional, Tuple
from construct import Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
from osmocom.construct import *
from osmocom.utils import b2h
from typing import Optional
from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
from construct import Flag, Padding, Switch, this
from pySim.construct import *
from pySim.utils import b2h
from pySim.sms import UserDataHeader
# ETS TS 102 225 gives the general command structure and the dialects for CAT_TP, TCP/IP and HTTPS
@@ -99,17 +99,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,
@@ -333,7 +322,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 +332,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 +342,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,55 +385,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:
cpl = None # FIXME this line was just added to silence pylint possibly-used-before-assignment
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)
@@ -493,7 +434,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

@@ -25,11 +25,54 @@ 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
applications as well as profile-specific SW and shell commands. Every card has
@@ -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):

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 h2b, i2h, is_hex, bertlv_parse_one, Hexstr
from pySim.exceptions import *
from pySim.filesystem import *
@@ -50,10 +49,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 = {}
self.adm_verified = False
# make sure the class and selection control bytes, which are specified
# by the card profile are used
@@ -136,19 +131,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]
self.adm_verified = False
atr = self.card.reset()
if cmd_app:
cmd_app.lchan = self.lchan[0]
atr = i2h(self.card.reset())
# select MF to reset internal state and to verify card really works
self.lchan[0].select('MF', cmd_app)
self.lchan[0].selected_adf = None
# store ATR as part of our card identies dict
self.identity['ATR'] = atr
return atr
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
@@ -213,18 +202,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.
@@ -249,42 +226,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.
@@ -313,8 +254,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.
@@ -347,6 +287,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.
@@ -362,7 +308,9 @@ class RuntimeLchan:
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, ...).
@@ -371,31 +319,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
@@ -468,8 +396,7 @@ class RuntimeLchan:
(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]
@@ -490,8 +417,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]:
@@ -515,8 +441,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):
@@ -526,7 +451,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):
@@ -538,8 +463,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)
@@ -562,8 +486,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)
@@ -576,7 +499,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):
@@ -599,8 +522,7 @@ 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__))
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)
@@ -613,18 +535,11 @@ 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

@@ -16,9 +16,7 @@
# 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
from pySim.utils import b2h, h2b, ResTuple, Hexstr
class SecureChannel(abc.ABC):
@abc.abstractmethod

View File

@@ -20,11 +20,12 @@
import typing
import abc
from bidict import bidict
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger
from construct import Struct, Enum, Tell, BitStruct, this, Padding
from construct import Prefixed, GreedyRange
from osmocom.construct import BcdAdapter, TonNpi, Bytes, GreedyBytes
from osmocom.utils import Hexstr, h2b, b2h
from construct import Prefixed, GreedyRange, GreedyBytes
from pySim.construct import HexAdapter, BcdAdapter, TonNpi
from pySim.utils import Hexstr, h2b, b2h
from smpp.pdu import pdu_types, operations
@@ -253,49 +254,6 @@ class SMS_DELIVER(SMS_TPDU):
}
return cls(**d)
@classmethod
def from_submit(cls, submit: 'SMS_SUBMIT') -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from a SMS_SUBMIT instance."""
d = {
# common fields (SMS_TPDU base class) which exist in submit, so we can copy them
'tp_mti': submit.tp_mti,
'tp_rp': submit.tp_rp,
'tp_udhi': submit.tp_udhi,
'tp_pid': submit.tp_pid,
'tp_dcs': submit.tp_dcs,
'tp_udl': submit.tp_udl,
'tp_ud': submit.tp_ud,
# SMS_DELIVER specific fields
'tp_lp': False,
'tp_mms': False,
'tp_oa': None,
'tp_scts': h2b('22705200000000'), # FIXME
'tp_sri': False,
}
return cls(**d)
def to_smpp(self) -> pdu_types.PDU:
"""Translate a SMS_DELIVER instance to a smpp.pdu.operations.DeliverSM instance."""
# we only deal with binary SMS here:
if self.tp_dcs != 0xF6:
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
dcs = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT,
gsmFeatures=[pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET])
if self.tp_oa:
oa_digits, oa_ton, oa_npi = self.tp_oa.to_smpp()
else:
oa_digits, oa_ton, oa_npi = None, None, None
return operations.DeliverSM(source_addr=oa_digits,
source_addr_ton=oa_ton,
source_addr_npi=oa_npi,
#destination_addr=ESME_MSISDN,
esm_class=esm_class,
protocol_id=self.tp_pid,
data_coding=dcs,
short_message=self.tp_ud)
class SMS_SUBMIT(SMS_TPDU):

View File

@@ -18,14 +18,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from struct import unpack
from construct import FlagsEnum, Byte, Struct, Int8ub, Mapping, Enum, Padding, BitsInteger
from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange, Const
from construct import FlagsEnum, Byte, Struct, Int8ub, Bytes, Mapping, Enum, Padding, BitsInteger
from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange
from construct import Optional as COptional
from osmocom.utils import *
from osmocom.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.runtime import RuntimeState
from pySim.construct import *
import pySim
key_type2str = {
@@ -51,13 +51,13 @@ class EF_PIN(TransparentEF):
( 'f1030331323334ffffffff0a0a3132333435363738',
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
'not_initialized': False, 'disabled': True },
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': b'1234',
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': b'12345678' }
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '31323334',
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': '3132333435363738' }
} ),
( 'f003039999999999999999',
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
'not_initialized': False, 'disabled': False },
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': h2b('9999999999999999'),
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '9999999999999999',
'puk': None } ),
]
def __init__(self, fid='6f01', name='EF.CHV1'):
@@ -66,32 +66,29 @@ class EF_PIN(TransparentEF):
change_able=0x40, valid=0x80)
PukStruct = Struct('attempts_remaining'/Int8ub,
'maximum_attempts'/Int8ub,
'puk'/Rpad(Bytes(8)))
'puk'/HexAdapter(Rpad(Bytes(8))))
self._construct = Struct('state'/StateByte,
'attempts_remaining'/Int8ub,
'maximum_attempts'/Int8ub,
'pin'/Rpad(Bytes(8)),
'pin'/HexAdapter(Rpad(Bytes(8))),
'puk'/COptional(PukStruct))
class EF_MILENAGE_CFG(TransparentEF):
_test_de_encode = [
( '40002040600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008',
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96,
"c1": h2b("00000000000000000000000000000000"),
"c2": h2b("00000000000000000000000000000001"),
"c3": h2b("00000000000000000000000000000002"),
"c4": h2b("00000000000000000000000000000004"),
"c5": h2b("00000000000000000000000000000008")} ),
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96, "c1": "00000000000000000000000000000000", "c2":
"00000000000000000000000000000001", "c3": "00000000000000000000000000000002", "c4":
"00000000000000000000000000000004", "c5": "00000000000000000000000000000008"} ),
]
def __init__(self, fid='6f21', name='EF.MILENAGE_CFG', desc='Milenage connfiguration'):
super().__init__(fid, name=name, desc=desc)
self._construct = Struct('r1'/Int8ub, 'r2'/Int8ub, 'r3'/Int8ub, 'r4'/Int8ub, 'r5'/Int8ub,
'c1'/Bytes(16),
'c2'/Bytes(16),
'c3'/Bytes(16),
'c4'/Bytes(16),
'c5'/Bytes(16))
'c1'/HexAdapter(Bytes(16)),
'c2'/HexAdapter(Bytes(16)),
'c3'/HexAdapter(Bytes(16)),
'c4'/HexAdapter(Bytes(16)),
'c5'/HexAdapter(Bytes(16)))
class EF_0348_KEY(LinFixedEF):
@@ -105,18 +102,18 @@ class EF_0348_KEY(LinFixedEF):
self._construct = Struct('security_domain'/Int8ub,
'key_set_version'/Int8ub,
'key_len_and_type'/KeyLenAndType,
'key'/Bytes(this.key_len_and_type.key_length))
'key'/HexAdapter(Bytes(this.key_len_and_type.key_length)))
class EF_0348_COUNT(LinFixedEF):
_test_de_encode = [
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": h2b("0000000000")} ),
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": "0000000000"} ),
]
def __init__(self, fid='6f23', name='EF.0348_COUNT', desc='TS 03.48 OTA Counters'):
super().__init__(fid, name=name, desc=desc, rec_len=(7, 7))
self._construct = Struct('sec_domain'/Int8ub,
'key_set_version'/Int8ub,
'counter'/Bytes(5))
'counter'/HexAdapter(Bytes(5)))
class EF_SIM_AUTH_COUNTER(TransparentEF):
@@ -148,9 +145,8 @@ class EF_GP_DIV_DATA(LinFixedEF):
class EF_SIM_AUTH_KEY(TransparentEF):
_test_de_encode = [
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"},
"key": h2b("000102030405060708090a0b0c0d0e0f"),
"op_opc": h2b("101112131415161718191a1b1c1d1e1f")} ),
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"}, "key":
"000102030405060708090a0b0c0d0e0f", "op_opc": "101112131415161718191a1b1c1d1e1f"} ),
]
def __init__(self, fid='6f20', name='EF.SIM_AUTH_KEY'):
super().__init__(fid, name=name, desc='USIM authentication key')
@@ -159,8 +155,8 @@ class EF_SIM_AUTH_KEY(TransparentEF):
'use_opc_instead_of_op'/Flag,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
self._construct = Struct('cfg'/CfgByte,
'key'/Bytes(16),
'op_opc' /Bytes(16))
'key'/HexAdapter(Bytes(16)),
'op_opc' /HexAdapter(Bytes(16)))
class DF_SYSTEM(CardDF):
@@ -210,18 +206,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": h2b("1898d827f70120d33b3e7462ee5fd6fe"), "op_opc": h2b("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" : h2b("000102030405060708090a0b0c0d0e0f"),
"op_opc" : h2b("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)
@@ -230,8 +214,8 @@ class EF_USIM_AUTH_KEY(TransparentEF):
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
'algorithm'/Algorithm)
self._construct = Struct('cfg'/CfgByte,
'key'/Bytes(16),
'op_opc'/Bytes(16))
'key'/HexAdapter(Bytes(16)),
'op_opc' /HexAdapter(Bytes(16)))
# TUAK has a rather different layout for the data, so we define a different
# construct below and use explicit _{decode,encode}_bin() methods for separating
# the TUAK and non-TUAK situation
@@ -247,8 +231,8 @@ class EF_USIM_AUTH_KEY(TransparentEF):
self._constr_tuak = Struct('cfg'/CfgByteTuak,
'tuak_cfg'/TuakCfgByte,
'num_of_keccak_iterations'/Int8ub,
'op_opc'/Bytes(32),
'k'/Bytes(this.cfg.key_length//8))
'op_opc'/HexAdapter(Bytes(32)),
'k'/HexAdapter(Bytes(this.cfg.key_length//8)))
def _decode_bin(self, raw_bin_data: bytearray) -> dict:
if raw_bin_data[0] & 0x0F == 0x06:
@@ -256,7 +240,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:
@@ -267,9 +251,8 @@ class EF_USIM_AUTH_KEY_2G(TransparentEF):
_test_de_encode = [
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
{"cfg": {"only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True,
"algorithm": "milenage"},
"key": h2b("000102030405060708090a0b0c0d0e0f"),
"op_opc": h2b("101112131415161718191a1b1c1d1e1f")} ),
"algorithm": "milenage"}, "key": "000102030405060708090a0b0c0d0e0f", "op_opc":
"101112131415161718191a1b1c1d1e1f"} ),
]
def __init__(self, fid='af22', name='EF.USIM_AUTH_KEY_2G'):
super().__init__(fid, name=name, desc='USIM authentication key in 2G context')
@@ -278,8 +261,8 @@ class EF_USIM_AUTH_KEY_2G(TransparentEF):
'use_opc_instead_of_op'/Flag,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3, xor=14))
self._construct = Struct('cfg'/CfgByte,
'key'/Bytes(16),
'op_opc'/Bytes(16))
'key'/HexAdapter(Bytes(16)),
'op_opc' /HexAdapter(Bytes(16)))
class EF_GBA_SK(TransparentEF):
@@ -303,9 +286,9 @@ class EF_GBA_INT_KEY(LinFixedEF):
class SysmocomSJA2(CardModel):
_atrs = ["3b9f96801f878031e073fe211b674a4c753034054ba9",
"3b9f96801f878031e073fe211b674a4c7531330251b2",
"3b9f96801f878031e073fe211b674a4c5275310451d5"]
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"]
@classmethod
def add_files(cls, rs: RuntimeState):
@@ -334,9 +317,9 @@ class SysmocomSJA2(CardModel):
isim_adf.add_files(files_adf_isim)
class SysmocomSJA5(CardModel):
_atrs = ["3b9f96801f878031e073fe211b674a357530350251cc",
"3b9f96801f878031e073fe211b674a357530350265f8",
"3b9f96801f878031e073fe211b674a357530350259c4"]
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"]
@classmethod
def add_files(cls, rs: RuntimeState):

464
pySim/tlv.py Normal file
View File

@@ -0,0 +1,464 @@
"""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/>.
import inspect
import abc
import re
from typing import List, Tuple
from pySim.utils import bertlv_encode_len, bertlv_parse_len, bertlv_encode_tag, bertlv_parse_tag
from pySim.utils import comprehensiontlv_encode_tag, comprehensiontlv_parse_tag
from pySim.utils import bertlv_parse_tag_raw, comprehensiontlv_parse_tag_raw
from pySim.utils import dgi_parse_tag_raw, dgi_parse_len, dgi_encode_tag, dgi_encode_len
from pySim.construct import build_construct, parse_construct
def camel_to_snake(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
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__(mcs, name, bases, namespace, **kwargs):
#print("TlvMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
x = super().__new__(mcs, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.tag = namespace.get('tag', kwargs.get('tag', None))
x.desc = namespace.get('desc', kwargs.get('desc', None))
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__(mcs, name, bases, namespace, **kwargs):
#print("TlvCollectionMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
x = super().__new__(mcs, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
return x
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 is 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?"""
return bool(len(self.children) > 0)
@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 _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."""
@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 DGI_TLV_IE(TLV_IE):
"""TLV_IE formated as GlobalPlatform Systems Scripting Language Specification v1.1.0 Annex B."""
@classmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
return dgi_parse_tag_raw(do)
@classmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return dgi_parse_len(do)
def _encode_tag(self) -> bytes:
return dgi_encode_tag(self._compute_tag())
def _encode_len(self, val: bytes) -> bytes:
return dgi_encode_len(len(val))
class TLV_IE_Collection(metaclass=TlvCollectionMeta):
# we specify the metaclass so any downstream subclasses will automatically use it
"""A TLV_IE_Collection consists of multiple TLV_IE classes identified by their tags.
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 is None:
break
if tag in self.members_by_tag:
cls = self.members_by_tag[tag]
# create an instance and parse accordingly
inst = cls()
_dec, remainder = inst.from_tlv(remainder, 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 = {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(v) for k,v in inp.items()}
else:
return [flatten_dict_lists(x) for x in inp]
elif isinstance(inp, dict):
return {k:flatten_dict_lists(v) for k,v in inp.items()}
else:
return inp

View File

@@ -8,10 +8,9 @@ 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.utils import sw_match, b2h, h2b, i2h, Hexstr, SwHexstr, SwMatchstr, ResTuple
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
#
@@ -40,18 +39,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 +56,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 +66,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."""
@@ -119,73 +93,68 @@ class LinkBase(abc.ABC):
"""Connect to a card immediately
"""
@abc.abstractmethod
def get_atr(self) -> Hexstr:
"""Retrieve card ATR
"""
@abc.abstractmethod
def disconnect(self):
"""Disconnect from card
"""
@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 also 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 response 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:
@@ -193,7 +162,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'):
@@ -212,26 +181,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.
@@ -244,89 +218,6 @@ 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)
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)
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if self.protocol == 0:
return self.__send_apdu_T0(apdu)
elif self.protocol == 1:
return self.__send_apdu_transparent(apdu)
raise ValueError('unspported protocol selected (T=%d)' % self.protocol)
def __send_apdu_T0(self, apdu: Hexstr) -> ResTuple:
# Transform the given APDU to the T=0 TPDU format and send it. Automatically fetch the response (case #4 APDUs)
# (see also ETSI TS 102 221, section 7.3.1.1)
# Transform APDU to T=0 TPDU (see also ETSI TS 102 221, section 7.3.1)
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
if case == 1:
# Attach an Le field to all case #1 APDUs (see also ETSI TS 102 221, section 7.3.1.1.1)
tpdu = apdu + '00'
elif case == 4:
# Remove the Le field from all case #4 APDUs (see also ETSI TS 102 221, section 7.3.1.1.4)
tpdu = apdu[:-2]
else:
tpdu = apdu
prev_tpdu = tpdu
data, sw = self.send_tpdu(tpdu)
# When we have sent the first APDU, the SW may indicate that there are response bytes
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
prev_tpdu = tpdu_gr
d, sw = self.send_tpdu(tpdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
data, sw = self.send_tpdu(tpdu_gr)
return data, sw
def __send_apdu_transparent(self, apdu: Hexstr) -> ResTuple:
# In cases where the TPDU format is the same as the APDU format, we may pass the given APDU through without modification
# (This is the case for T=1, see also ETSI TS 102 221, section 7.3.2.0.)
return self.send_tpdu(apdu)
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
"""Add all reader related arguments to the given argparse.Argumentparser instance."""
from pySim.transport.serial import SerialSimLink
@@ -338,8 +229,6 @@ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
PcscSimLink.argparse_add_reader_args(arg_parser)
ModemATCommandLink.argparse_add_reader_args(arg_parser)
CalypsoSimLink.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
@@ -348,9 +237,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)

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.transport import LinkBase
from pySim.exceptions import ReaderError, ProtocolError
from pySim.utils import ResTuple
from pySim.utils import h2b, b2h, Hexstr, ResTuple
class L1CTLMessage:
@@ -70,12 +69,12 @@ class L1CTLMessageSIM(L1CTLMessage):
L1CTL_SIM_REQ = 0x16
L1CTL_SIM_CONF = 0x17
def __init__(self, tpdu):
def __init__(self, pdu):
super().__init__(self.L1CTL_SIM_REQ)
self.data += tpdu
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())
@@ -123,19 +122,16 @@ class CalypsoSimLink(LinkBaseTpdu):
def connect(self):
self.reset_card()
def get_atr(self) -> Hexstr:
return "3b00" # Dummy ATR
def disconnect(self):
pass # Nothing to do really ...
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

@@ -22,17 +22,16 @@ 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.utils import Hexstr, ResTuple
from pySim.transport import LinkBase
from pySim.exceptions import ReaderError, ProtocolError
# 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)"
@@ -125,7 +124,7 @@ class ModemATCommandLink(LinkBaseTpdu):
return
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')
@@ -139,21 +138,18 @@ class ModemATCommandLink(LinkBaseTpdu):
def connect(self):
pass # Nothing to do really ...
def get_atr(self) -> Hexstr:
return "3b00" # Dummy ATR
def disconnect(self):
pass # Nothing to do really ...
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
@@ -167,14 +163,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()
(_rsp_pdu_len, rsp_pdu) = result.groups()
except Exception as exc:
raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc
# TODO: make sure we have at least SW
data = rsp_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

@@ -25,16 +25,13 @@ from smartcard.CardConnection import CardConnection
from smartcard.CardRequest import CardRequest
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException
from smartcard.System import readers
from smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
from osmocom.utils import h2i, i2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
from pySim.transport import 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:
@@ -84,37 +79,30 @@ 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)
# Explicitly select T=0 communication protocol
self._con.connect(CardConnection.T0_protocol)
except CardConnectionException as exc:
raise ProtocolError() from exc
except NoCardException as exc:
raise NoCardError() from exc
def get_atr(self) -> Hexstr:
return i2h(self._con.getATR())
return self._con.getATR()
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 +119,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

@@ -21,14 +21,13 @@ import os
import argparse
from typing import Optional
import serial
from osmocom.utils import h2b, b2h, i2h, 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'
@@ -96,20 +95,20 @@ class SerialSimLink(LinkBaseTpdu):
self.reset_card()
def get_atr(self) -> Hexstr:
return i2h(self._atr)
return self._atr
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:
raise ProtocolError()
return rv
def __reset_card(self):
def _reset_card(self):
self._atr = None
rst_meth_map = {
'rts': self._sl.setRTS,
@@ -187,13 +186,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,7 +200,7 @@ 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':
# Ok, it 'could' be SW1
@@ -214,12 +213,12 @@ 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:

View File

@@ -19,19 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
from bidict import bidict
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
from construct import Optional as COptional, Computed
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import *
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from pySim.profile import CardProfile
from pySim.profile import match_uicc
from pySim import iso7816_4
# A UICC will usually also support 2G functionality. If this is the case, we
# need to add DF_GSM and DF_TELECOM along with the UICC related files
from pySim.ts_51_011 import AddonSIM, EF_ICCID, EF_PL
from pySim.ts_51_011 import AddonSIM
from pySim.gsm_r import AddonGSMR
from pySim.cdma_ruim import AddonRUIM
@@ -94,23 +94,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))
)
@@ -119,11 +123,11 @@ class FileDescriptor(BER_TLV_IE, tag=0x82):
# ETSI TS 102 221 11.1.1.4.4
class FileIdentifier(BER_TLV_IE, tag=0x83):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# ETSI TS 102 221 11.1.1.4.5
class DfName(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# ETSI TS 102 221 11.1.1.4.6.1
class UiccCharacteristics(BER_TLV_IE, tag=0x80):
@@ -169,40 +173,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
@@ -217,7 +198,7 @@ class SecurityAttribExpanded(BER_TLV_IE, tag=0xab):
# ETSI TS 102 221 11.1.1.4.7.3
class SecurityAttribReferenced(BER_TLV_IE, tag=0x8b):
# TODO: longer format with SEID
_construct = Struct('ef_arr_file_id'/Bytes(2), 'ef_arr_record_nr'/Int8ub)
_construct = Struct('ef_arr_file_id'/HexAdapter(Bytes(2)), 'ef_arr_record_nr'/Int8ub)
# ETSI TS 102 221 11.1.1.4.8
class ShortFileIdentifier(BER_TLV_IE, tag=0x88):
@@ -637,7 +618,8 @@ SC_DO = DataObjectChoice('security_condition', 'Security Condition',
# TS 102 221 Section 13.1
class EF_DIR(LinFixedEF):
_test_de_encode = [
# FIXME: re-encode failure when changing to _test_de_encode
_test_decode = [
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
{ "application_label": "USim1" },
@@ -665,6 +647,44 @@ class EF_DIR(LinFixedEF):
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):
@@ -732,7 +752,7 @@ class EF_ARR(LinFixedEF):
raise ValueError
return by_mode
def _decode_record_bin(self, raw_bin_data, **kwargs):
def _decode_record_bin(self, raw_bin_data, **_kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
dec = arr_seq.decode_multi(raw_bin_data)
@@ -740,7 +760,7 @@ class EF_ARR(LinFixedEF):
# 'un-flattening' decoder, and hence would be unable to encode :(
return dec[0]
def _encode_record_bin(self, in_json, **kwargs):
def _encode_record_bin(self, in_json, **_kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
return arr_seq.encode_multi(in_json)
@@ -785,7 +805,7 @@ class EF_UMPC(TransparentEF):
class CardProfileUICC(CardProfile):
ORDER = 10
ORDER = 1
def __init__(self, name='UICC'):
files = [
@@ -882,11 +902,9 @@ class CardProfileUICC(CardProfile):
d = t.to_dict()
return flatten_dict_lists(d['fcp_template'])
@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"])
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_uicc(scc)
@with_default_category('TS 102 221 Specific Commands')
class AddlShellCommands(CommandSet):
@@ -906,14 +924,14 @@ class CardProfileUICC(CardProfile):
'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')
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)
self._cmd.card._scc.resume_uicc(opts.token)
term_cap_parser = argparse.ArgumentParser()
# power group

View File

@@ -19,22 +19,14 @@
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 pySim.utils import b2h, auto_uint8, auto_uint16, is_hexstr
from pySim.ts_102_221 import *
def expand_pattern(pattern: bytes, repeat: bool, size: int) -> bytes:
"""Expand the fill/repeat pattern as per TS 102 222 Section 6.3.2.2.2 Tags C1/C2."""
if not repeat:
pad_len = size - len(pattern)
return pattern + pattern[-1:] * pad_len
else:
count = size // len(pattern)
part_len = size - count * len(pattern)
return pattern * count + pattern[:part_len]
@with_default_category('TS 102 222 Administrative Commands')
class Ts102222Commands(CommandSet):
"""Administrative commands for telecommunication applications."""
@@ -52,7 +44,7 @@ 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)
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]:
@@ -73,7 +65,7 @@ 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)
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]:
@@ -89,7 +81,7 @@ 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)
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]:
@@ -111,6 +103,7 @@ class Ts102222Commands(CommandSet):
(_data, _sw) = self._cmd.lchan.scc.terminate_card_usage()
create_parser = argparse.ArgumentParser()
create_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
create_parser._action_groups.pop()
create_required = create_parser.add_argument_group('required arguments')
create_optional = create_parser.add_argument_group('optional arguments')
@@ -122,7 +115,6 @@ class Ts102222Commands(CommandSet):
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')
@cmd2.with_argparser(create_parser)
def do_create_ef(self, opts):
@@ -158,6 +150,7 @@ class Ts102222Commands(CommandSet):
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
createdf_parser = argparse.ArgumentParser()
createdf_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
createdf_parser._action_groups.pop()
createdf_required = createdf_parser.add_argument_group('required arguments')
createdf_optional = createdf_parser.add_argument_group('optional arguments')
@@ -172,7 +165,6 @@ class Ts102222Commands(CommandSet):
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):
@@ -209,15 +201,15 @@ class Ts102222Commands(CommandSet):
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')
@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)

View File

@@ -17,19 +17,20 @@ 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):
class Msk(BER_TLV_IE, tag=0x80):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class Emsk(BER_TLV_IE, tag=0x81):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class MskCollection(TLV_IE_Collection, nested=[EF_EAPKEYS.Msk, EF_EAPKEYS.Emsk]):
pass

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