28 Commits

Author SHA1 Message Date
Philipp Maier
473f31066c pySim/pcsc/cosmetic: reformat comment
Change-Id: Ic04bdfbc6727cc670679c377c1afd1de53504b8f
2026-03-23 19:03:19 +00:00
Philipp Maier
b59363b49e pySim/EF.SMSP: remove superflous line break
Change-Id: Ie02e02546e708e2c339810812188bd8e8af2a720
2026-03-23 16:44:38 +01:00
Vadim Yanitskiy
115b517c6a esim/saip: raise an exception properly
Change-Id: Ia3749c02120fdc16e556214d0461cbeca032447b
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
99aef1fecf cdma_ruim: fix inaccurate comment for EF_AD
Change-Id: I71ea27fd30e44685ff35f49843072ca392995973
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
caddd1c7a0 ts_31_102: EF_5G_PROSE_UIR: fix copy-pasted inner class name
Change-Id: I460e5ad70f35026d0d794271a4aef17323c14dfb
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
11a7a7e3b1 ts_31_102: fix description for EF_5GS3GPPLOCI
Change-Id: I9cf3adfce65090fedb3f0fd33c9b3d15a2c5fb8c
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
5138208ee6 ts_51_011: EF.EXT[6-7]: fix typo in desc
Change-Id: I93df1c9fd8a4d588ed7ed19ec2dc1d304412fc3d
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
5b2fabde62 utils: DataObjectCollection.encode(): fix TypeError
`members_by_name` is a plain dictionary.  Calling it with `()` raises:

  TypeError: 'dict' object is not callable

Change-Id: I7e0c09aa7303f1506fe3a025fdc3779919dd0e6c
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
24127e985a utils: dec_plmn(): remove redundant call
Change-Id: Ic95c3992ed57eb8fee952ec2dc7f092dd7689579
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
09ae327f8b ota: OtaAlgo{Crypt,Auth}: fix algo_auth vs algo_crypt
* OtaAlgoCrypt.from_keyset() searches by `otak.algo_crypt`
  but the error message prints `otak.algo_auth`.  Should be
  `otak.algo_crypt` instead.

* OtaAlgoAuth.__init__() checks `algo_auth` but the error message
  prints `algo_crypt`.  Should be `otak.algo_auth` instead.

Change-Id: Ia636fffaeadc68e3f6d5b65d477e753834c95895
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
d32bce19f6 sms: fix flags_construct in SMS_DELIVER
* field `tp_rp` appears at bit positions 7 and 5
** bit 7 should be `tp_rp` (Reply Path)
** bit 5 should be `tp_sri` (Status Report Indication)
* field `tp_lp` is completely missing
** should be at bit position 3

Change-Id: I0274849f0fa07281b5e050af429ffda7d249f9e8
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
83bfdc0d3b ara_m: fix undefined variable used in a format-string
Change-Id: I310a5d461bae2b5e4d8e07097000b079c23aa0f6
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
14ec52a06c ara_m: fix exceptions not being raised properly
Exceptions are meant to be thrown/raised, not returned.

Change-Id: Id799c264447e22887edcd2dc7eb991cf0af1bbfc
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
209d13e233 global_platform: fix docstring for Scp03SessionKeys._get_icv()
Change-Id: I8983bc27f581295544360ba8b4ae1d28b3ea850f
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
3b50e64c8b global_platform: fix s/GET/STORE/ DATA in docs
Both `do_store_data` and `store_data` have identical docstrings that
incorrectly describe the command as GET DATA.  Should be "STORE DATA".
Take a chance to fix missing space between `v2.3` and `Section`.

Change-Id: I33fc80ab8ca50fadc38217b0005eec6169c8e34e
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
b76cc80ea1 global_platform: fix store_data() returning last chunk only
The loop builds up `response` across multiple STORE DATA blocks,
but the function returns only `data` - the response from the
*last* block.  It should return the accumulated response instead.

Change-Id: I3e15c8004d1e366e8c3896e559656622f48bb1a2
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
3b87ba3cba global_platform: fix typo in ApplicationTemplate
The keyword argument should be `nested=`.  As written `ApplicationAID`
is silently ignored - `ApplicationTemplate` will not descend into its
nested TLVs.

