mirror of https://gerrit.osmocom.org/pysim
535 lines
24 KiB
Python
535 lines
24 KiB
Python
# 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
|