From 890e1951fef767dd1699a66624a895e930d86d15 Mon Sep 17 00:00:00 2001 From: Harald Welte Date: Fri, 2 Feb 2024 22:56:35 +0100 Subject: [PATCH] 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 --- docs/shell.rst | 6 + pySim/global_platform/__init__.py | 34 +++- pySim/global_platform/scp.py | 279 +++++++++++++++++++++++++++++- tests/test_globalplatform.py | 141 ++++++++++++++- 4 files changed, 448 insertions(+), 12 deletions(-) diff --git a/docs/shell.rst b/docs/shell.rst index 5288fc68..a0115427 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -989,6 +989,12 @@ establish_scp02 :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 3ca22aff..ba34db8d 100644 --- a/pySim/global_platform/__init__.py +++ b/pySim/global_platform/__init__.py @@ -20,9 +20,10 @@ along with this program. If not, see . 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 @@ class ADF_SD(CardADF): 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 023e7a76..ee0f8da2 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 # @@ -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 @@ def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> byte 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 @@ class SCP(SecureChannel, abc.ABC): 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 @@ class SCP(SecureChannel, abc.ABC): 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 @@ class SCP(SecureChannel, abc.ABC): """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 @@ class SCP02(SCP): 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 d50948f4..62eb43e3 100644 --- a/tests/test_globalplatform.py +++ b/tests/test_globalplatform.py @@ -19,7 +19,7 @@ import unittest 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 @@ class SCP02_Test(unittest.TestCase): 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()