| # Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation |
| # |
| # (C) 2023-2024 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 abc |
| import logging |
| from typing import Optional |
| from Cryptodome.Cipher import DES3, DES |
| from Cryptodome.Util.strxor import strxor |
| from construct import Struct, Bytes, Int8ub, Int16ub, Const |
| from construct import Optional as COptional |
| from pySim.utils import b2h, bertlv_parse_len, bertlv_encode_len |
| 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 |
| 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) |
| |
| # 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' |
| DERIV_CONST_RMAC = b'\x01\x02' |
| DERIV_CONST_ENC = b'\x01\x82' |
| DERIV_CONST_DENC = b'\x01\x81' |
| blocksize = 8 |
| |
| 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) |
| 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 |
| |
| def encrypt_key(self, key: bytes) -> bytes: |
| """Encrypt a key with the DEK.""" |
| num_pad = len(key) % self.sk.blocksize |
| if num_pad: |
| return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad) |
| return self.dek_encrypt(key) |
| |
| def decrypt_key(self, encrypted_key:bytes) -> bytes: |
| """Decrypt a key with the DEK.""" |
| if len(encrypted_key) % self.sk.blocksize: |
| # If the length of the Key Component Block is not a multiple of the block size of the encryption # |
| # algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that the key |
| # component value was right-padded prior to encryption and that the Key Component Block was |
| # formatted as described in Table 11-70. In this case, the first byte(s) of the Key Component |
| # Block provides the actual length of the key component value, which allows recovering the |
| # clear-text key component value after decryption of the encrypted key component value and removal |
| # of padding bytes. |
| decrypted = self.dek_decrypt(encrypted_key) |
| key_len, remainder = bertlv_parse_len(decrypted) |
| return remainder[:key_len] |
| else: |
| # If the length of the Key Component Block is a multiple of the block size of the encryption |
| # algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding |
| # bytes were added before encrypting the key component value and that the Key Component Block is |
| # only composed of the encrypted key component value (as shown in Table 11-71). In this case, the |
| # clear-text key component value is simply recovered by decrypting the Key Component Block. |
| return self.dek_decrypt(encrypted_key) |
| |
| @abc.abstractmethod |
| def dek_encrypt(self, plaintext:bytes) -> bytes: |
| pass |
| |
| @abc.abstractmethod |
| def dek_decrypt(self, ciphertext:bytes) -> 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 __init__(self, *args, **kwargs): |
| self.overhead = 8 |
| super().__init__(*args, **kwargs) |
| |
| def dek_encrypt(self, plaintext:bytes) -> bytes: |
| cipher = DES.new(self.card_keys.dek, DES.MODE_ECB) |
| return cipher.encrypt(plaintext) |
| |
| def dek_decrypt(self, ciphertext:bytes) -> bytes: |
| cipher = DES.new(self.card_keys.dek, DES.MODE_ECB) |
| return cipher.decrypt(ciphertext) |
| |
| 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, *args, **kwargs) -> 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, rsp_apdu: bytes) -> bytes: |
| # TODO: Implement R-MAC / R-ENC |
| return rsp_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 is 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) |
| self.overhead = self.s_mode |
| super().__init__(*args, **kwargs) |
| |
| def dek_encrypt(self, plaintext:bytes) -> bytes: |
| cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16) |
| return cipher.encrypt(plaintext) |
| |
| def dek_decrypt(self, ciphertext:bytes) -> bytes: |
| cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16) |
| return cipher.decrypt(ciphertext) |
| |
| 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 is 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, rsp_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, rsp_apdu=%s)", sw, rsp_apdu) |
| if not self.do_rmac: |
| assert not self.do_renc |
| return rsp_apdu |
| |
| if sw != b'\x90\x00' and sw[0] not in [0x62, 0x63]: |
| return rsp_apdu |
| response_data = rsp_apdu[:-self.s_mode] |
| rmac = rsp_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 |