aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarald Welte <laforge@osmocom.org>2024-02-02 22:56:35 +0100
committerHarald Welte <laforge@osmocom.org>2024-02-04 15:11:08 +0100
commit890e1951fef767dd1699a66624a895e930d86d15 (patch)
tree698a423034a2c1281f6f11d33862ee9a68622656
parentdd6895ff061decc928d365e4073dfa94713dbfef (diff)
Implement Global Platform SCP03laforge/scp
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
-rw-r--r--docs/shell.rst6
-rw-r--r--pySim/global_platform/__init__.py34
-rw-r--r--pySim/global_platform/scp.py279
-rw-r--r--tests/test_globalplatform.py141
4 files changed, 448 insertions, 12 deletions
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 @@ 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 3ca22af..ba34db8 100644
--- a/pySim/global_platform/__init__.py
+++ b/pySim/global_platform/__init__.py
@@ -20,9 +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
+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 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 @@ 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 d50948f..62eb43e 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()