Change-Id: If45dbb0c9b09fe53560d109957ce339267a9f2b0
2026-03-20 14:32:59 -07:00
Vadim Yanitskiy
ea1d5af383 global_platform: fix typo in SupportedTlsCipherSuitesForScp81
The attribute name is misspelled.  The BER-TLV infrastructure looks
for `_construct`; this typo means `SupportedTlsCipherSuitesForScp81`
will never decode its content.

Change-Id: I0f637951b0eeb7eca2a8b543baa737f216a935ed
2026-03-20 14:32:59 -07:00
Philipp Maier
0634f77308 esim/http_json_api: allow URL rewriting
The URL used when HTTP requests are performed is defined statically
with the url_prefix passed to the constructor of JsonHttpApiClient
together with the path property in JsonHttpApiFunction.

For applications that require dynamic URLs there is no way to rewrite
the URL. Let's add a mechanism that allows API users to apply custom
URL reqriting rules by adding a rewrite_url method to
JsonHttpApiFunction. API users may then overload this method with a
custom implementation as needed.

Related: SYS#7918
Change-Id: Id2713a867079cc140517fe312189e5e2162608a5
2026-03-17 11:17:12 +01:00
Vadim Yanitskiy
a5a5865c7c cdma_ruim: fix copy-pasted desc for EF.AD
Change-Id: I2338f35c21978dd6b8916c0abd57b94f5e087655
2026-03-10 17:43:10 +00:00
Harald Welte
3752aeb94e pySim.esim.saip.File: Support pinStatusTemplateDO + lcsi
The tuples defining a DF or ADF in an eSIM template must contain a
pinStatusTemplateDO.  When parsing tuples into a File() instance, we
must save it, and re-create it at the time we re-encode that file.

Same applies to the lcsi (life cycle state indicator), which may
optionally exist for any file.

Change-Id: I073aa4374f2cd664d07fa0224bf0d4c809cdf4aa
Closes: OS#6955
2026-03-10 16:34:48 +00:00
Philipp Maier
914abe3309 docs/smpp-ota-tool: Add documentation/tutorial
We already have documentation that explains how to run pySim-smpp2sim.
With smpp-ota-tool we now have a counterpart for pySim-smpp2sim, so
let's add documentation for this tool as well.

Related: SYS#7881
Change-Id: If0d18a263f5a6dc035b90f5c5c6a942d46bbba49
2026-03-10 09:23:03 +00:00
Philipp Maier
84754b6ebb contrib/smpp-ota-tool: define commandline arguments in global scope
The commandline arguments are currently defined under __main__ in a
private scope. From there they are not reachable to the sphinx
argparse module. We have to define the arguments globally at the
top. (like in the other applications)

Related: SYS#7881
Change-Id: I2d9782e3f5b1cac78c22d206fdcac4118c7d5e7c
2026-03-10 09:23:03 +00:00
Philipp Maier
c47005d408 contrib/smpp-ota-tool: use '-' instead of '_' in command line args
Some commandline arguments have an underscore in their name. Let's
replace those with dashes.

Change-Id: Icbe9d753d59263997e9ca34d46ed0daca36ca16c
Related: SYS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
2dfaac6e4f contrib/smpp-ota-tool: fix description string (copy+paste error)
Change-Id: I559844bfa1ac372370ef9d148f2f8a6bf4ab4ef5
Related: SYS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
a615ba5138 tests/pySim-smpp2sim_test: add testcases for AES128 and AES256
Extend the existing test script so that it can handle multiple
testcases. Also add support for switching eUICC profiles.
Finally, add a testcases to test OTA-SMS (RFM) with AES128 and
AES256 encryption.

Change-Id: I1f10504f3a29a8c74a17991632d932819fecfa5a
Related: OS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
8ee10ab1a5 tests/pySim-smpp2sim_test/card_sanitizer: update card backup with new test keyset
In our test setup we run the card_sanitizer.py script regualary to ensure that
we have consistent start conditions when running our tests. In case a testcase
crashes for some reason and leaves messed up files on a test card. The
card_sanitizer.py script will ensure that any problem like that is cleaned up
over night.

