# Global Platform SCP02 (Secure Channel Protocol) implementation # # (C) 2023-2024 by Harald Welte # # 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 . import abc import logging from Cryptodome.Cipher import DES3, DES from Cryptodome.Util.strxor import strxor from construct import * from pySim.utils import b2h from pySim.secure_channel import SecureChannel from typing import Optional logger = logging.getLogger(__name__) 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, 'big') + b'\x00' * 12 cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8) return cipher.encrypt(derivation_data) # FIXME: overlap 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 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) logger.debug("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)]))) logger.debug("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 SCP(SecureChannel, abc.ABC): """Abstract base class containing some common interface + functionality for SCP protocols.""" def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0): if hasattr(self, 'kvn_range'): if not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1): raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' % (self.__class__.__name__, self.kvn_range[0], self.kvn_range[1])) self.lchan_nr = lchan_nr self.card_keys = card_keys self.sk = None 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) 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 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, *args, **kwargs) else: return apdu @abc.abstractmethod def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes: """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.""" 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)) kvn_range = [0x20, 0x2f] def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes): logger.debug("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, 'big') + card_challenge + host_challenge) self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge) logger.debug("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.card_keys.kvn, 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) logger.debug(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") def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes: """Generate EXTERNAL AUTHENTICATE APDU.""" if security_level & 0xf0: raise NotImplementedError('R-MAC/R-ENC for SCP02 not implemented yet.') 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) logger.debug("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 def unwrap_rsp_apdu(self, sw: bytes, apdu: bytes) -> bytes: # TODO: Implement R-MAC / R-ENC return apdu