laforge has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/35462?usp=email )
Change subject: WIP: GlobalPlatform SCP02 implementation ...................................................................... WIP: GlobalPlatform SCP02 implementation Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27 --- M pySim/global_platform.py A pySim/scp02.py A tests/test_globalplatform.py 3 files changed, 280 insertions(+), 0 deletions(-) git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/62/35462/1 diff --git a/pySim/global_platform.py b/pySim/global_platform.py index e04e845..b782492 100644 --- a/pySim/global_platform.py +++ b/pySim/global_platform.py @@ -343,3 +343,20 @@ # # 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): + 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/scp02.py b/pySim/scp02.py new file mode 100644 index 0000000..2449f2e --- /dev/null +++ b/pySim/scp02.py @@ -0,0 +1,189 @@ +# Global Platform SCP02 (Secure Channel Protocol) implementation +# +# (C) 2023 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/>. + +from pySim.utils import b2h, h2b +from pySim.global_platform import GpCardKeyset +from Cryptodome.Cipher import DES3, DES +from Cryptodome.Util.strxor import strxor +from construct import * + +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) + b'\x00' * 12 + cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8) + return cipher.encrypt(derivation_data) + +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) + print("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)]))) + print("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 SCP02: + """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)) + + def __init__(self, card_keys: GpCardKeyset, lchan_nr: int = 0): + self.lchan_nr = lchan_nr + self.card_keys = card_keys + self.sk = None + self.mac_on_unmodified = False + self.key_ver = 0x70 + + 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 _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes): + print("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) + card_challenge + host_challenge) + self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2) + card_challenge) + print("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.key_ver, 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) + print(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") + + @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 + + def gen_ext_auth_apdu(self, security_level: int =0x01) -> bytes: + """Generate EXTERNAL AUTHENTICATE APDU.""" + 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) + + print("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 diff --git a/tests/test_globalplatform.py b/tests/test_globalplatform.py new file mode 100644 index 0000000..b47c67b --- /dev/null +++ b/tests/test_globalplatform.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +# (C) 2023 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.scp02 import * +from pySim.utils import b2h, h2b + +KIC = h2b('100102030405060708090a0b0c0d0e0f') # enc +KID = h2b('101102030405060708090a0b0c0d0e0f') # MAC +KIK = h2b('102102030405060708090a0b0c0d0e0f') # DEK +ck_3des_70 = GpCardKeyset(0x70, 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(), '805070000840A62C37FA6304F8') + 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(), '805070000840A62C37FA6304F8') + 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') -- 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: 1 Gerrit-Owner: laforge <lafo...@osmocom.org> Gerrit-MessageType: newchange