For the testcases we are about to add in the patch following this one, we need
to provision a new test keyset to one of our test cards. This has been already
done manually. However since the card_sanitizer still has the old keys in its
backup we will have to update that as well.

Change-Id: I5aa8a413b19b3e43a79d03e904daab50b4b1e767
Related: OS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
f10af30aed global_platform/scp: fix dek_encrypt/dek_decrypt for SCP02
The methods dek_encrypt/dek_decrypt use the wrong algorithm and the
wrong key material. The algorithm should be 3DES rather then single
DES and the key must be the DEK session key instead of the static
DEK key from which the DEK session key is derived.

Related: SYS#7902
Change-Id: I3d0cc7378680b346fa39152c8b7074446d2c869d
2026-03-06 15:51:19 +01:00
12 changed files with 82 additions and 40 deletions

View File

@@ -72,10 +72,10 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if do[0] == 0x01: if do[0] == 0x01:
self.decoded = {'generic_access_rule': 'always'} self.decoded = {'generic_access_rule': 'always'}
return self.decoded return self.decoded
return ValueError('Invalid 1-byte generic APDU access rule') raise ValueError('Invalid 1-byte generic APDU access rule')
else: else:
if len(do) % 8: if len(do) % 8:
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do)) raise ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
self.decoded = {'apdu_filter': []} self.decoded = {'apdu_filter': []}
offset = 0 offset = 0
while offset < len(do): while offset < len(do):
@@ -90,19 +90,19 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
return b'\x00' return b'\x00'
if self.decoded['generic_access_rule'] == 'always': if self.decoded['generic_access_rule'] == 'always':
return b'\x01' return b'\x01'
return ValueError('Invalid 1-byte generic APDU access rule') raise ValueError('Invalid 1-byte generic APDU access rule')
else: else:
if not 'apdu_filter' in self.decoded: if not 'apdu_filter' in self.decoded:
return ValueError('Invalid APDU AR DO') raise ValueError('Invalid APDU AR DO')
filters = self.decoded['apdu_filter'] filters = self.decoded['apdu_filter']
res = b'' res = b''
for f in filters: for f in filters:
if not 'header' in f or not 'mask' in f: if not 'header' in f or not 'mask' in f:
return ValueError('APDU filter must contain header and mask') raise ValueError('APDU filter must contain header and mask')
header_b = h2b(f['header']) header_b = h2b(f['header'])
mask_b = h2b(f['mask']) mask_b = h2b(f['mask'])
if len(header_b) != 4 or len(mask_b) != 4: if len(header_b) != 4 or len(mask_b) != 4:
return ValueError('APDU filter header and mask must each be 4 bytes') raise ValueError('APDU filter header and mask must each be 4 bytes')
res += header_b + mask_b res += header_b + mask_b
return res return res
@@ -269,7 +269,7 @@ class ADF_ARAM(CardADF):
cmd_do_enc = cmd_do.to_ie() cmd_do_enc = cmd_do.to_ie()
cmd_do_len = len(cmd_do_enc) cmd_do_len = len(cmd_do_enc)
if cmd_do_len > 255: if cmd_do_len > 255:
return ValueError('DO > 255 bytes not supported yet') raise ValueError('DO > 255 bytes not supported yet')
else: else:
cmd_do_enc = b'' cmd_do_enc = b''
cmd_do_len = 0 cmd_do_len = 0
@@ -361,7 +361,7 @@ class ADF_ARAM(CardADF):
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}] ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
elif opts.apdu_filter: elif opts.apdu_filter:
if len(opts.apdu_filter) % 16: if len(opts.apdu_filter) % 16:
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do)) raise ValueError(f'Invalid non-modulo-16 length of APDU filter: {len(opts.apdu_filter)}')
offset = 0 offset = 0
apdu_filter = [] apdu_filter = []
while offset < len(opts.apdu_filter): while offset < len(opts.apdu_filter):

View File

