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
diff --git a/docs/shell.rst b/docs/shell.rst
index 5288fc6..a011542 100644
--- a/docs/shell.rst
+++ b/docs/shell.rst
@@ -989,6 +989,12 @@
    :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)
diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py
index 3ca22af..ba34db8 100644
--- a/pySim/global_platform/__init__.py
+++ b/pySim/global_platform/__init__.py
@@ -20,9 +20,10 @@
 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
+from pySim.global_platform.scp import SCP02, SCP03
 from pySim.construct import *
 from pySim.utils import *
 from pySim.filesystem import *
@@ -692,16 +693,37 @@
             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)
+            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))
-            scp02.parse_init_update_resp(h2b(init_update_resp))
-            ext_auth_apdu = scp02.gen_ext_auth_apdu(opts.security_level)
+            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 SCP02 secure channel")
+            self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
             # store a reference to the SCP instance
-            self._cmd.lchan.scc.scp = scp02
+            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:
diff --git a/pySim/global_platform/scp.py b/pySim/global_platform/scp.py
index 023e7a7..ee0f8da 100644
--- a/pySim/global_platform/scp.py
+++ b/pySim/global_platform/scp.py
@@ -1,4 +1,4 @@
-# Global Platform SCP02 (Secure Channel Protocol) implementation
+# Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation
 #
 # (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
 #
@@ -17,13 +17,16 @@
 
 import abc
 import logging
+from typing import Optional
 from Cryptodome.Cipher import DES3, DES
 from Cryptodome.Util.strxor import strxor
-from construct import *
+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)
@@ -34,12 +37,21 @@
     cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8)
     return cipher.encrypt(derivation_data)
 
-# FIXME: overlap with BspAlgoCryptAES128
+# 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'
@@ -108,6 +120,26 @@
         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)
 
@@ -117,10 +149,11 @@
             ret = ret | CLA_SM
         return ret + self.lchan_nr
 
-    def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
+    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)
+            return self._wrap_cmd_apdu(apdu, *args, **kwargs)
         else:
             return apdu
 
@@ -129,6 +162,18 @@
         """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."""
@@ -206,3 +251,227 @@
     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 counter’s 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
diff --git a/tests/test_globalplatform.py b/tests/test_globalplatform.py
index d50948f..62eb43e 100644
--- a/tests/test_globalplatform.py
+++ b/tests/test_globalplatform.py
@@ -19,7 +19,7 @@
 import logging
 
 from pySim.global_platform import *
-from pySim.global_platform.scp import SCP02
+from pySim.global_platform.scp import *
 from pySim.utils import b2h, h2b
 
 KIC = h2b('100102030405060708090a0b0c0d0e0f') # enc
@@ -64,5 +64,144 @@
         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()