11 Commits

Author SHA1 Message Date
Harald Welte
890e1951fe Implement Global Platform SCP03
This adds an implementation of the GlobalPlatform SCP03 protocol. It has
been tested in S8 mode for C-MAC, C-ENC, R-MAC and R-ENC with AES using
128, 192 and 256 bit key lengh.  Test vectors generated while talking to
a sysmoEUICC1-C2T are included as unit tests.

Change-Id: Ibc35af5474923aed2e3bcb29c8d713b4127a160d
2024-02-04 15:11:08 +01:00
Harald Welte
dd6895ff06 rename global_platform.scp02 to global_platform.scp
This is in preparation of extending it to cover SCP03 in a follow-up
patch.

Change-Id: Idc0afac6e95f89ddaf277a89f9c95607e70a471c
2024-02-04 15:06:50 +01:00
Harald Welte
3a0f881d84 Contstrain argparse integers to permitted range
In many casese we used type=int permitting any integer value, positive
or negative without a constratint in size.  However, in reality often
we're constrained to unsigned 8 or 16 bit ranges.  Let's use the
auto_uint{8,16} functions to enforce this within argparse before
we even try to encode something that won't work.

Change-Id: I35c81230bc18e2174ec1930aa81463f03bcd69c8
2024-02-04 14:53:07 +01:00
Harald Welte
4b23130da9 global_platform: Fix --key-id argument
The key-id is actually a 7-bit integer and on the wire the 8th bit
has a special meaning which can be derived automatically.

Let's unburden the user from explicitly encoding that 8th bit and
instead set it automatically.

Change-Id: I8da37aa8fd064e6d35ed29a70f5d7a0e9060be3a
2024-02-04 14:53:07 +01:00
Harald Welte
c84c04afb5 global_platform: add delete_key and delete_card_content
This GlobalPlatform command is used to delete applications/load-files
or keys.

Change-Id: Ib5d18e983d0e918633d7c090c54fb9a3384a22e5
2024-02-04 14:53:07 +01:00
Harald Welte
0e9ad7b5d4 global_platform: add set_status command
Using this command, one can change the life cycle status of on-card
applications, specifically one can LOCK (disable) them and re-enable
them as needed.

Change-Id: Ie14297a119d01cad1284f315a2508aa92cb4633b
2024-02-04 14:53:07 +01:00
Harald Welte
5dc8471526 global_platform: Add install_for_personalization command
This allows us to perform STORE DATA on applications like ARA-M/ARA-D
after establishing SCP02 to the related security domain.

Change-Id: I2ce766b97bba42c64c4d4492b505be66c24f471e
2024-02-04 14:53:07 +01:00
Harald Welte
5bbd720512 pySim-shell: Make 'apdu' command use logical (and secure) channel
The 'apdu' command so far bypassed the logical channel and also
the recently-introduced support for secure channels.  Let's change
that, at least by default.  If somebody wants a raw APDU without
secure / logical channel processing, they may use the --raw option.

Change-Id: Id0c364f772c31e11e8dfa21624d8685d253220d0
2024-02-04 14:53:07 +01:00
Harald Welte
71a7a19159 SCP02: Only C-MAC/C-ENCRYPT APDUs whose CLA byte indicates GlobalPlatform
I'm not entirely sure if this is the right thing to do.  For sure I do
have cards which don't like SELECT with C-MAC appended... and
GlobalPlatform clearly states SELECT is coded with CLA value that has
the MSB not set (i.e. not a GlobalPlatform command).

Change-Id: Ieda75c865a6ff2725fc3c8772bb274d96b8a5a43
2024-02-04 14:53:07 +01:00
Harald Welte
ca3678e471 Add global_platform shell command establish_scp02 and release_scp
Those commands can be used to establish and release a SCP02 secure
channel on the currently active logical channel.

The prompt is adjusted with a 'SCP02' prefix while the secure channel is
established.

Change-Id: Ib2f3c8f0563f81a941dd55b97c9836e3a6856407
2024-02-04 14:53:07 +01:00
Harald Welte
1e052f1efa Introduce GlobalPlatform SCP02 implementation
This implementation of GlobalPlatform SCP02 currently only supports
C-MAC and C-ENC, but no R-MAC or R-ENC yet.

The patch also introduces the notion of having a SCP instance associated
with a SimCardCommands instance.  No code is using this yet, it will be
introduced in a separate patch.

Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27
2024-02-04 14:53:07 +01:00
10 changed files with 977 additions and 27 deletions

View File

@@ -947,6 +947,12 @@ get_status
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.get_status_parser
set_status
~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.set_status_parser
store_data
~~~~~~~~~~
.. argparse::
@@ -959,6 +965,40 @@ put_key
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.put_key_parser
delete_key
~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.del_key_parser
install_for_personalization
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.inst_for_perso_parser
delete_card_content
~~~~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.del_cc_parser
establish_scp02
~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.est_scp02_parser
establish_scp03
~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.est_scp03_parser
release_scp
~~~~~~~~~~~
Release any previously established SCP (Secure Channel Protocol)
eUICC ISD-R commands
--------------------

View File

@@ -205,6 +205,10 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
def update_prompt(self):
if self.lchan:
path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
scp = self.lchan.scc.scp
if scp:
self.prompt = 'pySIM-shell (%s:%02u:%s)> ' % (str(scp), self.lchan.lchan_nr, path_str)
else:
self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
else:
if self.card:
@@ -233,6 +237,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
apdu_cmd_parser = argparse.ArgumentParser()
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
@cmd2.with_argparser(apdu_cmd_parser)
def do_apdu(self, opts):
@@ -245,7 +250,10 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
# noted that the apdu command plays an exceptional role since it is the only card accessing command that
# can be executed without the presence of a runtime state (self.rs) object. However, this also means that
# self.lchan is also not present (see method equip).
if opts.raw:
data, sw = self.card._scc.send_apdu(opts.APDU)
else:
data, sw = self.lchan.scc.send_apdu(opts.APDU)
if data:
self.poutput("SW: %s, RESP: %s" % (sw, data))
else:
@@ -258,6 +266,8 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
def do_reset(self, opts):
"""Reset the Card."""
atr = self.card.reset()
if self.lchan and self.lchan.scc.scp:
self.lchan.scc.scp = None
self.poutput('Card ATR: %s' % i2h(atr))
self.update_prompt()