@@ -128,10 +128,10 @@ class EF_AD(TransparentEF):
cell_test = 0x04 cell_test = 0x04
def __init__(self, fid='6f43', sfid=None, name='EF.AD', def __init__(self, fid='6f43', sfid=None, name='EF.AD',
desc='Service Provider Name', size=(3, None), **kwargs): desc='Administrative Data', size=(3, None), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct( self._construct = Struct(
# Byte 1: Display Condition # Byte 1: MS operation mode
'ms_operation_mode'/Enum(Byte, self.OP_MODE), 'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information # Bytes 2-3: Additional information
'additional_info'/Bytes(2), 'additional_info'/Bytes(2),

View File

@@ -19,7 +19,7 @@ import abc
import requests import requests
import logging import logging
import json import json
from typing import Optional from typing import Optional, Tuple
import base64 import base64
from twisted.web.server import Request from twisted.web.server import Request
@@ -180,7 +180,7 @@ class JsonHttpApiFunction(abc.ABC):
# receives from the a requesting client. The same applies vice versa to class variables that have an "output_" # receives from the a requesting client. The same applies vice versa to class variables that have an "output_"
# prefix. # prefix.
# path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder') # path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder', see also method rewrite_url).
path = None path = None
# dictionary of input parameters. key is parameter name, value is ApiParam class # dictionary of input parameters. key is parameter name, value is ApiParam class
@@ -336,6 +336,22 @@ class JsonHttpApiFunction(abc.ABC):
output[p] = p_class.decode(v) output[p] = p_class.decode(v)
return output return output
def rewrite_url(self, data: dict, url: str) -> Tuple[dict, str]:
"""
Rewrite a static URL using information passed in the data dict. This method may be overloaded by a derived
class to allow fully dynamic URLs. The input parameters required for the URL rewriting may be passed using
data parameter. In case those parameters are additional parameters that are not intended to be passed to
the encode_client method later, they must be removed explcitly.
Args:
data: (see JsonHttpApiClient and JsonHttpApiServer)
url: statically generated URL string (see comment in JsonHttpApiClient)
"""
# This implementation is a placeholder in which we do not perform any URL rewriting. We just pass through data
# and url unmodified.
return data, url
class JsonHttpApiClient(): class JsonHttpApiClient():
def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str], def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str],
session: requests.Session): session: requests.Session):
@@ -352,8 +368,16 @@ class JsonHttpApiClient():
self.session = session self.session = session
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]: 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.""" Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as
json-serializable fields. `data` may also contain additional parameters required for URL rewriting (see
rewrite_url in class JsonHttpApiFunction). Output data is returned as json-deserialized dict.
Args:
data: Input data required to perform the request.
func_call_id: Function Call Identifier, if present a header field is generated automatically.
timeout: Maximum amount of time to wait for the request to complete.
"""
# In case a function caller ID is supplied, use it together with the stored function requestor ID to generate # In case a function caller ID is supplied, use it together with the stored function requestor ID to generate
# and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header # and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header
@@ -362,6 +386,11 @@ class JsonHttpApiClient():
data = {'header' : {'functionRequesterIdentifier': self.func_req_id, data = {'header' : {'functionRequesterIdentifier': self.func_req_id,
'functionCallIdentifier': func_call_id}} | data 'functionCallIdentifier': func_call_id}} | data
# The URL used for the HTTP request (see below) normally consists of the initially given url_prefix
# concatenated with the path defined by the JsonHttpApiFunction definition. This static URL path may be
# rewritten by rewrite_url method defined in the JsonHttpApiFunction.
data, url = self.api_func.rewrite_url(data, self.url_prefix + self.api_func.path)
# Encode the message (the presence of mandatory fields is checked during encoding) # Encode the message (the presence of mandatory fields is checked during encoding)
encoded = json.dumps(self.api_func.encode_client(data)) encoded = json.dumps(self.api_func.encode_client(data))
@@ -373,7 +402,6 @@ class JsonHttpApiClient():
req_headers.update(self.api_func.extra_http_req_headers) req_headers.update(self.api_func.extra_http_req_headers)
# Perform HTTP request # Perform HTTP request
url = self.url_prefix + self.api_func.path
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded)) logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout) response = self.session.request(self.api_func.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-STS: [%u] hdr: %s" % (response.status_code, response.headers))

View File

@@ -151,6 +151,8 @@ class File:
self.df_name = None self.df_name = None
self.fill_pattern = None self.fill_pattern = None
self.fill_pattern_repeat = False self.fill_pattern_repeat = False
self.pstdo = None # pinStatusTemplateDO, mandatory for DF/ADF
self.lcsi = None # optional life cycle status indicator
# apply some defaults from profile # apply some defaults from profile
if self.template: if self.template:
self.from_template(self.template) self.from_template(self.template)
@@ -278,6 +280,8 @@ class File:
elif self.file_type in ['MF', 'DF', 'ADF']: elif self.file_type in ['MF', 'DF', 'ADF']:
fdb_dec['file_type'] = 'df' fdb_dec['file_type'] = 'df'
fdb_dec['structure'] = 'no_info_given' fdb_dec['structure'] = 'no_info_given'
# pinStatusTemplateDO is mandatory for DF/ADF
fileDescriptor['pinStatusTemplateDO'] = self.pstdo
# build file descriptor based on above input data # build file descriptor based on above input data
fd_dict = {} fd_dict = {}
if len(fdb_dec): if len(fdb_dec):
@@ -304,6 +308,8 @@ class File:
# desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles # desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles
# downloaded to a V2.2 or earlier eUICC. # downloaded to a V2.2 or earlier eUICC.
fileDescriptor['proprietaryEFInfo'] = pefi fileDescriptor['proprietaryEFInfo'] = pefi
if self.lcsi:
fileDescriptor['lcsi'] = self.lcsi
logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor)) logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor))
return fileDescriptor return fileDescriptor
@@ -323,6 +329,8 @@ class File:
if efFileSize: if efFileSize:
self._file_size = self._decode_file_size(efFileSize) self._file_size = self._decode_file_size(efFileSize)
self.pstdo = fileDescriptor.get('pinStatusTemplateDO', None)
self.lcsi = fileDescriptor.get('lcsi', None)
pefi = fileDescriptor.get('proprietaryEFInfo', {}) pefi = fileDescriptor.get('proprietaryEFInfo', {})
securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None) securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None)
if securityAttributesReferenced: if securityAttributesReferenced:
@@ -433,7 +441,7 @@ class File:
elif k == 'fillFileContent': elif k == 'fillFileContent':
stream.write(v) stream.write(v)
else: else:
return ValueError("Unknown key '%s' in tuple list" % k) raise ValueError("Unknown key '%s' in tuple list" % k)
return stream.getvalue() return stream.getvalue()
def file_content_to_tuples(self, optimize:bool = False) -> List[Tuple]: def file_content_to_tuples(self, optimize:bool = False) -> List[Tuple]:

