forked from public/pysim
Compare commits
19 Commits
neels/saip3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 41e0d532f0 | |||
| e03530f89a | |||
| 078ac2bf19 | |||
| c582b5fee3 | |||
| d4717bd014 | |||
| 1cfb0f3da2 | |||
| cb3eb77236 | |||
| f381255639 | |||
| d13be84ccd | |||
| f4eb2f9356 | |||
| bb362482e8 | |||
| 9c77e4ed94 | |||
| ab19049d19 | |||
| 25e43e1540 | |||
| 6e10da4c55 | |||
| 973d6eb2cc | |||
| 597f1e0398 | |||
| 45d37ed959 | |||
| 757c7d048e |
+30
-48
@@ -24,21 +24,21 @@ import traceback
|
||||
import re
|
||||
import cmd2
|
||||
from packaging import version
|
||||
from cmd2 import style
|
||||
|
||||
import logging
|
||||
from pySim.log import PySimLogger
|
||||
from osmocom.utils import auto_uint8
|
||||
|
||||
# cmd2 >= 2.3.0 has deprecated the bg/fg in favor of Bg/Fg :(
|
||||
if version.parse(cmd2.__version__) < version.parse("2.3.0"):
|
||||
from cmd2 import fg, bg # pylint: disable=no-name-in-module
|
||||
RED = fg.red
|
||||
YELLOW = fg.yellow
|
||||
LIGHT_RED = fg.bright_red
|
||||
LIGHT_GREEN = fg.bright_green
|
||||
# cmd2 >= 3.0 replaced Fg + style() with Color + stylize()
|
||||
if version.parse(cmd2.__version__) >= version.parse("3.0.0"):
|
||||
from cmd2 import Color, stylize # pylint: disable=no-name-in-module
|
||||
RED = Color.RED
|
||||
YELLOW = Color.YELLOW
|
||||
LIGHT_RED = Color.BRIGHT_RED
|
||||
LIGHT_GREEN = Color.BRIGHT_GREEN
|
||||
def style(text, fg=None, bg=None, bold=False): # pylint: disable=function-redefined
|
||||
return stylize(text, fg) if fg else text
|
||||
else:
|
||||
from cmd2 import Fg, Bg # pylint: disable=no-name-in-module
|
||||
from cmd2 import style, Fg # pylint: disable=no-name-in-module
|
||||
RED = Fg.RED
|
||||
YELLOW = Fg.YELLOW
|
||||
LIGHT_RED = Fg.LIGHT_RED
|
||||
@@ -76,43 +76,19 @@ from pySim.app import init_card
|
||||
|
||||
log = PySimLogger.get(Path(__file__).stem)
|
||||
|
||||
class Cmd2Compat(cmd2.Cmd):
|
||||
"""Backwards-compatibility wrapper around cmd2.Cmd to support older and newer
|
||||
releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
|
||||
def run_editor(self, file_path: Optional[str] = None) -> None:
|
||||
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
|
||||
return self._run_editor(file_path) # pylint: disable=no-member
|
||||
else:
|
||||
return super().run_editor(file_path) # pylint: disable=no-member
|
||||
|
||||
class Settable2Compat(cmd2.Settable):
|
||||
"""Backwards-compatibility wrapper around cmd2.Settable to support older and newer
|
||||
releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
|
||||
def __init__(self, name, val_type, description, settable_object, **kwargs):
|
||||
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
|
||||
super().__init__(name, val_type, description, **kwargs) # pylint: disable=no-value-for-parameter
|
||||
else:
|
||||
super().__init__(name, val_type, description, settable_object, **kwargs) # pylint: disable=too-many-function-args
|
||||
|
||||
class PysimApp(Cmd2Compat):
|
||||
class PysimApp(cmd2.Cmd):
|
||||
CUSTOM_CATEGORY = 'pySim Commands'
|
||||
BANNER = """Welcome to pySim-shell!
|
||||
(C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
|
||||
Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html """
|
||||
|
||||
def __init__(self, verbose, card, rs, sl, ch, script=None):
|
||||
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
|
||||
kwargs = {'use_ipython': True}
|
||||
else:
|
||||
kwargs = {'include_ipy': True}
|
||||
|
||||
self.verbose = verbose
|
||||
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
|
||||
self._onchange_verbose('verbose', False, self.verbose)
|
||||
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
|
||||
auto_load_commands=False, startup_script=script, **kwargs)
|
||||
auto_load_commands=False, startup_script=script, include_ipy=True)
|
||||
self.intro = style(self.BANNER, fg=RED)
|
||||
self.default_category = 'pySim-shell built-in commands'
|
||||
self.card = None
|
||||
@@ -128,18 +104,24 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
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))
|
||||
self.add_settable(Settable2Compat('conserve_write', bool, 'Read and compare before write', self,
|
||||
onchange_cb=self._onchange_conserve_write))
|
||||
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,
|
||||
'Strictly apply APDU format according to ISO/IEC 7816-3, table 12', self))
|
||||
self.add_settable(Settable2Compat('verbose', bool,
|
||||
'Enable/disable verbose logging', self,
|
||||
onchange_cb=self._onchange_verbose))
|
||||
self.add_settable(cmd2.Settable('numeric_path', bool,
|
||||
'Print File IDs instead of names',
|
||||
self, onchange_cb=self._onchange_numeric_path))
|
||||
self.add_settable(cmd2.Settable('conserve_write', bool,
|
||||
'Read and compare before write',
|
||||
self, onchange_cb=self._onchange_conserve_write))
|
||||
self.add_settable(cmd2.Settable('json_pretty_print', bool,
|
||||
'Pretty-Print JSON output',
|
||||
self))
|
||||
self.add_settable(cmd2.Settable('apdu_trace', bool,
|
||||
'Trace and display APDUs exchanged with card',
|
||||
self, onchange_cb=self._onchange_apdu_trace))
|
||||
self.add_settable(cmd2.Settable('apdu_strict', bool,
|
||||
'Strictly apply APDU format according to ISO/IEC 7816-3, table 12',
|
||||
self))
|
||||
self.add_settable(cmd2.Settable('verbose', bool,
|
||||
'Enable/disable verbose logging',
|
||||
self, onchange_cb=self._onchange_verbose))
|
||||
self.equip(card, rs)
|
||||
|
||||
def equip(self, card, rs):
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ class Tracer:
|
||||
try:
|
||||
apdu = self.source.read()
|
||||
apdu_counter = apdu_counter + 1
|
||||
except StopIteration:
|
||||
except (StopIteration, KeyboardInterrupt):
|
||||
print("%i APDUs parsed, stop iteration." % apdu_counter)
|
||||
return 0
|
||||
|
||||
|
||||
+49
-38
@@ -300,6 +300,51 @@ class ADF_ARAM(CardADF):
|
||||
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80cadf21', cmd_do, ResponseAramConfigDO)
|
||||
|
||||
@staticmethod
|
||||
def store_ref_ar_do(scc, aid:Hexstr, aid_empty:bool, device_app_id:Hexstr, pkg_ref:str,
|
||||
apdu_filter:Hexstr, apdu_never:bool, apdu_always:bool,
|
||||
nfc_always:bool, nfc_never:bool, android_permissions:Hexstr):
|
||||
# REF
|
||||
ref_do_content = []
|
||||
if aid is not None:
|
||||
ref_do_content += [{'aid_ref_do': aid}]
|
||||
elif aid_empty:
|
||||
ref_do_content += [{'aid_ref_empty_do': None}]
|
||||
ref_do_content += [{'dev_app_id_ref_do': device_app_id}]
|
||||
if pkg_ref:
|
||||
ref_do_content += [{'pkg_ref_do': {'package_name_string': pkg_ref}}]
|
||||
# AR
|
||||
ar_do_content = []
|
||||
if apdu_never:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
|
||||
elif apdu_always:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
||||
elif apdu_filter:
|
||||
if len(apdu_filter) % 16:
|
||||
raise ValueError(f'Invalid non-modulo-16 length of APDU filter: {len(apdu_filter)}')
|
||||
offset = 0
|
||||
apdu_filter_list = []
|
||||
while offset < len(apdu_filter):
|
||||
apdu_filter_list += [{'header': apdu_filter[offset:offset+8],
|
||||
'mask': 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_list}}]
|
||||
if nfc_never:
|
||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'never'}}]
|
||||
elif nfc_always:
|
||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
|
||||
if android_permissions:
|
||||
ar_do_content += [{'perm_ar_do': {'permissions': android_permissions}}]
|
||||
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
|
||||
csrado = CommandStoreRefArDO()
|
||||
csrado.from_val_dict(d)
|
||||
return ADF_ARAM.store_data(scc, csrado)
|
||||
|
||||
@staticmethod
|
||||
def aram_delete_all(scc):
|
||||
deldo = CommandDelete()
|
||||
return ADF_ARAM.store_data(scc, deldo)
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def do_aram_get_all(self, _opts):
|
||||
@@ -344,48 +389,15 @@ class ADF_ARAM(CardADF):
|
||||
@cmd2.with_argparser(store_ref_ar_do_parse)
|
||||
def do_aram_store_ref_ar_do(self, opts):
|
||||
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a (new) access rule."""
|
||||
# REF
|
||||
ref_do_content = []
|
||||
if opts.aid is not None:
|
||||
ref_do_content += [{'aid_ref_do': opts.aid}]
|
||||
elif opts.aid_empty:
|
||||
ref_do_content += [{'aid_ref_empty_do': None}]
|
||||
ref_do_content += [{'dev_app_id_ref_do': opts.device_app_id}]
|
||||
if opts.pkg_ref:
|
||||
ref_do_content += [{'pkg_ref_do': {'package_name_string': opts.pkg_ref}}]
|
||||
# AR
|
||||
ar_do_content = []
|
||||
if opts.apdu_never:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
|
||||
elif opts.apdu_always:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
||||
elif opts.apdu_filter:
|
||||
if len(opts.apdu_filter) % 16:
|
||||
raise ValueError(f'Invalid non-modulo-16 length of APDU filter: {len(opts.apdu_filter)}')
|
||||
offset = 0
|
||||
apdu_filter = []
|
||||
while offset < len(opts.apdu_filter):
|
||||
apdu_filter += [{'header': opts.apdu_filter[offset:offset+8],
|
||||
'mask': opts.apdu_filter[offset+8:offset+16]}]
|
||||
offset += 16 # Move offset to the beginning of the next apdu_filter object
|
||||
ar_do_content += [{'apdu_ar_do': {'apdu_filter': apdu_filter}}]
|
||||
if opts.nfc_always:
|
||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
|
||||
elif opts.nfc_never:
|
||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'never'}}]
|
||||
if opts.android_permissions:
|
||||
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)
|
||||
res_do = ADF_ARAM.store_ref_ar_do(self._cmd.lchan.scc, opts.aid, opts.aid_empty, opts.device_app_id,
|
||||
opts.pkg_ref, opts.apdu_filter, opts.apdu_never, opts.apdu_always,
|
||||
opts.nfc_always, opts.nfc_never, opts.android_permissions)
|
||||
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.aram_delete_all(self._cmd.lchan.scc)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
@@ -394,7 +406,6 @@ class ADF_ARAM(CardADF):
|
||||
(Proprietary feature that is specific to sysmocom's fork of Bertrand Martel’s 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 = {
|
||||
'ARA-M': {
|
||||
|
||||
@@ -34,7 +34,7 @@ from pySim import ts_102_222
|
||||
from pySim.utils import dec_imsi
|
||||
from pySim.ts_102_221 import FileDescriptor
|
||||
from pySim.filesystem import CardADF, Path
|
||||
from pySim.ts_31_102 import ADF_USIM
|
||||
from pySim.ts_31_102 import ADF_USIM, EF_UST, EF_SUCI_Calc_Info
|
||||
from pySim.ts_31_103 import ADF_ISIM
|
||||
from pySim.esim import compile_asn1_subdir
|
||||
from pySim.esim.saip import templates
|
||||
@@ -1726,7 +1726,52 @@ class ProfileElementSequence:
|
||||
if 'BT' in ftype_list:
|
||||
svc_set.add('ber-tlv')
|
||||
# FIXME:dfLinked files (scan all files, check for non-empty Fcp.linkPath presence of DFs)
|
||||
# TODO: 5G related bits (derive from EF.UST or file presence?)
|
||||
|
||||
# 5G:
|
||||
# - When SUCI is:
|
||||
# - enabled (EF.UST 124 = true)
|
||||
# AND
|
||||
# - calculated in the USIM (EF.UST 125 = true),
|
||||
# then eUICC-Mandatory-services needs 'get-identity'.
|
||||
# - 'get-identity' implies that the eUICC must support ONE OF profile-A OR profile-B.
|
||||
# (One might assume from this that, when SUCI-CalcInfo for USIM in DF.SAIP contains both key types, then no
|
||||
# profile-A or B services need to be requested explicitly. However, the correct logic is:)
|
||||
# - Iff the SUCI-CalcInfo for USIM (DF.SAIP) contains a key of profile-A ("identifier": 1),
|
||||
# then eUICC-Mandatory-services needs 'profile-a-x25519'.
|
||||
# - Same: profile-B ("identifier": 2) needs 'profile-b-p256'.
|
||||
# - (When SUCI is calculated in the UE, then the eUICC does not need to provide any of these services.)
|
||||
suci_in_usim_enabled = False
|
||||
try:
|
||||
f_ust = self.get_pe_for_type("usim").files["ef-ust"]
|
||||
ust = EF_UST().decode_bin(f_ust.body)
|
||||
suci_in_usim_enabled = ust[124]['activated'] and ust[125]['activated']
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
if suci_in_usim_enabled:
|
||||
svc_set.add('get-identity')
|
||||
# now check for profile-a and profile-b presence
|
||||
suci_calcinfo_has_profile_a = False
|
||||
suci_calcinfo_has_profile_b = False
|
||||
try:
|
||||
f_sucici = self.get_pe_for_type("df-saip").files["ef-suci-calc-info-usim"]
|
||||
sucici = EF_SUCI_Calc_Info().decode_bin(f_sucici.body) or {}
|
||||
for prot_scheme in sucici['prot_scheme_id_list']:
|
||||
if not isinstance(prot_scheme, dict):
|
||||
continue
|
||||
ps_id = prot_scheme["identifier"]
|
||||
if ps_id == 1:
|
||||
suci_calcinfo_has_profile_a = True
|
||||
elif ps_id == 2:
|
||||
suci_calcinfo_has_profile_b = True
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
if suci_calcinfo_has_profile_a:
|
||||
# The profile has a profile-A key, so require that
|
||||
svc_set.add('profile-a-x25519')
|
||||
if suci_calcinfo_has_profile_b:
|
||||
# The profile has a profile-B key, so require that
|
||||
svc_set.add('profile-b-p256')
|
||||
|
||||
hdr_pe = self.get_pe_for_type('header')
|
||||
# patch in the 'manual' services from the existing list:
|
||||
for old_svc in hdr_pe.decoded['eUICC-Mandatory-services'].keys():
|
||||
|
||||
@@ -123,6 +123,8 @@ class BatchPersonalization:
|
||||
except Exception as e:
|
||||
raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e
|
||||
|
||||
pes.rebuild_mandatory_services()
|
||||
|
||||
yield pes
|
||||
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ import io
|
||||
import re
|
||||
from typing import List, Tuple, Generator, Optional
|
||||
|
||||
from construct.core import StreamError
|
||||
from osmocom.tlv import camel_to_snake
|
||||
from osmocom.utils import hexstr
|
||||
from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid
|
||||
from pySim.ts_31_102 import EF_AD
|
||||
from pySim.ts_51_011 import EF_SMSP
|
||||
from pySim.esim.saip import param_source
|
||||
from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence
|
||||
@@ -660,6 +662,72 @@ class SmspTpScAddr(ConfigurableParameter):
|
||||
yield { cls.name: cls.tuple_to_str((international, digits)) }
|
||||
|
||||
|
||||
class MncLen(EnumParam):
|
||||
"""MNC length. Sets only the MNC length field in EF.AD (Administrative Data).
|
||||
Accepted values: integer 2 or 3, digit strings '2' or '3', or enum names 'MNC2'/'MNC3'.
|
||||
"""
|
||||
name = 'MNC-LEN'
|
||||
example_input = '2'
|
||||
default_source = param_source.ConstantSource
|
||||
|
||||
class Values(enum.IntEnum):
|
||||
MNC2 = 2
|
||||
MNC3 = 3
|
||||
|
||||
@classmethod
|
||||
def validate_val(cls, val):
|
||||
if isinstance(val, str) and val.isdigit():
|
||||
val = int(val)
|
||||
return super().validate_val(val)
|
||||
|
||||
@classmethod
|
||||
def _get_f_ad(cls, pe: ProfileElement):
|
||||
if not hasattr(pe, 'files'):
|
||||
return None
|
||||
f_ad = pe.files.get('ef-ad', None)
|
||||
if f_ad and f_ad.body:
|
||||
return f_ad
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _decode_f_ad(cls, f_ad):
|
||||
try:
|
||||
ef_ad_dec = EF_AD().decode_bin(f_ad.body)
|
||||
except StreamError:
|
||||
return None
|
||||
if 'mnc_len' not in ef_ad_dec:
|
||||
return None
|
||||
return ef_ad_dec
|
||||
|
||||
@classmethod
|
||||
def apply_val(cls, pes: ProfileElementSequence, val: int):
|
||||
for pe in pes.get_pes_for_type('usim'):
|
||||
f_ad = cls._get_f_ad(pe)
|
||||
if f_ad is None:
|
||||
continue
|
||||
# decode existing values
|
||||
ef_ad_dec = cls._decode_f_ad(f_ad)
|
||||
if ef_ad_dec is None:
|
||||
continue
|
||||
# change mnc_len
|
||||
ef_ad_dec['mnc_len'] = val
|
||||
# re-encode into the File body
|
||||
f_ad.body = EF_AD().encode_bin(ef_ad_dec)
|
||||
pe.file2pe(f_ad)
|
||||
|
||||
@classmethod
|
||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
||||
for pe in pes.get_pes_for_type('usim'):
|
||||
f_ad = cls._get_f_ad(pe)
|
||||
if f_ad is None:
|
||||
continue
|
||||
ef_ad_dec = cls._decode_f_ad(f_ad)
|
||||
if ef_ad_dec is None:
|
||||
continue
|
||||
mnc_len = ef_ad_dec.get('mnc_len')
|
||||
yield { cls.name: str(mnc_len) }
|
||||
|
||||
|
||||
class SdKey(BinaryParam):
|
||||
"""Configurable Security Domain (SD) Key. Value is presented as bytes.
|
||||
Non-abstract implementations are generated in SdKey.generate_sd_key_classes"""
|
||||
@@ -1088,7 +1156,7 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
|
||||
|
||||
class MilenageXoringConstants(BinaryParam, AlgoConfig):
|
||||
"""XOR-ing constants c1,c2,c3,c4,c5 of Milenage, 128bit each. See 3GPP TS 35.206 Sections 2.3 + 5.3.
|
||||
Provided as octet-string concatenation of all 5 constants. The default value by 3GPP is the concetenation
|
||||
Provided as octet-string concatenation of all 5 constants. The default value by 3GPP is the concatenation
|
||||
of::
|
||||
|
||||
00000000000000000000000000000000
|
||||
|
||||
@@ -18,10 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import io
|
||||
import hashlib
|
||||
from copy import deepcopy
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
from construct import Optional as COptional
|
||||
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
|
||||
from construct import Construct, stream_read, stream_write
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.Cipher import DES, DES3, AES
|
||||
from osmocom.utils import *
|
||||
@@ -148,6 +150,24 @@ sw_table = {
|
||||
},
|
||||
}
|
||||
|
||||
class PutKeyLength(Construct):
|
||||
"""A length field of a PUT KEY data field, GP CardSpec v2.3.1 11.8.2.3.1
|
||||
- all lengths ASN.1 BER-TLV (ITU-T X.690 Section 8.1.3)
|
||||
- except that the length 128 may also be coded on one byte as '80' for backwards compatibility
|
||||
80 does not introduce the indefinite form here which is unused in GP as far as i know.
|
||||
That legacy form is accepted when parsing, but never generated, which agrees with the spec"""
|
||||
def _parse(self, stream, context, path):
|
||||
first = stream_read(stream, 1, path)[0]
|
||||
if first <= 0x80:
|
||||
return first
|
||||
return int.from_bytes(stream_read(stream, first & 0x7f, path), 'big')
|
||||
|
||||
def _build(self, obj, stream, context, path):
|
||||
data = bertlv_encode_len(obj)
|
||||
stream_write(stream, data, len(data), path)
|
||||
return obj
|
||||
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.1.6
|
||||
KeyType = Enum(Byte, des=0x80,
|
||||
tls_psk=0x85, # v2.3.1 Section 11.1.8
|
||||
@@ -602,8 +622,8 @@ class ADF_SD(CardADF):
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
|
||||
|
||||
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
|
||||
otherwise be automatically generated for DES and AES keys. You can suppress the latter using
|
||||
`--suppress-key-check`.
|
||||
otherwise be automatically generated for DES, AES and TLS-PSK keys. You can suppress the
|
||||
latter using `--suppress-key-check`.
|
||||
|
||||
Example (SCP80 KIC/KID/KIK):
|
||||
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
|
||||
@@ -620,33 +640,81 @@ class ADF_SD(CardADF):
|
||||
kdb = []
|
||||
for i in range(0, len(opts.key_type)):
|
||||
if opts.key_check and len(opts.key_check) > i:
|
||||
kcv = opts.key_check[i]
|
||||
kcv = h2b(opts.key_check[i])
|
||||
elif opts.suppress_key_check:
|
||||
kcv = ''
|
||||
kcv = b''
|
||||
else:
|
||||
kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
||||
kcv = b2h(kcv_bin)
|
||||
if self._cmd.lchan.scc.scp:
|
||||
# encrypted key data with DEK of current SCP
|
||||
kcb = b2h(self._cmd.lchan.scc.scp.encrypt_key(h2b(opts.key_data[i])))
|
||||
else:
|
||||
# (for example) during personalization, DEK might not be required)
|
||||
kcb = opts.key_data[i]
|
||||
kdb.append({'key_type': opts.key_type[i], 'kcb': kcb, 'kcv': kcv})
|
||||
kcv = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
||||
kdb.append({'key_type': opts.key_type[i], 'clear_key': h2b(opts.key_data[i]), 'kcv': kcv})
|
||||
p2 = opts.key_id
|
||||
if len(opts.key_type) > 1:
|
||||
p2 |= 0x80
|
||||
self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
|
||||
|
||||
# Table 11-68: Key Data Field - Format 1 (Basic Format)
|
||||
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
|
||||
'kcb'/Prefixed(Int8ub, GreedyBytes),
|
||||
'kcv'/Prefixed(Int8ub, GreedyBytes)))
|
||||
# Table 11-68: Key Data Field - Format 1 (Basic Format). The key component block length is
|
||||
# BER-TLV coded (Section 11.8.2.3.1), the key check value length is always '00' - '7F'.
|
||||
KeyDataBasic = Struct('key_type'/KeyType,
|
||||
'kcb'/Prefixed(PutKeyLength(), GreedyBytes),
|
||||
'kcv'/Prefixed(Int8ub, GreedyBytes))
|
||||
|
||||
def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
|
||||
@classmethod
|
||||
def encode_key_data_basic(cls, key_type: str, kcb: bytes, kcv: bytes) -> bytes:
|
||||
"""Generic Basic key data field, GP CardSpec v2.3 Table 11-68):
|
||||
tag || L1 || <maybe L2> KCB || <1-byte length> KCV"""
|
||||
return cls.KeyDataBasic.build({'key_type': key_type, 'kcb': kcb, 'kcv': kcv})
|
||||
|
||||
@classmethod
|
||||
def encode_key_data_psk(cls, clear_key: bytes, ciphered_key: bytes, kcv: bytes) -> bytes:
|
||||
"""Single PSK TLS '85' key data field per GP Amendment B 1.2, 3.9.1 / Table 3-13:
|
||||
85 | L1 | <L2> <ciphered PSK key> | <KCV length> | <KCV>
|
||||
- framing is like Basic Format, but the kcb is always GP CardSpec Table 11-70
|
||||
so always with the length of the clear text key value, even without padding!
|
||||
- 'ciphered_key' is DEK(block-padded clear key), no additional length prefix."""
|
||||
kcb = bertlv_encode_len(len(clear_key)) + ciphered_key
|
||||
return cls.encode_key_data_basic('tls_psk', kcb, kcv)
|
||||
|
||||
@classmethod
|
||||
def build_put_key_data(cls, kvn: int, keys: List[dict], scp) -> bytes:
|
||||
"""Assemble the PUT KEY data field, mixed PSK + DES DEK is supported:
|
||||
- new KVN followed by one key data field per key.
|
||||
- tls_psk keys per GP Amendment B
|
||||
- other key types generic Basic format
|
||||
Param 'keys' is a dict:
|
||||
- 'key_type' (str)
|
||||
- 'clear_key' (bytes)
|
||||
- 'kcv' (bytes / empty).
|
||||
'scp' may be None (e.g. during personalization, when the DEK may not be required)."""
|
||||
key_data = kvn.to_bytes(1, 'big')
|
||||
for k in keys:
|
||||
clear = k['clear_key']
|
||||
if k['key_type'] == 'tls_psk':
|
||||
# len always part of the data see CardSpec Table 11-70 vs Table 11-71
|
||||
if scp:
|
||||
ciphered = scp.dek_encrypt(scp.pad_to_blocksize(clear))
|
||||
else:
|
||||
ciphered = clear
|
||||
key_data += cls.encode_key_data_psk(clear, ciphered, k['kcv'])
|
||||
else:
|
||||
if scp:
|
||||
ciphered = scp.encrypt_key(clear)
|
||||
else:
|
||||
# (for example) during personalization, DEK might not be required
|
||||
ciphered = clear
|
||||
key_data += cls.encode_key_data_basic(k['key_type'], ciphered, k['kcv'])
|
||||
return key_data
|
||||
|
||||
def put_key(self, old_kvn:int, kvn: int, kid: int, keys: List[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)
|
||||
key_data = self.build_put_key_data(kvn, keys, self._cmd.lchan.scc.scp)
|
||||
# Lc of Table 11-64 is a single byte, while LOAD or STORE DATA splits we can't:
|
||||
# 11.8.2.3.3 splits a key at component boundaries -> not helping here
|
||||
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
|
||||
if len(key_data) > max_cmd_len:
|
||||
raise ValueError('key data field of %u bytes exceeds the maximum command length of %u '
|
||||
'(limited by the overhead of the current secure channel); use fewer '
|
||||
'keys per command, a single key component that large needs STORE DATA' %
|
||||
(len(key_data), max_cmd_len))
|
||||
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
|
||||
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data) + "00")
|
||||
return data
|
||||
@@ -826,23 +894,32 @@ class ADF_SD(CardADF):
|
||||
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')
|
||||
load_parser.add_argument('--chunk-len', type=auto_uint8, default=None,
|
||||
help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240')
|
||||
|
||||
@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))
|
||||
self.load(h2b(opts.from_hex), opts.chunk_len)
|
||||
elif opts.from_file is not None:
|
||||
self.load(opts.from_file.read())
|
||||
self.load(opts.from_file.read(), opts.chunk_len)
|
||||
elif opts.from_cap_file is not None:
|
||||
cap = CapFile(opts.from_cap_file)
|
||||
self.load(cap.get_loadfile())
|
||||
self.load(cap.get_loadfile(), opts.chunk_len)
|
||||
else:
|
||||
raise ValueError('load source not specified!')
|
||||
|
||||
def load(self, contents:bytes, chunk_len:int = 240):
|
||||
# TODO:tune chunk_len based on the overhead of the used SCP?
|
||||
def load(self, contents:bytes, chunk_len:Optional[int] = None):
|
||||
# scc.max_cmd_len knows the overhead the currently active SCP
|
||||
# 240 is the old default, keep it for now.
|
||||
max_chunk_len = self._cmd.lchan.scc.max_cmd_len
|
||||
if chunk_len is None:
|
||||
chunk_len = min(240, max_chunk_len)
|
||||
elif not 1 <= chunk_len <= max_chunk_len:
|
||||
raise ValueError('chunk_len must be in range 1..%u (limited by the overhead of the current secure channel)' %
|
||||
max_chunk_len)
|
||||
# build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case
|
||||
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
|
||||
# transfer this in various chunks to the card
|
||||
@@ -881,6 +958,8 @@ class ADF_SD(CardADF):
|
||||
install_cap_parser_inst_prm_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)')
|
||||
install_cap_parser.add_argument('--chunk-len', type=auto_uint8, default=None,
|
||||
help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240')
|
||||
|
||||
@cmd2.with_argparser(install_cap_parser)
|
||||
def do_install_cap(self, opts):
|
||||
@@ -919,7 +998,7 @@ class ADF_SD(CardADF):
|
||||
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.load(load_file, opts.chunk_len)
|
||||
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))
|
||||
@@ -1065,10 +1144,16 @@ def compute_kcv_aes(key:bytes) -> bytes:
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
def compute_kcv_psk(key:bytes) -> bytes:
|
||||
# GP Amendment B v1.2, 3.9.1 / Table 3-13
|
||||
# KCV of a PSK TLS key is the 3 highest-order bytes of the SHA-1 digest of the clear key value.
|
||||
return hashlib.sha1(key).digest()
|
||||
|
||||
# dict is keyed by the string name of the KeyType enum above in this file
|
||||
KCV_CALCULATOR = {
|
||||
'aes': compute_kcv_aes,
|
||||
'des': compute_kcv_des,
|
||||
'tls_psk': compute_kcv_psk,
|
||||
}
|
||||
|
||||
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
|
||||
|
||||
@@ -182,6 +182,29 @@ class SCP(SecureChannel, abc.ABC):
|
||||
"""Should we perform R-ENC?"""
|
||||
return self.security_level & 0x20
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def mac_len(self) -> int:
|
||||
"""Length of the appended C-MAC, to be provided by derived class."""
|
||||
|
||||
@property
|
||||
def overhead(self) -> int:
|
||||
"""Worst-case len that wrapping a command APDU adds to its data field at the
|
||||
current sec level is (255 - overhead), C-MAC + C-DECRYPTION encryption padding."""
|
||||
if not self.do_cmac:
|
||||
return 0
|
||||
if not self.do_cenc:
|
||||
return self.mac_len
|
||||
# see Secure Channel Protocol '03' Card Specification v2.3 - Amendment D v1.1.2
|
||||
# which defers to GPCS v2.3 Section B.2 which then defers to
|
||||
# NIST SP 800-38B for encryption and points out that
|
||||
# the padding is, as expected, just the usual padding from NIST SP 800-38A
|
||||
# C-DECRYPTION pads with ('80'+['00'...] at least 1 byte) up to
|
||||
# the cipher block size + C-MAC on top -> largest usable data field
|
||||
# is one byte less than the largest block-size multiple within 255 - mac_len.
|
||||
bs = self.sk.blocksize
|
||||
return 255 - ((255 - self.mac_len) // bs * bs - 1)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
|
||||
|
||||
@@ -215,11 +238,20 @@ class SCP(SecureChannel, abc.ABC):
|
||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
||||
pass
|
||||
|
||||
def pad_to_blocksize(self, data: bytes) -> bytes:
|
||||
"""Right pad the data with zero bytes to a multiple of the DEK cipher block size."""
|
||||
if len(data) % self.sk.blocksize:
|
||||
# not '+=' which would mutate the callers bytearray in place..
|
||||
data = data + b'\x00' * (self.sk.blocksize - len(data) % self.sk.blocksize)
|
||||
return data
|
||||
|
||||
def encrypt_key(self, key: bytes) -> bytes:
|
||||
"""Encrypt a key with the DEK."""
|
||||
num_pad = len(key) % self.sk.blocksize
|
||||
if num_pad:
|
||||
return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad)
|
||||
if len(key) % self.sk.blocksize:
|
||||
# The kcv is right padded before encryption and the kcb
|
||||
# is formatted as described in Table 11-70: preceded by the actual length of the
|
||||
# clear text kcv.
|
||||
return bertlv_encode_len(len(key)) + self.dek_encrypt(self.pad_to_blocksize(key))
|
||||
return self.dek_encrypt(key)
|
||||
|
||||
def decrypt_key(self, encrypted_key:bytes) -> bytes:
|
||||
@@ -232,9 +264,8 @@ class SCP(SecureChannel, abc.ABC):
|
||||
# Block provides the actual length of the key component value, which allows recovering the
|
||||
# clear-text key component value after decryption of the encrypted key component value and removal
|
||||
# of padding bytes.
|
||||
decrypted = self.dek_decrypt(encrypted_key)
|
||||
key_len, remainder = bertlv_parse_len(decrypted)
|
||||
return remainder[:key_len]
|
||||
key_len, remainder = bertlv_parse_len(encrypted_key)
|
||||
return self.dek_decrypt(remainder)[:key_len]
|
||||
else:
|
||||
# If the length of the Key Component Block is a multiple of the block size of the encryption
|
||||
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding
|
||||
@@ -260,10 +291,8 @@ class SCP02(SCP):
|
||||
# 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]]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.overhead = 8
|
||||
super().__init__(*args, **kwargs)
|
||||
# C-MAC (Single DES + final 3DES, B.1.2.2) is always one full DES block
|
||||
mac_len = 8
|
||||
|
||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
||||
@@ -338,10 +367,16 @@ class SCP02(SCP):
|
||||
# CMAC on modified APDU
|
||||
mlc = lc + 8
|
||||
clac = cla | CLA_SM
|
||||
if mlc >= 256:
|
||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending 8 bytes of mac' % mlc)
|
||||
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data)
|
||||
if self.do_cenc:
|
||||
padded_data = pad80(data, 8)
|
||||
if len(padded_data) + 8 >= 256:
|
||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending padding and mac' %
|
||||
(len(padded_data) + 8))
|
||||
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
|
||||
data = k.encrypt(pad80(data, 8))
|
||||
data = k.encrypt(padded_data)
|
||||
lc = len(data)
|
||||
|
||||
lc += 8
|
||||
@@ -477,9 +512,13 @@ class SCP03(SCP):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.s_mode = kwargs.pop('s_mode', 8)
|
||||
self.overhead = self.s_mode
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def mac_len(self) -> int:
|
||||
# C-MAC truncated to 8 in S8 or 16 bytes in S16 mode
|
||||
return self.s_mode
|
||||
|
||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
+10
-2
@@ -24,7 +24,15 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
from cmd2 import style
|
||||
import cmd2
|
||||
from packaging import version
|
||||
|
||||
if version.parse(cmd2.__version__) >= version.parse("3.0.0"):
|
||||
from cmd2 import stylize as _stylize # pylint: disable=no-name-in-module
|
||||
def _style(text, fg=None): # pylint: disable=function-redefined
|
||||
return _stylize(text, fg) if fg else text
|
||||
else: # cmd2>=2.6.2
|
||||
from cmd2 import style as _style # pylint: disable=no-name-in-module
|
||||
|
||||
class _PySimLogHandler(logging.Handler):
|
||||
def __init__(self, log_callback):
|
||||
@@ -121,7 +129,7 @@ class PySimLogger:
|
||||
if isinstance(color, str):
|
||||
PySimLogger.print_callback(color + formatted_message + "\033[0m")
|
||||
else:
|
||||
PySimLogger.print_callback(style(formatted_message, fg = color))
|
||||
PySimLogger.print_callback(_style(formatted_message, fg = color))
|
||||
else:
|
||||
PySimLogger.print_callback(formatted_message)
|
||||
|
||||
|
||||
+36
-11
@@ -17,6 +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 bidict import bidict
|
||||
import copy
|
||||
|
||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
|
||||
from construct import Optional as COptional, Computed
|
||||
@@ -335,6 +336,8 @@ class TerminalCapability(BER_TLV_IE, tag=0xa9, nested=[TerminalPowerSupply, Exte
|
||||
|
||||
# ETSI TS 102 221 Section 9.2.7 + ISO7816-4 9.3.3/9.3.4
|
||||
class _AM_DO_DF(DataObject):
|
||||
"""ISO7816-4:2005 5.4.3.1 Table 16"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
||||
|
||||
@@ -381,7 +384,7 @@ class _AM_DO_DF(DataObject):
|
||||
|
||||
|
||||
class _AM_DO_EF(DataObject):
|
||||
"""ISO7816-4 9.3.2 Table 18 + 9.3.3.1 Table 31"""
|
||||
"""ISO7816-4:2005 5.4.3.1 Table 17"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
||||
@@ -429,7 +432,7 @@ class _AM_DO_EF(DataObject):
|
||||
|
||||
|
||||
class _AM_DO_CHDR(DataObject):
|
||||
"""Command Header Access Mode DO according to ISO 7816-4 Table 32."""
|
||||
"""Command Header Access Mode DO according to ISO 7816-4:2005 5.4.3.2 Table 22."""
|
||||
|
||||
def __init__(self, tag):
|
||||
super().__init__('command_header', 'Command Header Description', tag=tag)
|
||||
@@ -543,8 +546,9 @@ class CRT_DO(DataObject):
|
||||
pin = pin_names.inverse[self.decoded]
|
||||
return b'\x83\x01' + pin.to_bytes(1, 'big') + b'\x95\x01\x08'
|
||||
|
||||
# ISO7816-4 9.3.3 Table 33
|
||||
class SecCondByte_DO(DataObject):
|
||||
"""ISO7816-4:2005 5.4.3.1 Table 20"""
|
||||
|
||||
def __init__(self, tag=0x9d):
|
||||
super().__init__('security_condition_byte', tag=tag)
|
||||
|
||||
@@ -732,36 +736,57 @@ class EF_ARR(LinFixedEF):
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
@staticmethod
|
||||
def __get_do_sequence(decode_for_df : bool = False):
|
||||
if decode_for_df:
|
||||
return DataObjectSequence('arr', sequence=[AM_DO_DF, SC_DO])
|
||||
else:
|
||||
return DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
|
||||
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])
|
||||
# we can only guess if we should decode for EF or DF here, but our caller may
|
||||
# be able to pass us a hint:
|
||||
arr_seq = self.__get_do_sequence(kwargs.get('decode_for_df', False))
|
||||
dec = arr_seq.decode_multi(raw_bin_data)
|
||||
# we cannot pass the result through flatten() here, as we don't have a related
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
# we can only guess if we should decode for EF or DF here, but our caller may
|
||||
# be able to pass us a hint:
|
||||
arr_seq = self.__get_do_sequence(kwargs.get('encode_for_df', False))
|
||||
return arr_seq.encode_multi(in_json)
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
read_arr_argparser = copy.deepcopy(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
read_arr_argparser.add_argument('--decode-for-df', action='store_true',
|
||||
help='Decode EF.ARR record as if used by a DF (default: EF)')
|
||||
|
||||
@cmd2.with_argparser(read_arr_argparser)
|
||||
def do_read_arr_record(self, opts):
|
||||
"""Read one EF.ARR record in flattened, human-friendly form."""
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
||||
(hexdata, _sw) = self._cmd.lchan.read_record(opts.RECORD_NR)
|
||||
data = self._cmd.lchan.selected_file._decode_record_bin(h2b(hexdata),
|
||||
decode_for_df = opts.decode_for_df)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
read_arrs_argparser = copy.deepcopy(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
read_arrs_argparser.add_argument('--decode-for-df', action='store_true',
|
||||
help='Decode EF.ARR records as if used by a DF (default: EF)')
|
||||
|
||||
@cmd2.with_argparser(read_arrs_argparser)
|
||||
def do_read_arr_records(self, opts):
|
||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
# collect all results in list so they are rendered as JSON list when printing
|
||||
data_list = []
|
||||
for recnr in range(1, 1 + num_of_rec):
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
|
||||
(hexdata, _sw) = self._cmd.lchan.read_record(recnr)
|
||||
data = self._cmd.lchan.selected_file._decode_record_bin(h2b(hexdata),
|
||||
decode_for_df = opts.decode_for_df)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
+10
-2
@@ -285,6 +285,14 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
{"hnet_pubkey_identifier": 11, "hnet_pubkey":
|
||||
h2b("d1bc365f4997d17ce4374e72181431cbfeba9e1b98d7618f79d48561b144672a")}]} ),
|
||||
]
|
||||
_test_decode = [
|
||||
( 'A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
|
||||
{"prot_scheme_id_list": [],
|
||||
"hnet_pubkey_list": []} ),
|
||||
( 'A000A100FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
|
||||
{"prot_scheme_id_list": [],
|
||||
"hnet_pubkey_list": []} ),
|
||||
]
|
||||
# 3GPP TS 31.102 Section 4.4.11.8
|
||||
class ProtSchemeIdList(BER_TLV_IE, tag=0xa0):
|
||||
# FIXME: 3GPP TS 24.501 Protection Scheme Identifier
|
||||
@@ -327,7 +335,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
"""conversion method to generate list of {hnet_pubkey_identifier, hnet_pubkey} dicts
|
||||
from flat [{hnet_pubkey_identifier: }, {net_pubkey: }, ...] list"""
|
||||
out = []
|
||||
while len(l):
|
||||
while l:
|
||||
a = l.pop(0)
|
||||
b = l.pop(0)
|
||||
z = {**a, **b}
|
||||
@@ -389,7 +397,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
# remaining data holds Home Network Public Key Data Object
|
||||
hpkl = EF_SUCI_Calc_Info.HnetPubkeyList()
|
||||
hpkl.from_tlv(in_bytes[pos:])
|
||||
hnet_pubkey_list = self._compact_pubkey_list(hpkl.to_dict()['hnet_pubkey_list'])
|
||||
hnet_pubkey_list = self._compact_pubkey_list(hpkl.to_dict()['hnet_pubkey_list'] or [])
|
||||
|
||||
return {
|
||||
'prot_scheme_id_list': prot_scheme_id_list,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
pyscard
|
||||
pyserial
|
||||
pytlv
|
||||
cmd2>=2.6.2,<3.0
|
||||
cmd2>=2.6.2,<4.0
|
||||
jsonpath-ng
|
||||
construct>=2.10.70
|
||||
bidict
|
||||
|
||||
@@ -21,7 +21,7 @@ setup(
|
||||
"pyscard",
|
||||
"pyserial",
|
||||
"pytlv",
|
||||
"cmd2 >= 1.5.0, < 3.0",
|
||||
"cmd2 >= 2.6.2, < 4.0",
|
||||
"jsonpath-ng",
|
||||
"construct >= 2.10.70",
|
||||
"bidict",
|
||||
|
||||
@@ -55,8 +55,6 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
upp_fnames = (
|
||||
'TS48v5_SAIP2.1A_NoBERTLV.der',
|
||||
'TS48v5_SAIP2.3_BERTLV_SUCI.der',
|
||||
'TS48v5_SAIP2.1B_NoBERTLV.der',
|
||||
'TS48v5_SAIP2.3_NoBERTLV.der',
|
||||
)
|
||||
|
||||
class Paramtest:
|
||||
@@ -267,6 +265,15 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
'11111111111111111111111111111111'
|
||||
'22222222222222222222222222222222'),
|
||||
|
||||
Paramtest(param_cls=p13n.MncLen,
|
||||
val='2',
|
||||
expect_clean_val=2,
|
||||
expect_val='2'),
|
||||
Paramtest(param_cls=p13n.MncLen,
|
||||
val=3,
|
||||
expect_clean_val=3,
|
||||
expect_val='3'),
|
||||
|
||||
]
|
||||
|
||||
for sdkey_cls in (
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
|
||||
import unittest
|
||||
import logging
|
||||
import hashlib
|
||||
from types import SimpleNamespace
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.tlv import bertlv_encode_len
|
||||
|
||||
from pySim.global_platform import *
|
||||
from pySim.global_platform.scp import *
|
||||
@@ -283,6 +286,41 @@ class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
|
||||
# FIXME: test auth with random (0x60) vs pseudo-random (0x70) challenge
|
||||
|
||||
|
||||
class KeyComponentBlock_Test(unittest.TestCase):
|
||||
"""Tests for the kcb of GP CardSpec v2.3
|
||||
- Table 11-70 kcv that required padding, preceded by its clear-text length
|
||||
- Table 11-71 no padding required"""
|
||||
|
||||
def setUp(self):
|
||||
# SCP02 (3DES DEK, 8 byte blocks), same vectors as SCP02_Test
|
||||
self.scp02 = SCP02(card_keys=ck_3des_70)
|
||||
self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
self.scp02.gen_ext_auth_apdu()
|
||||
# SCP03 (AES DEK, 16 byte blocks), same vectors as SCP03_Test_AES128_11
|
||||
self.scp03 = SCP03(card_keys=KEYSET_AES128)
|
||||
self.scp03.gen_init_update_apdu(h2b('b13e5f938fc108c4'))
|
||||
self.scp03.parse_init_update_resp(h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002'))
|
||||
self.scp03.gen_ext_auth_apdu(0x11)
|
||||
|
||||
def test_encrypt_decrypt_key(self):
|
||||
for scp in (self.scp02, self.scp03):
|
||||
bs = scp.sk.blocksize
|
||||
for keylen in range(1, 3 * bs + 1):
|
||||
with self.subTest(scp=type(scp).__name__, keylen=keylen):
|
||||
key = bytes(range(keylen))
|
||||
kcb = scp.encrypt_key(key)
|
||||
if keylen % bs:
|
||||
# Table 11-70: <length of clear key component> || <encrypted padded value>
|
||||
self.assertEqual(kcb[0], keylen)
|
||||
self.assertEqual((len(kcb) - 1) % bs, 0)
|
||||
self.assertEqual(len(kcb) - 1, keylen + (bs - keylen % bs))
|
||||
else:
|
||||
# Table 11-71: only the encrypted key component value
|
||||
self.assertEqual(len(kcb), keylen)
|
||||
self.assertEqual(scp.decrypt_key(kcb), key)
|
||||
|
||||
|
||||
class SCP03_KCV_Test(unittest.TestCase):
|
||||
def test_kcv(self):
|
||||
self.assertEqual(compute_kcv('aes', KEYSET_AES128.enc), h2b('C35280'))
|
||||
@@ -290,6 +328,208 @@ class SCP03_KCV_Test(unittest.TestCase):
|
||||
self.assertEqual(compute_kcv('aes', KEYSET_AES128.dek), h2b('840DE5'))
|
||||
|
||||
|
||||
class PutKey_PSK_Test(unittest.TestCase):
|
||||
"""Tests for the PUT KEY command data field encoding, in particular the PSK TLS ('85') key data
|
||||
field defined by GlobalPlatform Amendment B (Remote Application Management over HTTP) Table 3-13."""
|
||||
|
||||
# the PUT KEY encoder we exercise
|
||||
C = ADF_SD.AddlShellCommands
|
||||
|
||||
# SCP80 TLS-PSK example key from the do_put_key docstring (16 bytes)
|
||||
PSK_CLEAR = h2b('303132333435363738393a3b3c3d3e3f')
|
||||
# its DEK ciphertext + Table 3-13 KCV with SCP02 session set up below
|
||||
PSK_CIPHERED = h2b('15abf1fe16ccc5aa13743394442942cd')
|
||||
PSK_KCV = h2b('06125d') # = SHA-1(PSK_CLEAR)[:3]
|
||||
|
||||
def setUp(self):
|
||||
# SCP02 with the same vectors as SCP02_Test, so that the whole PUT KEY data field is reproducible.
|
||||
self.scp02 = SCP02(card_keys=ck_3des_70)
|
||||
self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
self.scp02.gen_ext_auth_apdu()
|
||||
|
||||
def test_psk_kcv_is_sha1(self):
|
||||
# GP Amendment B Table 3-13: KCV = 3 most significant bytes of SHA-1(clear key)
|
||||
self.assertEqual(compute_kcv('tls_psk', self.PSK_CLEAR), hashlib.sha1(self.PSK_CLEAR).digest()[:3])
|
||||
self.assertEqual(compute_kcv('tls_psk', self.PSK_CLEAR), self.PSK_KCV)
|
||||
|
||||
def test_encode_psk_framing_golden(self):
|
||||
# assert the exact Table 3-13 layout
|
||||
# 85 | L1 | L2 | <ciphered> | 03 | <SHA-1(clear)[:3]>
|
||||
clear = self.PSK_CLEAR
|
||||
ciphered = h2b('aabbccddeeff00112233445566778899') # arbitrary 16-byte ciphertext
|
||||
kcv = hashlib.sha1(clear).digest()[:3]
|
||||
field = self.C.encode_key_data_psk(clear, ciphered, kcv)
|
||||
# 85 L1 L2 <---------- ciphered -----------> 03 <-kcv->
|
||||
self.assertEqual(b2h(field),'85' '11' '10' 'aabbccddeeff00112233445566778899' '03' + b2h(kcv))
|
||||
self.assertEqual(b2h(field),'851110aabbccddeeff0011223344556677889903' + '06125d')
|
||||
|
||||
def test_psk_golden_over_scp02(self):
|
||||
# Full PUT KEY data field (KVN 0x40 + single PSK key) enciphered with the SCP02 DEK.
|
||||
keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
|
||||
'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)}]
|
||||
data = self.C.build_put_key_data(0x40, keys, self.scp02)
|
||||
self.assertEqual(b2h(data),
|
||||
'40' '85' '11' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV))
|
||||
|
||||
def test_wrong_basic_format_differs(self):
|
||||
# regression test, the generic "Basic format" does NOT match Table 3-13 for a PSK key
|
||||
# rejected by card with with 6a88
|
||||
wrong_basic = self.C.encode_key_data_basic('tls_psk', self.PSK_CIPHERED, b'')
|
||||
right_psk = self.C.encode_key_data_psk(self.PSK_CLEAR, self.PSK_CIPHERED, self.PSK_KCV)
|
||||
self.assertEqual(b2h(wrong_basic), '8510' + b2h(self.PSK_CIPHERED) + '00')
|
||||
self.assertEqual(b2h(right_psk), '8511' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV))
|
||||
self.assertNotEqual(wrong_basic, right_psk)
|
||||
|
||||
def test_key_component_block_length_is_bertlv(self):
|
||||
# GP CardSpec v2.3.1 Section 11.8.2.3.1: all lengths ofPUT KEY are always BER TLV coded
|
||||
for kcb_len, exp_len_field in [(127, '7f'), (128, '8180'), (129, '8181'), (256, '820100')]:
|
||||
with self.subTest(kcb_len=kcb_len):
|
||||
kcb = bytes(kcb_len)
|
||||
field = self.C.encode_key_data_basic('rsa_modulus_n', kcb, b'')
|
||||
self.assertEqual(b2h(field), 'a2' + exp_len_field + b2h(kcb) + '00')
|
||||
# 85 field of Amendment B Table 3-13 uses the same coding
|
||||
# single byte inner length (clear key < 128) == block kcb_len bytes long
|
||||
psk = self.C.encode_key_data_psk(bytes(120), bytes(kcb_len - 1), b'')
|
||||
self.assertEqual(b2h(psk)[:2 + len(exp_len_field)], '85' + exp_len_field)
|
||||
|
||||
def test_basic_format_unchanged(self):
|
||||
# as before
|
||||
for kt, clear in [('des', h2b('404142434445464748494a4b4c4d4e4f')),
|
||||
('aes', h2b('000102030405060708090a0b0c0d0e0f'))]:
|
||||
ciph = self.scp02.encrypt_key(clear)
|
||||
kcv = compute_kcv(kt, clear)
|
||||
via_construct = build_construct(self.C.KeyDataBasic, {'key_type': kt, 'kcb': b2h(ciph), 'kcv': b2h(kcv)})
|
||||
via_helper = self.C.encode_key_data_basic(kt, ciph, kcv)
|
||||
self.assertEqual(via_helper, via_construct)
|
||||
|
||||
def test_psk_padding_no_double_length(self):
|
||||
# A PSK key whose length is not a multiple of the DEK block size (DES: 8) is right-padded before
|
||||
# ciphering. Table 3-13 states the clear key length (L2) in the '85' DO itself, so the ciphered
|
||||
# key field is the bare cryptogram:
|
||||
# - ciphered field == padded ciphertext (no duplicated length prefix),
|
||||
# - clear key == first L2 bytes.
|
||||
for keylen in (18, 20):
|
||||
with self.subTest(keylen=keylen):
|
||||
clear = bytes(range(keylen))
|
||||
padded_len = keylen + (-keylen % 8)
|
||||
field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear,
|
||||
'kcv': compute_kcv('tls_psk', clear)}], self.scp02)[1:]
|
||||
self.assertEqual(field[0], 0x85)
|
||||
l1 = field[1]
|
||||
l2 = field[2]
|
||||
self.assertEqual(l2, keylen) # single-byte BER length of clear key
|
||||
ciphered = field[3:3 + (l1 - 1)] # value = L2 (1 byte) || ciphered key
|
||||
self.assertEqual(len(ciphered), padded_len) # padded to the 8-byte DES block size
|
||||
self.assertEqual(l1, 1 + padded_len) # no duplicated length prefix
|
||||
self.assertEqual(self.scp02.dek_decrypt(ciphered)[:keylen], clear)
|
||||
|
||||
def test_psk_clear_key_is_not_padded_in_place(self):
|
||||
# padding the bytearray in place would make L2 the padded length,
|
||||
# then stored as key material and rejected thanks to the KCV
|
||||
clear = h2b('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d') # 30, not %8
|
||||
kcv = compute_kcv('tls_psk', clear)
|
||||
field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear,
|
||||
'kcv': kcv}], self.scp02)[1:]
|
||||
self.assertEqual(len(clear), 30)
|
||||
self.assertEqual(field[2], 30) # L2 == clear key length, not 32
|
||||
self.assertEqual(self.scp02.dek_decrypt(field[3:3 + field[1] - 1])[:30], clear)
|
||||
|
||||
def test_kcv_suppressed(self):
|
||||
# --suppress-key-check -> KCV length 00 and no KCV bytes
|
||||
field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
|
||||
'kcv': b''}], self.scp02)[1:]
|
||||
self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CIPHERED) + '00')
|
||||
|
||||
def test_multikey_psk_plus_des_dek(self):
|
||||
# load a PSK TLS key (KID 1, Amendment B format) together with its DES DEK
|
||||
# (KID 2, Basic format) in one PUT KEY.
|
||||
# Verify the concatenated data field parses back into the two components with proper type formats.
|
||||
dek = h2b('404142434445464748494a4b4c4d4e4f')
|
||||
keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)},
|
||||
{'key_type': 'des', 'clear_key': dek, 'kcv': compute_kcv('des', dek)}]
|
||||
data = self.C.build_put_key_data(0x40, keys, self.scp02)
|
||||
|
||||
b = data
|
||||
self.assertEqual(b[0], 0x40) # KVN
|
||||
b = b[1:]
|
||||
# component 1: PSK TLS (Table 3-13)
|
||||
self.assertEqual(b[0], 0x85)
|
||||
self.assertEqual(b[1], 0x11) # L1 = 17
|
||||
self.assertEqual(b[2], 0x10) # L2 = 16 (clear key length)
|
||||
self.assertEqual(b[3:3 + 16], self.PSK_CIPHERED)
|
||||
self.assertEqual(b[3 + 16], 0x03) # KCV length
|
||||
self.assertEqual(b[3 + 16 + 1:3 + 16 + 1 + 3], self.PSK_KCV)
|
||||
b = b[3 + 16 + 1 + 3:]
|
||||
# component 2: DES DEK (Basic format)
|
||||
self.assertEqual(b[0], 0x80) # key type des
|
||||
kcb_len = b[1]
|
||||
self.assertEqual(kcb_len, 16)
|
||||
self.assertEqual(b[2:2 + kcb_len], self.scp02.encrypt_key(dek))
|
||||
b = b[2 + kcb_len:]
|
||||
self.assertEqual(b[0], 0x03) # KCV length
|
||||
self.assertEqual(b[1:1 + 3], compute_kcv('des', dek))
|
||||
self.assertEqual(b[1 + 3:], b'') # no trailing bytes
|
||||
|
||||
def test_no_scp_leaves_key_clear(self):
|
||||
# During personalization (no SCP) the key is not enciphered, framing still follows Table 3-13.
|
||||
field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
|
||||
'kcv': self.PSK_KCV}], None)[1:]
|
||||
self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CLEAR) + '03' + b2h(self.PSK_KCV))
|
||||
|
||||
|
||||
class PutKey_Length_Test(unittest.TestCase):
|
||||
"""Tests for the length of the PUT KEY command APDU. Lc of GP CardSpec v2.3 Table 11-64 is a
|
||||
single byte, so an oversized key data field cannot be sent."""
|
||||
|
||||
class PutKeyOnly(ADF_SD.AddlShellCommands):
|
||||
"""ADF_SD.AddlShellCommands with a canned scc to drive put_key()"""
|
||||
def __init__(self, scp=None, max_cmd_len=255):
|
||||
super().__init__()
|
||||
self.sent = []
|
||||
self.scc = SimpleNamespace(scp=scp, max_cmd_len=max_cmd_len,
|
||||
send_apdu_checksw=lambda pdu: (self.sent.append(pdu), ('', '9000'))[1])
|
||||
|
||||
@property
|
||||
def _cmd(self):
|
||||
return SimpleNamespace(lchan=SimpleNamespace(scc=self.scc))
|
||||
|
||||
# KVN, key type, two byte BER length of the key component block, KCV length; KCV suppressed
|
||||
FRAMING = 1 + 1 + 2 + 1
|
||||
|
||||
@staticmethod
|
||||
def key(nbytes: int):
|
||||
return [{'key_type': 'rsa_modulus_n', 'clear_key': bytes(nbytes), 'kcv': b''}]
|
||||
|
||||
def test_lc_matches_data_field(self):
|
||||
# largest key component block that still fits without a secure channel
|
||||
sd = self.PutKeyOnly()
|
||||
sd.put_key(0, 0x40, 1, self.key(255 - self.FRAMING))
|
||||
apdu = sd.sent[0]
|
||||
self.assertEqual(apdu[:8], '80D80001')
|
||||
lc = int(apdu[8:10], 16)
|
||||
self.assertEqual(lc, 255) # Lc ...
|
||||
self.assertEqual(len(apdu[10:-2]) // 2, lc) # ... and it matches the actual data field
|
||||
|
||||
def test_oversized_key_data_raises(self):
|
||||
# real world fat example: RSA-2048 modulus does not fit, led to 3 nibble Lc 106,
|
||||
# which silently shifted and broke the whole APDU by half a byte.
|
||||
sd = self.PutKeyOnly()
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
sd.put_key(0, 0x40, 1, self.key(256))
|
||||
self.assertIn('262', str(ctx.exception))
|
||||
self.assertIn('255', str(ctx.exception))
|
||||
self.assertEqual(sd.sent, []) # nothing was sent to the card
|
||||
|
||||
def test_secure_channel_overhead_lowers_the_limit(self):
|
||||
# scc.max_cmd_len shrinks by the C-MAC + encryption padding of active SCP
|
||||
sd = self.PutKeyOnly(max_cmd_len=239)
|
||||
sd.put_key(0, 0x40, 1, self.key(239 - self.FRAMING))
|
||||
self.assertEqual(int(sd.sent[0][8:10], 16), 239)
|
||||
with self.assertRaises(ValueError):
|
||||
sd.put_key(0, 0x40, 1, self.key(239 - self.FRAMING + 1))
|
||||
|
||||
|
||||
class Install_param_Test(unittest.TestCase):
|
||||
def test_gen_install_parameters(self):
|
||||
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
|
||||
@@ -298,5 +538,180 @@ class Install_param_Test(unittest.TestCase):
|
||||
load_parameters = gen_install_parameters()
|
||||
self.assertEqual(load_parameters, 'c900')
|
||||
|
||||
class SCP_Overhead_Test(unittest.TestCase):
|
||||
"""SCP.overhead varies according to the current security level:
|
||||
C-MAC + at level >= 3 the worst-case padding!
|
||||
"""
|
||||
|
||||
def _scp02(self, security_level):
|
||||
scp = SCP02(card_keys=ck_3des_70)
|
||||
scp.sk = Scp02SessionKeys(0x0001, ck_3des_70)
|
||||
scp.security_level = security_level
|
||||
return scp
|
||||
|
||||
def _scp03(self, security_level, s_mode=8):
|
||||
scp = SCP03(card_keys=KEYSET_AES128, s_mode=s_mode)
|
||||
scp.sk = Scp03SessionKeys(KEYSET_AES128, b'\x00' * s_mode, b'\x11' * s_mode)
|
||||
scp.security_level = security_level
|
||||
return scp
|
||||
|
||||
def test_scp02(self):
|
||||
self.assertEqual(self._scp02(0x00).overhead, 0) # no wrapping at all
|
||||
self.assertEqual(self._scp02(0x01).overhead, 8) # C-MAC
|
||||
self.assertEqual(self._scp02(0x03).overhead, 16) # C-MAC + C-DEC: pad80 to 8, largest fit 239
|
||||
|
||||
def test_scp03_s8(self):
|
||||
self.assertEqual(self._scp03(0x00).overhead, 0)
|
||||
self.assertEqual(self._scp03(0x01).overhead, 8)
|
||||
self.assertEqual(self._scp03(0x03).overhead, 16) # pad80 to 16 within 247 -> 240, minus pad byte
|
||||
self.assertEqual(self._scp03(0x33).overhead, 16) # R-MAC/R-ENC add no *command* overhead
|
||||
|
||||
def test_scp03_s16(self):
|
||||
self.assertEqual(self._scp03(0x01, s_mode=16).overhead, 16)
|
||||
self.assertEqual(self._scp03(0x03, s_mode=16).overhead, 32) # pad80 to 16 within 239 -> 224, minus pad byte
|
||||
|
||||
|
||||
class SCP_Lc_Limit_Test_Base(unittest.TestCase):
|
||||
"""Test wrap_cmd_apdu() boundary handling: data of (255 - overhead) must produce Lc <= 255 else ValueError"""
|
||||
|
||||
def _load_apdu(self, data_len):
|
||||
return h2b('80E80000') + bytes([data_len]) + b'\xa5' * data_len
|
||||
|
||||
def _check_boundary(self, scp):
|
||||
fits = 255 - scp.overhead
|
||||
wrapped = scp.wrap_cmd_apdu(self._load_apdu(fits))
|
||||
self.assertLessEqual(wrapped[4], 255)
|
||||
self.assertEqual(len(wrapped), 5 + wrapped[4]) # case #3: header + Lc bytes, no Le
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
scp.wrap_cmd_apdu(self._load_apdu(fits + 1))
|
||||
self.assertIn('Lc', str(ctx.exception))
|
||||
|
||||
|
||||
class SCP02_Lc_Limit_Test(SCP_Lc_Limit_Test_Base):
|
||||
"""Same session vectors as SCP02_Auth_Test"""
|
||||
|
||||
def setUp(self):
|
||||
self.scp02 = SCP02(card_keys=ck_3des_70)
|
||||
self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
self.scp02.gen_ext_auth_apdu()
|
||||
|
||||
def test_cmac_only(self):
|
||||
self.scp02.security_level = 0x01
|
||||
self._check_boundary(self.scp02) # 247 fits, 248 raises
|
||||
|
||||
def test_cmac_cdec(self):
|
||||
self.scp02.security_level = 0x03
|
||||
self._check_boundary(self.scp02) # 239 fits (-> Lc 248), 240 raises (would be 256)
|
||||
|
||||
def test_cmac_cdec_wrapped_lc(self):
|
||||
# my actual failing case: 240 bytes at level 3
|
||||
self.scp02.security_level = 0x03
|
||||
wrapped = self.scp02.wrap_cmd_apdu(self._load_apdu(239))
|
||||
self.assertEqual(wrapped[4], 248) # 239 -> pad80 -> 240 ciphertext + 8 mac
|
||||
|
||||
|
||||
class SCP03_Lc_Limit_Test(SCP_Lc_Limit_Test_Base):
|
||||
"""Session keys derived directly"""
|
||||
|
||||
def _scp03(self, security_level, s_mode):
|
||||
scp = SCP03(card_keys=KEYSET_AES128, s_mode=s_mode)
|
||||
scp.sk = Scp03SessionKeys(KEYSET_AES128, b'\x00' * s_mode, b'\x11' * s_mode)
|
||||
scp.security_level = security_level
|
||||
return scp
|
||||
|
||||
def test_s8_cmac_only(self):
|
||||
self._check_boundary(self._scp03(0x01, 8)) # 247 fits, 248 raises
|
||||
|
||||
def test_s8_cmac_cdec(self):
|
||||
self._check_boundary(self._scp03(0x03, 8)) # 239 fits, 240 raises
|
||||
|
||||
def test_s16_cmac_only(self):
|
||||
self._check_boundary(self._scp03(0x01, 16)) # 239 fits, 240 raises
|
||||
|
||||
def test_s16_cmac_cdec(self):
|
||||
self._check_boundary(self._scp03(0x03, 16)) # 223 fits, 224 raises
|
||||
|
||||
|
||||
class _FakeSccForLoad:
|
||||
"""mock lchan.scc: records LOAD APDUs, optionally wrapping them through a real SCP
|
||||
instance first where the Lc overflow used to blow up"""
|
||||
|
||||
def __init__(self, max_cmd_len=255, scp=None):
|
||||
self.max_cmd_len = max_cmd_len
|
||||
self.scp = scp
|
||||
self.sent = []
|
||||
self.wrapped = []
|
||||
|
||||
def send_apdu_checksw(self, apdu, sw='9000'):
|
||||
self.sent.append(apdu.lower())
|
||||
if self.scp:
|
||||
self.wrapped.append(self.scp.wrap_cmd_apdu(h2b(apdu)))
|
||||
return ('', '9000')
|
||||
|
||||
|
||||
class Load_ChunkLen_Test(unittest.TestCase):
|
||||
"""ADF_SD.load() chunking: block size must use scc.max_cmd_len"""
|
||||
|
||||
payload = b'\xaa' * 500 # actual real world case LOAD TLV: C4 + 8201f4 + 500 = 504 total
|
||||
|
||||
def _sd(self, scc):
|
||||
cmd = type('_Cmd', (), {'lchan': type('_Lchan', (), {'scc': scc})(),
|
||||
'poutput': lambda self, *args: None})()
|
||||
# cmd2 CommandSet has a r/o _cmd property -> shadow it
|
||||
_SD = type('_SD', (ADF_SD.AddlShellCommands,), {'_cmd': cmd})
|
||||
return _SD.__new__(_SD)
|
||||
|
||||
def _blocks(self, scc):
|
||||
"""Get (p1, p2, lc) from LOAD APDU"""
|
||||
for apdu in scc.sent:
|
||||
self.assertEqual(apdu[0:4], '80e8')
|
||||
yield int(apdu[4:6], 16), int(apdu[6:8], 16), int(apdu[8:10], 16)
|
||||
|
||||
def test_default_no_scp(self):
|
||||
"""Without SCP the old 240 byte block size is kept, no idea what else might rely on this number"""
|
||||
scc = _FakeSccForLoad(max_cmd_len=255)
|
||||
self._sd(scc).load(self.payload)
|
||||
blocks = list(self._blocks(scc))
|
||||
self.assertEqual([b[2] for b in blocks], [240, 240, 24])
|
||||
self.assertEqual([b[0] for b in blocks], [0x00, 0x00, 0x80]) # P1: last block flagged
|
||||
self.assertEqual([b[1] for b in blocks], [0, 1, 2]) # P2: block num
|
||||
|
||||
def test_default_scp02_level3(self):
|
||||
"""max_cmd_len 239 (SCP02 lvl 3) squeezes the blocks"""
|
||||
scc = _FakeSccForLoad(max_cmd_len=239)
|
||||
self._sd(scc).load(self.payload)
|
||||
self.assertEqual([b[2] for b in list(self._blocks(scc))], [239, 239, 26])
|
||||
|
||||
def test_explicit_chunk_len(self):
|
||||
scc = _FakeSccForLoad(max_cmd_len=255)
|
||||
self._sd(scc).load(self.payload, chunk_len=100)
|
||||
self.assertEqual([b[2] for b in list(self._blocks(scc))], [100] * 5 + [4])
|
||||
|
||||
def test_explicit_chunk_len_too_large(self):
|
||||
scc = _FakeSccForLoad(max_cmd_len=239)
|
||||
with self.assertRaises(ValueError):
|
||||
self._sd(scc).load(self.payload, chunk_len=240)
|
||||
self.assertEqual(scc.sent, []) # nothing sent!
|
||||
|
||||
def test_explicit_chunk_len_zero(self):
|
||||
scc = _FakeSccForLoad(max_cmd_len=255)
|
||||
with self.assertRaises(ValueError):
|
||||
self._sd(scc).load(self.payload, chunk_len=0)
|
||||
|
||||
def test_end_to_end_scp02_level3(self):
|
||||
"""original failure: 286 byte CAP + SCP02 lvl 3"""
|
||||
scp02 = SCP02(card_keys=ck_3des_70)
|
||||
scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
scp02.gen_ext_auth_apdu()
|
||||
scp02.security_level = 0x03
|
||||
scc = _FakeSccForLoad(max_cmd_len=255 - scp02.overhead, scp=scp02)
|
||||
self._sd(scc).load(b'\x5a' * 286)
|
||||
self.assertEqual(len(scc.sent), 2) # 289 byte TLV in blocks of 239
|
||||
for wrapped in scc.wrapped:
|
||||
self.assertLessEqual(wrapped[4], 255)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -37,6 +37,17 @@ expected_message = None
|
||||
|
||||
class PySimLogger_Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# PySimLogger.setup() is global, so a print callback left installed here fires for
|
||||
# every PySimLogger message emitted by any test module that runs later in the same process
|
||||
# ... where it asserts against a stale 'expected_message' and fails a test that has nothing
|
||||
# to do with logging. Great fun!
|
||||
# Restore before each test.
|
||||
saved = (PySimLogger.print_callback, PySimLogger.verbose)
|
||||
def _restore():
|
||||
PySimLogger.print_callback, PySimLogger.verbose = saved
|
||||
self.addCleanup(_restore)
|
||||
|
||||
def __test_01_safe_defaults_one(self, callback, message:str):
|
||||
# When log messages are sent to an unconfigured PySimLogger class, we expect the unmodified message being
|
||||
# logged to stdout, just as if it were printed via a normal print() statement.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user