View File

@@ -69,6 +69,7 @@ class SimCardCommands:
self.lchan_nr = lchan_nr
# invokes the setter below
self.cla_byte = "a0"
self.scp = None # Secure Channel Protocol
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
@@ -110,6 +111,9 @@ class SimCardCommands:
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
else:
return self._tp.send_apdu(pdu)
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
@@ -124,6 +128,9 @@ class SimCardCommands:
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if self.scp:
return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
else:
return self._tp.send_apdu_checksw(pdu, sw)
def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,

View File

@@ -38,7 +38,7 @@ from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
from smartcard.util import toBytes
from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, Hexstr, is_hexstr
from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, auto_uint8, auto_uint16, Hexstr, is_hexstr
from pySim.construct import filter_dict, parse_construct, build_construct
from pySim.exceptions import *
from pySim.jsonpath import js_path_find, js_path_modify
@@ -589,9 +589,9 @@ class TransparentEF(CardEF):
read_bin_parser = argparse.ArgumentParser()
read_bin_parser.add_argument(
'--offset', type=int, default=0, help='Byte offset for start of read')
'--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
read_bin_parser.add_argument(
'--length', type=int, help='Number of bytes to read')
'--length', type=auto_uint16, help='Number of bytes to read')
@cmd2.with_argparser(read_bin_parser)
def do_read_binary(self, opts):
@@ -611,7 +611,7 @@ class TransparentEF(CardEF):
upd_bin_parser = argparse.ArgumentParser()
upd_bin_parser.add_argument(
'--offset', type=int, default=0, help='Byte offset for start of read')
'--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
upd_bin_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(upd_bin_parser)
@@ -810,9 +810,9 @@ class LinFixedEF(CardEF):
read_rec_parser = argparse.ArgumentParser()
read_rec_parser.add_argument(
'record_nr', type=int, help='Number of record to be read')
'record_nr', type=auto_uint8, help='Number of record to be read')
read_rec_parser.add_argument(
'--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
'--count', type=auto_uint8, default=1, help='Number of records to be read, beginning at record_nr')
@cmd2.with_argparser(read_rec_parser)
def do_read_record(self, opts):
@@ -828,7 +828,7 @@ class LinFixedEF(CardEF):
read_rec_dec_parser = argparse.ArgumentParser()
read_rec_dec_parser.add_argument(
'record_nr', type=int, help='Number of record to be read')
'record_nr', type=auto_uint8, help='Number of record to be read')
read_rec_dec_parser.add_argument('--oneline', action='store_true',
help='No JSON pretty-printing, dump as a single line')
@@ -869,7 +869,7 @@ class LinFixedEF(CardEF):
upd_rec_parser = argparse.ArgumentParser()
upd_rec_parser.add_argument(
'record_nr', type=int, help='Number of record to be read')
'record_nr', type=auto_uint8, help='Number of record to be read')
upd_rec_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(upd_rec_parser)
@@ -881,7 +881,7 @@ class LinFixedEF(CardEF):
upd_rec_dec_parser = argparse.ArgumentParser()
upd_rec_dec_parser.add_argument(
'record_nr', type=int, help='Number of record to be read')
'record_nr', type=auto_uint8, help='Number of record to be read')
upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
upd_rec_dec_parser.add_argument('--json-path', type=str,
help='JSON path to modify specific element of record only')
@@ -902,7 +902,7 @@ class LinFixedEF(CardEF):
edit_rec_dec_parser = argparse.ArgumentParser()
edit_rec_dec_parser.add_argument(
'record_nr', type=int, help='Number of record to be edited')
'record_nr', type=auto_uint8, help='Number of record to be edited')
@cmd2.with_argparser(edit_rec_dec_parser)
def do_edit_record_decoded(self, opts):

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
(C) 2022-2023 by Harald Welte <laforge@osmocom.org>
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -20,7 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, List, Dict, Tuple
from construct import Optional as COptional
from construct import *
from copy import deepcopy
from bidict import bidict
from Cryptodome.Random import get_random_bytes
from pySim.global_platform.scp import SCP02, SCP03
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
@@ -92,6 +95,12 @@ KeyType = Enum(Byte, des=0x80,
ecc_key_parameters_reference=0xF0, # v2.3.1 Section 11.1.8
not_available=0xff)
# GlobalPlatform 2.3 Section 11.10.2.1 Table 11-86
SetStatusScope = Enum(Byte, isd=0x80, app_or_ssd=0x40, isd_and_assoc_apps=0xc0)
# GlobalPlatform 2.3 section 11.1.1
CLifeCycleState = Enum(Byte, loaded=0x01, installed=0x03, selectable=0x07, personalized=0x0f, locked=0x83)
# GlobalPlatform 2.1.1 Section 9.3.3.1
class KeyInformationData(BER_TLV_IE, tag=0xc0):
_test_de_encode = [
@@ -374,7 +383,7 @@ StatusSubset = Enum(Byte, isd=0x80, applications=0x40, files=0x20, files_and_mod
# Section 11.4.3.1 Table 11-36
class LifeCycleState(BER_TLV_IE, tag=0x9f70):
_construct = Int8ub
_construct = CLifeCycleState
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
class Privileges(BER_TLV_IE, tag=0xc5):
@@ -514,12 +523,12 @@ class ADF_SD(CardADF):
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
Example (SCP80 KIC/KID/KIK):
put_key --key-version-nr 1 --key-id 0x81 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
--key-type aes --key-data 101112131415161718191a1b1c1d1e1f
--key-type aes --key-data 202122232425262728292a2b2c2d2e2f
Example (SCP81 TLS-PSK/KEK):
put_key --key-version-nr 0x40 --key-id 0x81 --key-type tls_psk --key-data 303132333435363738393a3b3c3d3e3f
put_key --key-version-nr 0x40 --key-id 0x01 --key-type tls_psk --key-data 303132333435363738393a3b3c3d3e3f
--key-type des --key-data 404142434445464748494a4b4c4d4e4f
"""
@@ -532,7 +541,10 @@ class ADF_SD(CardADF):
else:
kcv = ''
kdb.append({'key_type': opts.key_type[i], 'kcb': opts.key_data[i], 'kcv': kcv})
self.put_key(opts.old_key_version_nr, opts.key_version_nr, opts.key_id, kdb)
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,
@@ -555,7 +567,7 @@ class ADF_SD(CardADF):
@cmd2.with_argparser(get_status_parser)
def do_get_status(self, opts):
"""Perform GlobalPlatform GET STATUS command in order to retriev status information
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
grd_list = self.get_status(opts.subset, opts.aid)
for grd in grd_list:
@@ -582,6 +594,144 @@ class ADF_SD(CardADF):
p2 |= 0x01
return grd_list
set_status_parser = argparse.ArgumentParser()
set_status_parser.add_argument('scope', choices=SetStatusScope.ksymapping.values(),
help='Defines the scope of the requested status change')
set_status_parser.add_argument('status', choices=CLifeCycleState.ksymapping.values(),
help='Specify the new intended status')
set_status_parser.add_argument('--aid', type=is_hexstr,
help='AID of the target Application or Security Domain')
@cmd2.with_argparser(set_status_parser)
def do_set_status(self, opts):
"""Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
prior authentication with a Secure Channel Protocol."""
self.set_status(opts.scope, opts.status, opts.aid)
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
'scope'/SetStatusScope, 'status'/CLifeCycleState,
'aid'/HexAdapter(Prefixed(Int8ub, Optional(GreedyBytes))))
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
data, sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
inst_perso_parser = argparse.ArgumentParser()
inst_perso_parser.add_argument('application-aid', type=is_hexstr, help='Application AID')
@cmd2.with_argparser(inst_perso_parser)
def do_install_for_personalization(self, opts):
"""Perform GlobalPlatform INSTALL [for personalization] command in order toinform a Security
Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
# Section 11.5.2.3.6 / Table 11-47
self.install(0x20, 0x00, "0000%02u%s000000" % (len(opts.application_aid)//2, opts.application_aid))
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
del_cc_parser = argparse.ArgumentParser()
del_cc_parser.add_argument('aid', type=is_hexstr,
help='Executable Load File or Application AID')
del_cc_parser.add_argument('--delete-related-objects', action='store_true',
help='Delete not only the object but also its related objects')
@cmd2.with_argparser(del_cc_parser)
def do_delete_card_content(self, opts):
"""Perform a GlobalPlatform DELETE [card content] command in order to delete an Executable Load
File, an Application or an Executable Load File and its related Applications."""
p2 = 0x80 if opts.delete_related_objects else 0x00
aid = ApplicationAID(decoded=opts.aid)
self.delete(0x00, p2, b2h(aid.to_tlv()))
del_key_parser = argparse.ArgumentParser()
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
del_key_parser.add_argument('--key-ver', type=auto_uint8, help='Key Version Number (KVN)')
del_key_parser.add_argument('--delete-related-objects', action='store_true',
help='Delete not only the object but also its related objects')
@cmd2.with_argparser(del_key_parser)
def do_delete_key(self, opts):
"""Perform GlobalPlaform DELETE (Key) command.
If both KID and KVN are specified, exactly one key is deleted. If only either of the two is
specified, multiple matching keys may be deleted."""
if opts.key_id == None and opts.key_ver == None:
raise ValueError('At least one of KID or KVN must be specified')
p2 = 0x80 if opts.delete_related_objects else 0x00
cmd = ""
if opts.key_id != None:
cmd += "d001%02x" % opts.key_id
if opts.key_ver != None:
cmd += "d201%02x" % opts.key_ver
self.delete(0x00, p2, cmd)
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
est_scp02_parser = argparse.ArgumentParser()
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True,
help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--key-enc', type=is_hexstr, required=True,
help='Secure Channel Encryption Key')
est_scp02_parser.add_argument('--key-mac', type=is_hexstr, required=True,
help='Secure Channel MAC Key')
est_scp02_parser.add_argument('--key-dek', type=is_hexstr, required=True,
help='Data Encryption Key')
est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
help='Hard-code the host challenge; default: random')
est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
help='Security Level. Default: 0x01 (C-MAC only)')
@cmd2.with_argparser(est_scp02_parser)
def do_establish_scp02(self, opts):
"""Establish a secure channel using the GlobalPlatform SCP02 protocol. It can be released
again by using `release_scp`."""
if self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot establish SCP02 as this lchan already has a SCP instance!")
return
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
scp02 = SCP02(card_keys=kset)
self._establish_scp(scp02, host_challenge, opts.security_level)
est_scp03_parser = deepcopy(est_scp02_parser)
est_scp03_parser.add_argument('--s16-mode', action='store_true', help='S16 mode (S8 is default)')
@cmd2.with_argparser(est_scp03_parser)
def do_establish_scp03(self, opts):
"""Establish a secure channel using the GlobalPlatform SCP03 protocol. It can be released
again by using `release_scp`."""
if self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot establish SCP03 as this lchan already has a SCP instance!")
return
s_mode = 16 if opts.s16_mode else 8
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
scp03 = SCP03(card_keys=kset, s_mode = s_mode)
self._establish_scp(scp03, host_challenge, opts.security_level)
def _establish_scp(self, scp, host_challenge, security_level):
# perform the common functionality shared by SCP02 and SCP03 establishment
init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
init_update_resp, sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
scp.parse_init_update_resp(h2b(init_update_resp))
ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
ext_auth_resp, sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
# store a reference to the SCP instance
self._cmd.lchan.scc.scp = scp
self._cmd.update_prompt()
def do_release_scp(self, opts):
"""Release a previously establiehed secure channel."""
if not self._cmd.lchan.scc.scp:
self._cmd.poutput("Cannot release SCP as none is established")
return
self._cmd.lchan.scc.scp = None
self._cmd.update_prompt()
# Card Application of a Security Domain
class CardApplicationSD(CardApplication):
@@ -601,3 +751,22 @@ class CardApplicationISD(CardApplicationSD):
#
# def __init__(self, name='GlobalPlatform'):
# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
class GpCardKeyset:
"""A single set of GlobalPlatform card keys and the associated KVN."""
def __init__(self, kvn: int, enc: bytes, mac: bytes, dek: bytes):
assert kvn >= 0 and kvn < 256
assert len(enc) == len(mac) == len(dek)
self.kvn = kvn
self.enc = enc
self.mac = mac
self.dek = dek
@classmethod
def from_single_key(cls, kvn: int, base_key: bytes) -> 'GpCardKeyset':
return cls(int, base_key, base_key, base_key)
def __str__(self):
return "%s(KVN=%u, ENC=%s, MAC=%s, DEK=%s)" % (self.__class__.__name__,
self.kvn, b2h(self.enc), b2h(self.mac), b2h(self.dek))

View File

@@ -0,0 +1,477 @@
# Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import logging
from typing import Optional
from Cryptodome.Cipher import DES3, DES
from Cryptodome.Util.strxor import strxor
from construct import Struct, Bytes, Int8ub, Int16ub, Const
from construct import Optional as COptional
from pySim.utils import b2h
from pySim.secure_channel import SecureChannel
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes:
assert(len(constant) == 2)
assert(counter >= 0 and counter <= 65535)
assert(len(base_key) == 16)
derivation_data = constant + counter.to_bytes(2, 'big') + b'\x00' * 12
cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8)
return cipher.encrypt(derivation_data)
# TODO: resolve duplication with BspAlgoCryptAES128
def pad80(s: bytes, BS=8) -> bytes:
""" Pad bytestring s: add '\x80' and '\0'* so the result to be multiple of BS."""
l = BS-1 - len(s) % BS
return s + b'\x80' + b'\0'*l
# TODO: resolve duplication with BspAlgoCryptAES128
def unpad80(padded: bytes) -> bytes:
"""Remove the customary 80 00 00 ... padding used for AES."""
# first remove any trailing zero bytes
stripped = padded.rstrip(b'\0')
# then remove the final 80
assert stripped[-1] == 0x80
return stripped[:-1]
class Scp02SessionKeys:
"""A single set of GlobalPlatform session keys."""
DERIV_CONST_CMAC = b'\x01\x01'
DERIV_CONST_RMAC = b'\x01\x02'
DERIV_CONST_ENC = b'\x01\x82'
DERIV_CONST_DENC = b'\x01\x81'
def calc_mac_1des(self, data: bytes, reset_icv: bool = False) -> bytes:
"""Pad and calculate MAC according to B.1.2.2 - Single DES plus final 3DES"""
e = DES.new(self.c_mac[:8], DES.MODE_ECB)
d = DES.new(self.c_mac[8:], DES.MODE_ECB)
padded_data = pad80(data, 8)
q = len(padded_data) // 8
icv = b'\x00' * 8 if reset_icv else self.icv
h = icv
for i in range(q):
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
h = d.decrypt(h)
h = e.encrypt(h)
logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
if self.des_icv_enc:
self.icv = self.des_icv_enc.encrypt(h)
else:
self.icv = h
return h
def calc_mac_3des(self, data: bytes) -> bytes:
e = DES3.new(self.enc, DES.MODE_ECB)
padded_data = pad80(data, 8)
q = len(padded_data) // 8
h = b'\x00' * 8
for i in range(q):
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
return h
def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True):
self.icv = None
self.counter = counter
self.card_keys = card_keys
self.c_mac = scp02_key_derivation(self.DERIV_CONST_CMAC, self.counter, card_keys.mac)
self.r_mac = scp02_key_derivation(self.DERIV_CONST_RMAC, self.counter, card_keys.mac)
self.enc = scp02_key_derivation(self.DERIV_CONST_ENC, self.counter, card_keys.enc)
self.data_enc = scp02_key_derivation(self.DERIV_CONST_DENC, self.counter, card_keys.dek)
self.des_icv_enc = DES.new(self.c_mac[:8], DES.MODE_ECB) if icv_encrypt else None
def __str__(self) -> str:
return "%s(CTR=%u, ICV=%s, ENC=%s, D-ENC=%s, MAC-C=%s, MAC-R=%s)" % (
self.__class__.__name__, self.counter, b2h(self.icv) if self.icv else "None",
b2h(self.enc), b2h(self.data_enc), b2h(self.c_mac), b2h(self.r_mac))
INS_INIT_UPDATE = 0x50
INS_EXT_AUTH = 0x82
CLA_SM = 0x04
class SCP(SecureChannel, abc.ABC):
"""Abstract base class containing some common interface + functionality for SCP protocols."""
def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0):
if hasattr(self, 'kvn_range'):
if not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1):
raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' %
(self.__class__.__name__, self.kvn_range[0], self.kvn_range[1]))
self.lchan_nr = lchan_nr
self.card_keys = card_keys
self.sk = None
self.mac_on_unmodified = False
self.security_level = 0x00
@property
def do_cmac(self) -> bool:
"""Should we perform C-MAC?"""
return self.security_level & 0x01
@property
def do_rmac(self) -> bool:
"""Should we perform R-MAC?"""
return self.security_level & 0x10
@property
def do_cenc(self) -> bool:
"""Should we perform C-ENC?"""
return self.security_level & 0x02
@property
def do_renc(self) -> bool:
"""Should we perform R-ENC?"""
return self.security_level & 0x20
def __str__(self) -> str:
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
def _cla(self, sm: bool = False, b8: bool = True) -> int:
ret = 0x80 if b8 else 0x00
if sm:
ret = ret | CLA_SM
return ret + self.lchan_nr
def wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
# Generic handling of GlobalPlatform SCP, implements SecureChannel.wrap_cmd_apdu
# only protect those APDUs that actually are global platform commands
if apdu[0] & 0x80:
return self._wrap_cmd_apdu(apdu, *args, **kwargs)
else:
return apdu
@abc.abstractmethod
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
"""Method implementation to be provided by derived class."""
pass
@abc.abstractmethod
def gen_init_update_apdu(self, host_challenge: Optional[bytes]) -> bytes:
pass
@abc.abstractmethod
def parse_init_update_resp(self, resp_bin: bytes):
pass
@abc.abstractmethod
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
pass
class SCP02(SCP):
"""An instance of the GlobalPlatform SCP02 secure channel protocol."""
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x02'),
'seq_counter'/Int16ub, 'card_challenge'/Bytes(6), 'card_cryptogram'/Bytes(8))
kvn_range = [0x20, 0x2f]
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge)
self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge)
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
"""Generate INITIALIZE UPDATE APDU."""
self.host_challenge = host_challenge
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALZIE UPDATE."""
resp = self.constr_iur.parse(resp_bin)
self.card_challenge = resp['card_challenge']
self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys)
logger.debug(self.sk)
self._compute_cryptograms(self.card_challenge, self.host_challenge)
if self.card_cryptogram != resp['card_cryptogram']:
raise ValueError("card cryptogram doesn't match")
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
"""Generate EXTERNAL AUTHENTICATE APDU."""
if security_level & 0xf0:
raise NotImplementedError('R-MAC/R-ENC for SCP02 not implemented yet.')
self.security_level = security_level
if self.mac_on_unmodified:
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, 8])
else:
header = bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16])
#return self.wrap_cmd_apdu(header + self.host_cryptogram)
mac = self.sk.calc_mac_1des(header + self.host_cryptogram, True)
return bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16]) + self.host_cryptogram + mac
def _wrap_cmd_apdu(self, apdu: bytes) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
lc = len(apdu) - 5
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
cla = apdu[0]
b8 = cla & 0x80
if cla & 0x03 or cla & CLA_SM:
# nonzero logical channel in APDU, check that are the same
assert cla == self._cla(False, b8), "CLA mismatch"
# CLA without log. channel can be 80 or 00 only
if self.do_cmac:
if self.mac_on_unmodified:
mlc = lc
clac = cla
else: # CMAC on modified APDU
mlc = lc + 8
clac = cla | CLA_SM
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
if self.do_cenc:
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
data = k.encrypt(pad80(apdu[5:], 8))
lc = len(data)
else:
data = apdu[5:]
lc += 8
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
return apdu
def unwrap_rsp_apdu(self, sw: bytes, apdu: bytes) -> bytes:
# TODO: Implement R-MAC / R-ENC
return apdu
from Cryptodome.Cipher import AES
from Cryptodome.Hash import CMAC
def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Optional[int] = None) -> bytes:
"""SCP03 Key Derivation Function as specified in Annex D 4.1.5."""
# Data derivation shall use KDF in counter mode as specified in NIST SP 800-108 ([NIST 800-108]). The PRF
# used in the KDF shall be CMAC as specified in [NIST 800-38B], used with full 16-byte output length.
def prf(key: bytes, data:bytes):
return CMAC.new(key, data, AES).digest()
if l == None:
l = len(base_key) * 8
logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
output_len = l // 8
# SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the
# existing Cryptodome.Protocol.KDF.SP800_108_Counter function :(
# A 12-byte “label” consisting of 11 bytes with value '00' followed by a 1-byte derivation constant
assert len(constant) == 1
label = b'\x00' *11 + constant
i = 1
dk = b''
while len(dk) < output_len:
# 12B label, 1B separation, 2B L, 1B i, Context
info = label + b'\x00' + l.to_bytes(2, 'big') + bytes([i]) + context
dk += prf(base_key, info)
i += 1
if i > 0xffff:
raise ValueError("Overflow in SP800 108 counter")
return dk[:output_len]
class Scp03SessionKeys:
# GPC 2.3 Amendment D v1.2 Section 4.1.5 Table 4-1
DERIV_CONST_AUTH_CGRAM_CARD = b'\x00'
DERIV_CONST_AUTH_CGRAM_HOST = b'\x01'
DERIV_CONST_CARD_CHLG_GEN = b'\x02'
DERIV_CONST_KDERIV_S_ENC = b'\x04'
DERIV_CONST_KDERIV_S_MAC = b'\x06'
DERIV_CONST_KDERIV_S_RMAC = b'\x07'
blocksize = 16
def __init__(self, card_keys: 'GpCardKeyset', host_challenge: bytes, card_challenge: bytes):
# GPC 2.3 Amendment D v1.2 Section 6.2.1
context = host_challenge + card_challenge
self.s_enc = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_ENC, context, card_keys.enc)
self.s_mac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_MAC, context, card_keys.mac)
self.s_rmac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_RMAC, context, card_keys.mac)
# The first MAC chaining value is set to 16 bytes '00'
self.mac_chaining_value = b'\x00' * 16
# The encryption counters start value shall be set to 1 (we set it immediately before generating ICV)
self.block_nr = 0
def calc_cmac(self, apdu: bytes):
"""Compute C-MAC for given to-be-transmitted APDU.
Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
cmac_input = self.mac_chaining_value + apdu
cmac_val = CMAC.new(self.s_mac, cmac_input, ciphermod=AES).digest()
self.mac_chaining_value = cmac_val
return cmac_val
def calc_rmac(self, rdata_and_sw: bytes):
"""Compute R-MAC for given received R-APDU data section.
Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
rmac_input = self.mac_chaining_value + rdata_and_sw
return CMAC.new(self.s_rmac, rmac_input, ciphermod=AES).digest()
def _get_icv(self, is_response: bool = False):
"""Obtain the ICV value computed as described in 6.2.6.
This method has two modes:
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
* is_response=False for computing the ICV for R-DEC."""
if not is_response:
self.block_nr += 1
# The binary value of this number SHALL be left padded with zeroes to form a full block.
data = self.block_nr.to_bytes(self.blocksize, "big")
if is_response:
# Section 6.2.7: additional intermediate step: Before encryption, the most significant byte of
# this block shall be set to '80'.
data = b'\x80' + data[1:]
iv = bytes([0] * self.blocksize)
# This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
icv = cipher.encrypt(data)
logger.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv))
return icv
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-wrapping
def _encrypt(self, data: bytes, is_response: bool = False) -> bytes:
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
return cipher.encrypt(data)
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-unwrapping
def _decrypt(self, data: bytes, is_response: bool = True) -> bytes:
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
return cipher.decrypt(data)
class SCP03(SCP):
"""Secure Channel Protocol (SCP) 03 as specified in GlobalPlatform v2.3 Amendment D."""
# Section 7.1.1.6 / Table 7-3
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x03'), 'i_param'/Int8ub,
'card_challenge'/Bytes(lambda ctx: ctx._.s_mode),
'card_cryptogram'/Bytes(lambda ctx: ctx._.s_mode),
'sequence_counter'/COptional(Bytes(3)))
kvn_range = [0x30, 0x3f]
def __init__(self, *args, **kwargs):
self.s_mode = kwargs.pop('s_mode', 8)
super().__init__(*args, **kwargs)
def _compute_cryptograms(self):
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge))
# Card + Host Authentication Cryptogram: Section 6.2.2.2 + 6.2.2.3
context = self.host_challenge + self.card_challenge
self.card_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_CARD, context, self.sk.s_mac, l=self.s_mode*8)
self.host_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_HOST, context, self.sk.s_mac, l=self.s_mode*8)
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
"""Generate INITIALIZE UPDATE APDU."""
if host_challenge == None:
host_challenge = b'\x00' * self.s_mode
if len(host_challenge) != self.s_mode:
raise ValueError('Host Challenge must be %u bytes long' % self.s_mode)
self.host_challenge = host_challenge
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALIZE UPDATE."""
if len(resp_bin) not in [10+3+8+8, 10+3+16+16, 10+3+8+8+3, 10+3+16+16+3]:
raise ValueError('Invalid length of Initialize Update Response')
resp = self.constr_iur.parse(resp_bin, s_mode=self.s_mode)
self.card_challenge = resp['card_challenge']
self.i_param = resp['i_param']
# derive session keys and compute cryptograms
self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge)
logger.debug(self.sk)
self._compute_cryptograms()
# verify computed cryptogram matches received cryptogram
if self.card_cryptogram != resp['card_cryptogram']:
raise ValueError("card cryptogram doesn't match")
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
"""Generate EXTERNAL AUTHENTICATE APDU."""
self.security_level = security_level
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, self.s_mode])
# bypass encryption for EXTERNAL AUTHENTICATE
return self.wrap_cmd_apdu(header + self.host_cryptogram, skip_cenc=True)
def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
cla = apdu[0]
ins = apdu[1]
p1 = apdu[2]
p2 = apdu[3]
lc = apdu[4]
assert lc == len(apdu) - 5
cmd_data = apdu[5:]
if self.do_cenc and not skip_cenc:
assert self.do_cmac
if lc == 0:
# No encryption shall be applied to a command where there is no command data field. In this
# case, the encryption counter shall still be incremented
self.sk.block_nr += 1
else:
# data shall be padded as defined in [GPCS] section B.2.3
padded_data = pad80(cmd_data, 16)
lc = len(padded_data)
if lc >= 256:
raise ValueError('Modified Lc (%u) would exceed maximum when appending padding' % (lc))
# perform AES-CBC with ICV + S_ENC
cmd_data = self.sk._encrypt(padded_data)
if self.do_cmac:
# The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
# mode) to indicate the inclusion of the C-MAC in the data field of the command message.
mlc = lc + self.s_mode
if mlc >= 256:
raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
# The class byte shall be modified for the generation or verification of the C-MAC: The logical
# channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
# GlobalPlatform proprietary secure messaging.
mcla = (cla & 0xF0) | CLA_SM
mapdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
cmac = self.sk.calc_cmac(mapdu)
mapdu += cmac[:self.s_mode]
return mapdu
def unwrap_rsp_apdu(self, sw: bytes, apdu: bytes) -> bytes:
# No R-MAC shall be generated and no protection shall be applied to a response that includes an error
# status word: in this case only the status word shall be returned in the response. All status words
# except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status
# words.
logger.debug("unwrap_rsp_apdu(sw=%s, apdu=%s)", sw, apdu)
if not self.do_rmac:
assert not self.do_renc
return apdu
if sw != b'\x90\x00' and sw[0] not in [0x62, 0x63]:
return apdu
response_data = apdu[:-self.s_mode]
rmac = apdu[-self.s_mode:]
rmac_exp = self.sk.calc_rmac(response_data + sw)[:self.s_mode]
if rmac != rmac_exp:
raise ValueError("R-MAC value not matching: received: %s, computed: %s" % (rmac, rmac_exp))
if self.do_renc:
# decrypt response data
decrypted = self.sk._decrypt(response_data)
logger.debug("decrypted: %s", b2h(decrypted))
# remove padding
response_data = unpad80(decrypted)
logger.debug("response_data: %s", b2h(response_data))
return response_data

37
pySim/secure_channel.py Normal file
View File

@@ -0,0 +1,37 @@
# Generic code related to Secure Channel processing
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
from pySim.utils import b2h, h2b, ResTuple, Hexstr
class SecureChannel(abc.ABC):
@abc.abstractmethod
def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
"""Wrap Command APDU according to specific Secure Channel Protocol."""
pass
@abc.abstractmethod
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
"""UnWrap Response-APDU according to specific Secure Channel Protocol."""
pass
def send_apdu_wrapper(self, send_fn: callable, pdu: Hexstr, *args, **kwargs) -> ResTuple:
"""Wrapper function to wrap command APDU and unwrap repsonse APDU around send_apdu callable."""
pdu_wrapped = b2h(self.wrap_cmd_apdu(h2b(pdu)))
res, sw = send_fn(pdu_wrapped, *args, **kwargs)
res_unwrapped = b2h(self.unwrap_rsp_apdu(h2b(sw), h2b(res)))
return res_unwrapped, sw

View File

@@ -24,7 +24,7 @@ from cmd2 import CommandSet, with_default_category, with_argparser
import argparse
from pySim.exceptions import *
from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder, auto_uint8, auto_uint16
from pySim.ts_102_221 import *
@@ -112,13 +112,13 @@ class Ts102222Commands(CommandSet):
create_required = create_parser.add_argument_group('required arguments')
create_optional = create_parser.add_argument_group('optional arguments')
create_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
create_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
create_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
create_required.add_argument('--ef-arr-record-nr', required=True, type=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
create_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
create_required.add_argument('--structure', required=True, type=str, choices=['transparent', 'linear_fixed', 'ber_tlv'],
help='Structure of the to-be-created EF')
create_optional.add_argument('--short-file-id', type=str, help='Short File Identifier as 2-digit hex string')
create_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
create_optional.add_argument('--record-length', type=int, help='Length of each record in octets')
create_optional.add_argument('--record-length', type=auto_uint16, help='Length of each record in octets')
@cmd2.with_argparser(create_parser)
def do_create_ef(self, opts):
@@ -160,11 +160,11 @@ class Ts102222Commands(CommandSet):
createdf_optional = createdf_parser.add_argument_group('optional arguments')
createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments')
createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
createdf_required.add_argument('--ef-arr-record-nr', required=True, type=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
createdf_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
createdf_optional.add_argument('--aid', type=is_hexstr, help='Application ID (creates an ADF, instead of a DF)')
# mandatory by spec, but ignored by several OS, so don't force the user
createdf_optional.add_argument('--total-file-size', type=int, help='Physical memory allocated for DF/ADi in octets')
createdf_optional.add_argument('--total-file-size', type=auto_uint16, help='Physical memory allocated for DF/ADi in octets')
createdf_sja_optional.add_argument('--permit-rfm-create', action='store_true')
createdf_sja_optional.add_argument('--permit-rfm-delete-terminate', action='store_true')
createdf_sja_optional.add_argument('--permit-other-applet-create', action='store_true')
@@ -208,7 +208,7 @@ class Ts102222Commands(CommandSet):
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
resize_ef_parser._action_groups.pop()
resize_ef_required = resize_ef_parser.add_argument_group('required arguments')
resize_ef_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
resize_ef_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
@cmd2.with_argparser(resize_ef_parser)
def do_resize_ef(self, opts):

View File

@@ -927,6 +927,9 @@ def auto_uint7(x):
def auto_uint8(x):
return _auto_uint(x, 255)
def auto_uint16(x):
return _auto_uint(x, 65535)
def expand_hex(hexstring, length):
"""Expand a given hexstring to a specified length by replacing "." or ".."
with a filler that is derived from the neighboring nibbles respective