View File

@@ -276,7 +276,7 @@ class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82): class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04) _construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83): class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
_consuruct = GreedyRange(Int16ub) _construct = GreedyRange(Int16ub)
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03, class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
SupportedTlsCipherSuitesForScp81]): SupportedTlsCipherSuitesForScp81]):
pass pass
@@ -319,7 +319,7 @@ class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
# GlobalPlatform v2.3.1 Section 11.3.3.1.3 # GlobalPlatform v2.3.1 Section 11.3.3.1.3
class ApplicationAID(BER_TLV_IE, tag=0x4f): class ApplicationAID(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes _construct = GreedyBytes
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]): class ApplicationTemplate(BER_TLV_IE, tag=0x61, nested=[ApplicationAID]):
pass pass
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]): class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
pass pass
@@ -562,14 +562,14 @@ class ADF_SD(CardADF):
@cmd2.with_argparser(store_data_parser) @cmd2.with_argparser(store_data_parser)
def do_store_data(self, opts): def do_store_data(self, opts):
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data. """Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3Section 11.11 for details.""" See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
response_permitted = opts.response == 'may_be_returned' response_permitted = opts.response == 'may_be_returned'
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted) self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes: def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data. """Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3Section 11.11 for details.""" See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
max_cmd_len = self._cmd.lchan.scc.max_cmd_len max_cmd_len = self._cmd.lchan.scc.max_cmd_len
# Table 11-89 of GP Card Specification v2.3 # Table 11-89 of GP Card Specification v2.3
remainder = data remainder = data
@@ -585,7 +585,7 @@ class ADF_SD(CardADF):
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) + "00")
block_nr += 1 block_nr += 1
response += data response += data
return data return h2b(response)
put_key_parser = argparse.ArgumentParser() put_key_parser = argparse.ArgumentParser()
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number') put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')

