laforge has submitted this change. ( 
https://gerrit.osmocom.org/c/pysim/+/35462?usp=email )

Change subject: Introduce GlobalPlatform SCP02 implementation
......................................................................

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.  It also adds the establish_scp0w and
release_scp shell commands to all GlobalPlatform Security Domains.

Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27
---
M docs/shell.rst
M pySim-shell.py
M pySim/commands.py
M pySim/global_platform/__init__.py
A pySim/global_platform/scp02.py
A pySim/secure_channel.py
A tests/test_globalplatform.py
7 files changed, 453 insertions(+), 4 deletions(-)

Approvals:
  Jenkins Builder: Verified
  laforge: Looks good to me, approved




diff --git a/docs/shell.rst b/docs/shell.rst
index a917f8f..dff6cd1 100644
--- a/docs/shell.rst
+++ b/docs/shell.rst
@@ -959,6 +959,16 @@
    :module: pySim.global_platform
    :func: ADF_SD.AddlShellCommands.put_key_parser

+establish_scp02
+~~~~~~~~~~~~~~~
+.. argparse::
+   :module: pySim.global_platform
+   :func: ADF_SD.AddlShellCommands.est_scp02_parser
+
+release_scp
+~~~~~~~~~~~
+Release any previously established SCP (Secure Channel Protocol)
+

 eUICC ISD-R commands
 --------------------
diff --git a/pySim-shell.py b/pySim-shell.py
index 70eaee2..abe0b5f 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -205,7 +205,11 @@
     def update_prompt(self):
         if self.lchan:
             path_str = self.lchan.selected_file.fully_qualified_path_str(not 
self.numeric_path)
-            self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, 
path_str)
+            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:
                 self.prompt = 'pySIM-shell (no card profile)> '
@@ -258,6 +262,8 @@
     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()

diff --git a/pySim/commands.py b/pySim/commands.py
index 2d0736e..81edf82 100644
--- a/pySim/commands.py
+++ b/pySim/commands.py
@@ -69,6 +69,7 @@
         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,7 +111,10 @@
                         data : string (in hex) of returned data (ex. 
"074F4EFFFF")
                         sw   : string (in hex) of status word (ex. "9000")
         """
-        return self._tp.send_apdu(pdu)
+        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:
         """Sends an APDU and check returned SW
@@ -124,7 +128,10 @@
                         data : string (in hex) of returned data (ex. 
"074F4EFFFF")
                         sw   : string (in hex) of status word (ex. "9000")
         """
-        return self._tp.send_apdu_checksw(pdu, sw)
+        if self.scp:
+            return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, 
sw)
+        else:
+            return self._tp.send_apdu_checksw(pdu, sw)

     def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: 
Hexstr, cmd_constr: Construct,
                          cmd_data: Hexstr, resp_constr: Construct) -> 
Tuple[dict, SwHexstr]:
diff --git a/pySim/global_platform/__init__.py 
b/pySim/global_platform/__init__.py
index 25b0d02..ad193ee 100644
--- a/pySim/global_platform/__init__.py
+++ b/pySim/global_platform/__init__.py
@@ -1,7 +1,7 @@
 # coding=utf-8
 """Partial Support for GlobalPLatform Card Spec (currently 2.1.1)

-(C) 2022-2023 by Harald Welte <lafo...@osmocom.org>
+(C) 2022-2024 by Harald Welte <lafo...@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
@@ -21,6 +21,8 @@
 from construct import Optional as COptional
 from construct import *
 from bidict import bidict
+from Cryptodome.Random import get_random_bytes
+from pySim.global_platform.scp02 import SCP02
 from pySim.construct import *
 from pySim.utils import *
 from pySim.filesystem import *
@@ -582,6 +584,48 @@
                     p2 |= 0x01
             return grd_list

+        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)
+            init_update_apdu = 
scp02.gen_init_update_apdu(host_challenge=host_challenge)
+            init_update_resp, sw = 
self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
+            scp02.parse_init_update_resp(h2b(init_update_resp))
+            ext_auth_apdu = scp02.gen_ext_auth_apdu(opts.security_level)
+            ext_auth_resp, sw = 
self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
+            self._cmd.poutput("Successfully established a SCP02 secure 
channel")
+            # store a reference to the SCP instance
+            self._cmd.lchan.scc.scp = scp02
+            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 +645,22 @@
 #
 #    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))
diff --git a/pySim/global_platform/scp02.py b/pySim/global_platform/scp02.py
new file mode 100644
index 0000000..eec1180
--- /dev/null
+++ b/pySim/global_platform/scp02.py
@@ -0,0 +1,242 @@
+# Global Platform SCP02 (Secure Channel Protocol) implementation
+#
+# (C) 2023-2024 by Harald Welte <lafo...@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 Cryptodome.Cipher import DES3, DES
+from Cryptodome.Util.strxor import strxor
+from construct import *
+from pySim.utils import b2h
+from pySim.secure_channel import SecureChannel
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+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)
+
+# FIXME: overlap 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
+
+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
diff --git a/pySim/secure_channel.py b/pySim/secure_channel.py
new file mode 100644
index 0000000..974780e
--- /dev/null
+++ b/pySim/secure_channel.py
@@ -0,0 +1,37 @@
+# Generic code related to Secure Channel processing
+#
+# (C) 2023-2024 by Harald Welte <lafo...@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
diff --git a/tests/test_globalplatform.py b/tests/test_globalplatform.py
new file mode 100644
index 0000000..280199f
--- /dev/null
+++ b/tests/test_globalplatform.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+# (C) 2023-2024 by Harald Welte <lafo...@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.scp02 import SCP02
+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')
+
+if __name__ == "__main__":
+       unittest.main()

--
To view, visit https://gerrit.osmocom.org/c/pysim/+/35462?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings

Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27
Gerrit-Change-Number: 35462
Gerrit-PatchSet: 10
Gerrit-Owner: laforge <lafo...@osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: dexter <pma...@sysmocom.de>
Gerrit-Reviewer: fixeria <vyanits...@sysmocom.de>
Gerrit-Reviewer: laforge <lafo...@osmocom.org>
Gerrit-MessageType: merged

Reply via email to