View File

@@ -0,0 +1,207 @@
#!/usr/bin/env python3
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import logging
from pySim.global_platform import *
from pySim.global_platform.scp import *
from pySim.utils import b2h, h2b
KIC = h2b('100102030405060708090a0b0c0d0e0f') # enc
KID = h2b('101102030405060708090a0b0c0d0e0f') # MAC
KIK = h2b('102102030405060708090a0b0c0d0e0f') # DEK
ck_3des_70 = GpCardKeyset(0x20, KIC, KID, KIK)
class SCP02_Auth_Test(unittest.TestCase):
host_challenge = h2b('40A62C37FA6304F8')
init_update_resp = h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')
def setUp(self):
self.scp02 = SCP02(card_keys=ck_3des_70)
def test_mutual_auth_success(self):
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8')
self.scp02.parse_init_update_resp(self.init_update_resp)
ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
self.assertEqual(b2h(ext_auth_cmd).upper(), '8482010010BA6961667737C5BCEBECE14C7D6A4376')
def test_mutual_auth_fail_card_cryptogram(self):
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8')
wrong_init_update_resp = self.init_update_resp.copy()
wrong_init_update_resp[-1:] = b'\xff'
with self.assertRaises(ValueError):
self.scp02.parse_init_update_resp(wrong_init_update_resp)
class SCP02_Test(unittest.TestCase):
host_challenge = h2b('40A62C37FA6304F8')
init_update_resp = h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')
def setUp(self):
self.scp02 = SCP02(card_keys=ck_3des_70)
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.scp02.parse_init_update_resp(self.init_update_resp)
ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
def test_mac_command(self):
wrapped = self.scp02.wrap_cmd_apdu(h2b('80f28002024f00'))
self.assertEqual(b2h(wrapped).upper(), '84F280020A4F00B21AAFA3EB2D1672')
class SCP03_Test:
"""some kind of 'abstract base class' for a unittest.UnitTest, implementing common functionality for all
of our SCP03 test caseses."""
get_eid_cmd_plain = h2b('80E2910006BF3E035C015A')
get_eid_rsp_plain = h2b('bf3e125a1089882119900000000000000000000005')
@property
def host_challenge(self) -> bytes:
return self.init_upd_cmd[5:]
@property
def kvn(self) -> int:
return self.init_upd_cmd[2]
@property
def security_level(self) -> int:
return self.ext_auth_cmd[2]
@property
def card_challenge(self) -> bytes:
if len(self.init_upd_rsp) in [10+3+8+8, 10+3+8+8+3]:
return self.init_upd_rsp[10+3:10+3+8]
else:
return self.init_upd_rsp[10+3:10+3+16]
@property
def card_cryptogram(self) -> bytes:
if len(self.init_upd_rsp) in [10+3+8+8, 10+3+8+8+3]:
return self.init_upd_rsp[10+3+8:10+3+8+8]
else:
return self.init_upd_rsp[10+3+16:10+3+16+16]
@classmethod
def setUpClass(cls):
cls.scp = SCP03(card_keys = cls.keyset)
def test_01_initialize_update(self):
self.assertEqual(self.init_upd_cmd, self.scp.gen_init_update_apdu(self.host_challenge))
def test_02_parse_init_upd_resp(self):
self.scp.parse_init_update_resp(self.init_upd_rsp)
def test_03_gen_ext_auth_apdu(self):
self.assertEqual(self.ext_auth_cmd, self.scp.gen_ext_auth_apdu(self.security_level))
def test_04_wrap_cmd_apdu_get_eid(self):
self.assertEqual(self.get_eid_cmd, self.scp.wrap_cmd_apdu(self.get_eid_cmd_plain))
def test_05_unwrap_rsp_apdu_get_eid(self):
self.assertEqual(self.get_eid_rsp_plain, self.scp.unwrap_rsp_apdu(h2b('9000'), self.get_eid_rsp))
# The SCP03 keysets used for various key lenghs
KEYSET_AES128 = GpCardKeyset(0x30, h2b('000102030405060708090a0b0c0d0e0f'), h2b('101112131415161718191a1b1c1d1e1f'), h2b('202122232425262728292a2b2c2d2e2f'))
KEYSET_AES192 = GpCardKeyset(0x31, h2b('000102030405060708090a0b0c0d0e0f0001020304050607'),
h2b('101112131415161718191a1b1c1d1e1f1011121314151617'), h2b('202122232425262728292a2b2c2d2e2f2021222324252627'))
KEYSET_AES256 = GpCardKeyset(0x32, h2b('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f'),
h2b('101112131415161718191a1b1c1d1e1f101112131415161718191a1b1c1d1e1f'),
h2b('202122232425262728292a2b2c2d2e2f202122232425262728292a2b2c2d2e2f'))
class SCP03_Test_AES128_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128
init_upd_cmd = h2b('8050300008b13e5f938fc108c4')
init_upd_rsp = h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002')
ext_auth_cmd = h2b('84821100107d5f5826a993ebc89eea24957fa0b3ce')
get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a28297')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005971be68992dbbdfa')
class SCP03_Test_AES128_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128
init_upd_cmd = h2b('80503000088e1552d0513c60f3')
init_upd_rsp = h2b('0000000000000000000030037030760cd2c47c1dd395065fe5ead8a9d7000001')
ext_auth_cmd = h2b('8482030010fd4721a14d9b07003c451d2f8ae6bb21')
get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
class SCP03_Test_AES128_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128
init_upd_cmd = h2b('8050300008fdf38259a1e0de44')
init_upd_rsp = h2b('000000000000000000003003703b1aca81e821f219081cdc01c26b372d000003')
ext_auth_cmd = h2b('84823300108c36f96bcc00724a4e13ad591d7da3f0')
get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f')
get_eid_rsp = h2b('f3ba2b1013aa6224f5e1c138d71805c569e5439b47576260b75fc021b25097cb2e68f8a0144975b9')
class SCP03_Test_AES192_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192
init_upd_cmd = h2b('80503100087396430b768b085b')
init_upd_rsp = h2b('000000000000000000003103708cfc23522ffdbf1e5df5542cac8fd866000003')
ext_auth_cmd = h2b('84821100102145ed30b146f5db252fb7e624cec244')
get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d143944')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005162fbd33e04940a9')
class SCP03_Test_AES192_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192
init_upd_cmd = h2b('805031000869c65da8202bf19f')
init_upd_rsp = h2b('00000000000000000000310370b570a67be38446717729d6dd3d2ec5b1000001')
ext_auth_cmd = h2b('848203001065df4f1a356a887905466516d9e5b7c1')
get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
class SCP03_Test_AES192_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192
init_upd_cmd = h2b('80503100089b3f2eef0e8c9374')
init_upd_rsp = h2b('00000000000000000000310370f6bb305a15bae1a68f79fb08212fbed7000002')
ext_auth_cmd = h2b('84823300109100bc22d58b45b86a26365ce39ff3cf')
get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de589')
get_eid_rsp = h2b('c05176c1b6f72aae50c32cbee63b0e95998928fd4dfb2be9f27ffde8c8476f5909b4805cc4039599')
class SCP03_Test_AES256_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256
init_upd_cmd = h2b('805032000811666d57866c6f54')
init_upd_rsp = h2b('0000000000000000000032037053ea8847efa7674e41498a4d66cf0dee000003')
ext_auth_cmd = h2b('84821100102f2ad190eff2fafc4908996d1cebd310')
get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d')
get_eid_rsp = h2b('bf3e125a10898821199000000000000000000000058012dd7f01f1c4c1')
class SCP03_Test_AES256_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256
init_upd_cmd = h2b('8050320008c6066990fc426e1d')
init_upd_rsp = h2b('000000000000000000003203708682cd81bbd8919f2de3f2664581f118000001')
ext_auth_cmd = h2b('848203001077c493b632edadaf865a1e64acc07ce9')
get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe44')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256
init_upd_cmd = h2b('805032000897b2055fe58599fd')
init_upd_rsp = h2b('00000000000000000000320370a8439a22cedf045fa9f1903b2834f26e000002')
ext_auth_cmd = h2b('8482330010508a0fd959d2e547c6b33154a6be2057')
get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea')
get_eid_rsp = h2b('ea90dbfa648a67c5eb6abc57f8530b97d0cd5647c5e8732016b55203b078dd2ace7f8bc5d1c1cd99')
# FIXME:
# - for S8 and S16 mode
# FIXME: test auth with random (0x60) vs pseudo-random (0x70) challenge
if __name__ == "__main__":
unittest.main()