View File

@@ -266,11 +266,13 @@ class SCP02(SCP):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def dek_encrypt(self, plaintext:bytes) -> bytes: def dek_encrypt(self, plaintext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB) # See also GPC section B.1.1.2, E.4.7, and E.4.1
cipher = DES3.new(self.sk.data_enc, DES.MODE_ECB)
return cipher.encrypt(plaintext) return cipher.encrypt(plaintext)
def dek_decrypt(self, ciphertext:bytes) -> bytes: def dek_decrypt(self, ciphertext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB) # See also GPC section B.1.1.2, E.4.7, and E.4.1
cipher = DES3.new(self.sk.data_enc, DES.MODE_ECB)
return cipher.decrypt(ciphertext) return cipher.decrypt(ciphertext)
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes): def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
@@ -436,7 +438,7 @@ class Scp03SessionKeys:
"""Obtain the ICV value computed as described in 6.2.6. """Obtain the ICV value computed as described in 6.2.6.
This method has two modes: This method has two modes:
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter. * is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
* is_response=False for computing the ICV for R-DEC.""" * is_response=True for computing the ICV for R-DEC."""
if not is_response: if not is_response:
self.block_nr += 1 self.block_nr += 1
# The binary value of this number SHALL be left padded with zeroes to form a full block. # The binary value of this number SHALL be left padded with zeroes to form a full block.

View File

@@ -221,12 +221,12 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
for subc in cls.__subclasses__(): for subc in cls.__subclasses__():
if subc.enum_name == otak.algo_crypt: if subc.enum_name == otak.algo_crypt:
return subc(otak) return subc(otak)
raise ValueError('No implementation for crypt algorithm %s' % otak.algo_auth) raise ValueError('No implementation for crypt algorithm %s' % otak.algo_crypt)
class OtaAlgoAuth(OtaAlgo, abc.ABC): class OtaAlgoAuth(OtaAlgo, abc.ABC):
def __init__(self, otak: OtaKeyset): def __init__(self, otak: OtaKeyset):
if self.enum_name != otak.algo_auth: if self.enum_name != otak.algo_auth:
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_crypt)) raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_auth))
super().__init__(otak) super().__init__(otak)
def sign(self, data:bytes) -> bytes: def sign(self, data:bytes) -> bytes:

View File

@@ -169,8 +169,14 @@ class SMS_TPDU(abc.ABC):
class SMS_DELIVER(SMS_TPDU): class SMS_DELIVER(SMS_TPDU):
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction.""" """Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
flags_construct = BitStruct('tp_rp'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag, 'tp_sri'/Flag, flags_construct = BitStruct('tp_rp'/Flag,
Padding(1), 'tp_mms'/Flag, 'tp_mti'/BitsInteger(2)) 'tp_udhi'/Flag,
'tp_sri'/Flag,
Padding(1),
'tp_lp'/Flag,
'tp_mms'/Flag,
'tp_mti'/BitsInteger(2))
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs['tp_mti'] = 0 kwargs['tp_mti'] = 0
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@@ -80,8 +80,7 @@ class PcscSimLink(LinkBaseTpdu):
def connect(self): def connect(self):
try: try:
# To avoid leakage of resources, make sure the reader # To avoid leakage of resources, make sure the reader is disconnected
# is disconnected
self.disconnect() self.disconnect()
# Make card connection and select a suitable communication protocol # Make card connection and select a suitable communication protocol

View File

