laforge has uploaded this change for review.

View Change

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 <laforge@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 <laforge@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 change 35462. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27
Gerrit-Change-Number: 35462
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge@osmocom.org>
Gerrit-MessageType: newchange