@@ -1058,7 +1058,7 @@ class EF_OCSGL(LinFixedEF):
# TS 31.102 Section 4.4.11.2 (Rel 15) # TS 31.102 Section 4.4.11.2 (Rel 15)
class EF_5GS3GPPLOCI(TransparentEF): class EF_5GS3GPPLOCI(TransparentEF):
def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20), def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20),
desc='5S 3GP location information', **kwargs): desc='5GS 3GPP location information', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
upd_status_constr = Enum( upd_status_constr = Enum(
Byte, updated=0, not_updated=1, roaming_not_allowed=2) Byte, updated=0, not_updated=1, roaming_not_allowed=2)
@@ -1326,7 +1326,7 @@ class EF_5G_PROSE_UIR(TransparentEF):
pass pass
class FiveGDdnmfCtfAddrForUploading(BER_TLV_IE, tag=0x97): class FiveGDdnmfCtfAddrForUploading(BER_TLV_IE, tag=0x97):
pass pass
class ProSeConfigDataForUeToNetworkRelayUE(BER_TLV_IE, tag=0xa0, class ProSeConfigDataForUsageInfoReporting(BER_TLV_IE, tag=0xa0,
nested=[EF_5G_PROSE_DD.ValidityTimer, nested=[EF_5G_PROSE_DD.ValidityTimer,
CollectionPeriod, ReportingWindow, CollectionPeriod, ReportingWindow,
ReportingIndicators, ReportingIndicators,
@@ -1336,7 +1336,7 @@ class EF_5G_PROSE_UIR(TransparentEF):
desc='5G ProSe configuration data for usage information reporting', **kwargs): desc='5G ProSe configuration data for usage information reporting', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
# contains TLV structure despite being TransparentEF, not BER-TLV ?!? # contains TLV structure despite being TransparentEF, not BER-TLV ?!?
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUeToNetworkRelayUE self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUsageInfoReporting
# TS 31.102 Section 4.4.13.8 (Rel 18) # TS 31.102 Section 4.4.13.8 (Rel 18)
class EF_5G_PROSE_U2URU(TransparentEF): class EF_5G_PROSE_U2URU(TransparentEF):

View File

@@ -309,7 +309,6 @@ class EF_SMSP(LinFixedEF):
'tp_dest_addr'/Flag)), 'tp_dest_addr'/Flag)),
'tp_dest_addr'/ScAddr, 'tp_dest_addr'/ScAddr,
'tp_sc_addr'/ScAddr, 'tp_sc_addr'/ScAddr,
'tp_pid'/Bytes(1), 'tp_pid'/Bytes(1),
'tp_dcs'/Bytes(1), 'tp_dcs'/Bytes(1),
'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte)) 'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte))
@@ -1117,8 +1116,8 @@ class DF_GSM(CardDF):
EF_MBI(), EF_MBI(),
EF_MWIS(), EF_MWIS(),
EF_CFIS(), EF_CFIS(),
EF_EXT('6fc8', None, 'EF.EXT6', desc='Externsion6 (MBDN)'), EF_EXT('6fc8', None, 'EF.EXT6', desc='Extension6 (MBDN)'),
EF_EXT('6fcc', None, 'EF.EXT7', desc='Externsion7 (CFIS)'), EF_EXT('6fcc', None, 'EF.EXT7', desc='Extension7 (CFIS)'),
EF_SPDI(), EF_SPDI(),
EF_MMSN(), EF_MMSN(),
EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'), EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'),

View File

@@ -139,7 +139,6 @@ def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
def dec_plmn(threehexbytes: Hexstr) -> dict: def dec_plmn(threehexbytes: Hexstr) -> dict:
res = {'mcc': "0", 'mnc': "0"} res = {'mcc': "0", 'mnc': "0"}
dec_mcc_from_plmn_str(threehexbytes)
res['mcc'] = dec_mcc_from_plmn_str(threehexbytes) res['mcc'] = dec_mcc_from_plmn_str(threehexbytes)
res['mnc'] = dec_mnc_from_plmn_str(threehexbytes) res['mnc'] = dec_mnc_from_plmn_str(threehexbytes)
return res return res
@@ -911,7 +910,8 @@ class DataObjectCollection:
def encode(self, decoded) -> bytes: def encode(self, decoded) -> bytes:
res = bytearray() res = bytearray()
for i in decoded: for i in decoded:
obj = self.members_by_name(i[0]) name = i[0]
obj = self.members_by_name[name]
res.append(obj.to_tlv()) res.append(obj.to_tlv())
return res return res