diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py index 9600c7cb..bc582043 100644 --- a/pySim/global_platform/__init__.py +++ b/pySim/global_platform/__init__.py @@ -31,7 +31,7 @@ from osmocom.tlv import * from osmocom.construct import * from pySim.utils import ResTuple from pySim.card_key_provider import card_key_provider_get_field -from pySim.global_platform.scp import SCP02, SCP03 +from pySim.global_platform.scp import SCP, SCP02, SCP03 from pySim.global_platform.install_param import gen_install_parameters from pySim.filesystem import * from pySim.profile import CardProfile @@ -607,6 +607,241 @@ class ADF_SD(CardADF): def decode_select_response(self, data_hex: str) -> object: return decode_select_response(data_hex) + @staticmethod + def store_data(scc: SimCardCommands, data: bytes, structure:str = 'none', encryption:str = 'none', + response_permitted: bool = False) -> bytes: + """ + Perform the GlobalPlatform STORE DATA command in order to store some card-specific data. + See GlobalPlatform CardSpecification v2.3 Section 11.11 for details. + """ + max_cmd_len =scc.max_cmd_len + # Table 11-89 of GP Card Specification v2.3 + remainder = data + block_nr = 0 + response = '' + while len(remainder): + chunk = remainder[:max_cmd_len] + remainder = remainder[max_cmd_len:] + p1b = build_construct(ADF_SD.StoreData, + {'last_block': len(remainder) == 0, 'encryption': encryption, + 'structure': structure, 'response': response_permitted}) + hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk)) + data, _sw =scc.send_apdu_checksw(hdr + b2h(chunk) + "00") + block_nr += 1 + response += data + return h2b(response) + + @staticmethod + def get_data(scc: SimCardCommands, tag: int) -> bytes: + (data, _sw) = scc.get_data(cla=0x80, tag=tag) + return data + + # Table 11-68: Key Data Field - Format 1 (Basic Format). The key component block length is + # BER-TLV coded (Section 11.8.2.3.1), the key check value length is always '00' - '7F'. + KeyDataBasic = Struct('key_type'/KeyType, + 'kcb'/Prefixed(PutKeyLength(), GreedyBytes), + 'kcv'/Prefixed(Int8ub, GreedyBytes)) + + @staticmethod + def encode_key_data_basic(key_type: str, kcb: bytes, kcv: bytes) -> bytes: + """Generic Basic key data field, GP CardSpec v2.3 Table 11-68): + tag || L1 || KCB || <1-byte length> KCV""" + return ADF_SD.KeyDataBasic.build({'key_type': key_type, 'kcb': kcb, 'kcv': kcv}) + + @staticmethod + def encode_key_data_psk(clear_key: bytes, ciphered_key: bytes, kcv: bytes) -> bytes: + """Single PSK TLS '85' key data field per GP Amendment B 1.2, 3.9.1 / Table 3-13: + 85 | L1 | | | + - framing is like Basic Format, but the kcb is always GP CardSpec Table 11-70 + so always with the length of the clear text key value, even without padding! + - 'ciphered_key' is DEK(block-padded clear key), no additional length prefix.""" + kcb = bertlv_encode_len(len(clear_key)) + ciphered_key + return ADF_SD.encode_key_data_basic('tls_psk', kcb, kcv) + + @staticmethod + def build_put_key_data(kvn: int, keys: List[dict], scp) -> bytes: + """Assemble the PUT KEY data field, mixed PSK + DES DEK is supported: + - new KVN followed by one key data field per key. + - tls_psk keys per GP Amendment B + - other key types generic Basic format + Param 'keys' is a dict: + - 'key_type' (str) + - 'clear_key' (bytes) + - 'kcv' (bytes / empty). + 'scp' may be None (e.g. during personalization, when the DEK may not be required).""" + key_data = kvn.to_bytes(1, 'big') + for k in keys: + clear = k['clear_key'] + if k['key_type'] == 'tls_psk': + # len always part of the data see CardSpec Table 11-70 vs Table 11-71 + if scp: + ciphered = scp.dek_encrypt(scp.pad_to_blocksize(clear)) + else: + ciphered = clear + key_data += ADF_SD.encode_key_data_psk(clear, ciphered, k['kcv']) + else: + if scp: + ciphered = scp.encrypt_key(clear) + else: + # (for example) during personalization, DEK might not be required + ciphered = clear + key_data += ADF_SD.encode_key_data_basic(k['key_type'], ciphered, k['kcv']) + return key_data + + @staticmethod + def put_key(scc: SimCardCommands, old_kvn:int, kvn: int, kid: int, keys: List[dict]) -> bytes: + """Perform the GlobalPlatform PUT KEY command in order to store a new key on the card. + See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.""" + key_data = ADF_SD.build_put_key_data(kvn, keys, scc.scp) + # Lc of Table 11-64 is a single byte, while LOAD or STORE DATA splits we can't: + # 11.8.2.3.3 splits a key at component boundaries -> not helping here + max_cmd_len = scc.max_cmd_len + if len(key_data) > max_cmd_len: + raise ValueError('key data field of %u bytes exceeds the maximum command length of %u ' + '(limited by the overhead of the current secure channel); use fewer ' + 'keys per command, a single key component that large needs STORE DATA' % + (len(key_data), max_cmd_len)) + hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data)) + data, _sw = scc.send_apdu_checksw(hdr + b2h(key_data) + "00") + return data + + @staticmethod + def gp_version(scc: SimCardCommands) -> Optional[Tuple[int, ...]]: + """GP version the selected SD reports in its Card Recognition + Data, e.g. (2, 1, 1). Card Recognition Data "shall be present" v2.1.1/v2.3.1 section 7.4.1.3, + so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown. + Cached, it cannot change during a session.""" + version = None + try: + data, _sw = scc.get_data(cla=0x80, tag=CardData.tag) + version = decode_gp_version(h2b(data)) + log.debug("Card Recognition Data reports GlobalPlatform %s", + '.'.join(str(v) for v in version) if version else 'unknown') + except (SwMatchError, ValueError) as e: + log.warning("Could not determine GlobalPlatform version: %s", e) + return version + + @staticmethod + def get_status(scc: SimCardCommands, subset:str, aid_search_qualifier:Hexstr = '', + version:Optional[Tuple[int, ...]] = None) -> List[GpRegistryRelatedData]: + aid = ApplicationAID(decoded=aid_search_qualifier) + # GPC CardSpec v2.3.1 Table 11-35 says only the AID search tag is mandatory, tag list is + # Optional and not present in the older v2.1.1, where section 9.4.2.3 defines the data + # field as the search qualifier. + # Cards like the sja5 implementing that old GP version reject anything else with 6A80 + # from v2.1.1 Table 9-26 so only send a tag list to a card that announces v2.2 or later. + # + # Not sending one is not a problem on older cards, the tag list only gives us data beyond + # what 11.4.3.1 gives us anyway, for example the associated SD AID which matters on an eUICC + # where entries belong to different SD. + if version is not None and version >= (2, 2): + try: + return ADF_SD._get_status(scc, subset, aid.to_tlv() + get_status_tag_list(subset)) + except SwMatchError as e: + # Retry if v2.2 or later but rejected the tag list anyway. + # 6A80 and 6A88 are the error conditions GET STATUS defines in table 11-39. + # Retrying beats not ending up with a list again... + if e.sw_actual not in ('6a80', '6a88'): + raise + log.warning("Card reports GlobalPlatform %s but answered %s to the GET STATUS tag list; " + "retrying with the default search", + '.'.join(str(v) for v in version), e.sw_actual) + return ADF_SD._get_status(scc, subset, aid.to_tlv(), empty_on_6a88=True) + + @staticmethod + def _get_status(scc: SimCardCommands, subset:str, cmd_data:bytes, + empty_on_6a88: bool = False) -> List[GpRegistryRelatedData]: + subset_hex = b2h(build_construct(StatusSubset, subset)) + p2 = 0x02 # GPC v2.3.1 11.4.2.2 table 11-34, b2: response data structure per table 11-36 + grd_list = [] + while True: + hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data)) + data, sw = scc.send_apdu(hdr + b2h(cmd_data) + "00") + if sw == '6a88': + # Table 11-39 "Referenced data not found". After collecting all pages this can + # only mean "nothing more matches" -> listing is complete. On the first page + # it is ambiguous, empty result or bad command data field, so leave that to get_status() + # which knows if a tag list was sent. + if grd_list or empty_on_6a88: + return grd_list + raise SwMatchError(sw, ['9000', '6310']) + if sw not in ['9000', '6310']: + # Never return a silently truncated registry + raise SwMatchError(sw, ['9000', '6310']) + remainder = h2b(data) + while len(remainder): + # tlv sequence, each element is one GpRegistryRelatedData() + grd = GpRegistryRelatedData() + _dec, remainder = grd.from_tlv(remainder) + grd_list.append(grd) + if sw == '9000': + return grd_list + # 6310 = more data available, table 11-38: reissue as get next occurrence(s), b1 of + # table 11-34. Keeps b2 unchanged. + p2 |= 0x01 + + @staticmethod + def set_status(scc: SimCardCommands, scope:str, status:str, aid:Hexstr = ''): + SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte), + 'scope'/SetStatusScope, 'status'/CLifeCycleState, + 'aid'/Prefixed(Int8ub, COptional(GreedyBytes))) + apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid}) + _data, _sw =scc.send_apdu_checksw(b2h(apdu)) + + @staticmethod + def install(scc: SimCardCommands, p1:int, p2:int, data:Hexstr) -> ResTuple: + cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data) + return scc.send_apdu_checksw(cmd_hex) + + @staticmethod + def delete(scc: SimCardCommands, p1:int, p2:int, data:Hexstr) -> ResTuple: + cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data) + return scc.send_apdu_checksw(cmd_hex) + + @staticmethod + def load(scc: SimCardCommands, contents:bytes, chunk_len:Optional[int] = None): + # scc.max_cmd_len knows the overhead the currently active SCP + # 240 is the old default, keep it for now. + max_chunk_len = scc.max_cmd_len + if chunk_len is None: + chunk_len = min(240, max_chunk_len) + elif not 1 <= chunk_len <= max_chunk_len: + raise ValueError('chunk_len must be in range 1..%u (limited by the overhead of the current secure channel)' % + max_chunk_len) + # build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case + remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents + # transfer this in various chunks to the card + total_size = len(remainder) + block_nr = 0 + while len(remainder): + block = remainder[:chunk_len] + remainder = remainder[chunk_len:] + # build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56 + p1 = 0x00 if len(remainder) else 0x80 + p2 = block_nr % 256 + block_nr += 1 + cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block)) + _rsp_hex, _sw = scc.send_apdu_checksw(cmd_hex) + log.info("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!", + total_size, block_nr) + + @staticmethod + def establish_scp(scc: SimCardCommands, scp: SCP, host_challenge: Optional[bytes] = None, + security_level: int = 0x01): + # perform the common functionality shared by SCP02 and SCP03 establishment + init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge) + init_update_resp, _sw =scc.send_apdu_checksw(b2h(init_update_apdu)) + scp.parse_init_update_resp(h2b(init_update_resp)) + ext_auth_apdu = scp.gen_ext_auth_apdu(security_level) + _ext_auth_resp, _sw =scc.send_apdu_checksw(b2h(ext_auth_apdu)) + log.info("Successfully established a %s secure channel", str(scp)) + # store a reference to the SCP instance + scc.scp = scp + + @staticmethod + def release_scp(scc: SimCardCommands): + scc.scp = None + @with_default_category('Application-Specific Commands') class AddlShellCommands(CommandSet): get_data_parser = argparse.ArgumentParser() @@ -624,7 +859,8 @@ class ADF_SD(CardADF): self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name, do_names)) return - (data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag) + + data = ADF_SD.get_data(self._cmd.lchan.scc, tag=tlv_cls.tag) ie = tlv_cls() ie.from_tlv(h2b(data)) self._cmd.poutput_json(ie.to_dict()) @@ -645,27 +881,8 @@ class ADF_SD(CardADF): """Perform the GlobalPlatform STORE DATA command in order to store some card-specific data. See GlobalPlatform CardSpecification v2.3 Section 11.11 for details.""" response_permitted = opts.response == 'may_be_returned' - self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted) - - def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes: - """Perform the GlobalPlatform STORE DATA command in order to store some card-specific data. - See GlobalPlatform CardSpecification v2.3 Section 11.11 for details.""" - max_cmd_len = self._cmd.lchan.scc.max_cmd_len - # Table 11-89 of GP Card Specification v2.3 - remainder = data - block_nr = 0 - response = '' - while len(remainder): - chunk = remainder[:max_cmd_len] - remainder = remainder[max_cmd_len:] - p1b = build_construct(ADF_SD.StoreData, - {'last_block': len(remainder) == 0, 'encryption': encryption, - 'structure': structure, 'response': response_permitted}) - hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk)) - data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00") - block_nr += 1 - response += data - return h2b(response) + ADF_SD.store_data(self._cmd.lchan.scc, h2b(opts.DATA), opts.data_structure, opts.encryption, + response_permitted) put_key_parser = argparse.ArgumentParser() put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number') @@ -709,75 +926,8 @@ class ADF_SD(CardADF): p2 = opts.key_id if len(opts.key_type) > 1: p2 |= 0x80 - self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb) + ADF_SD.put_key(self._cmd.lchan.scc, opts.old_key_version_nr, opts.key_version_nr, p2, kdb) - # Table 11-68: Key Data Field - Format 1 (Basic Format). The key component block length is - # BER-TLV coded (Section 11.8.2.3.1), the key check value length is always '00' - '7F'. - KeyDataBasic = Struct('key_type'/KeyType, - 'kcb'/Prefixed(PutKeyLength(), GreedyBytes), - 'kcv'/Prefixed(Int8ub, GreedyBytes)) - - @classmethod - def encode_key_data_basic(cls, key_type: str, kcb: bytes, kcv: bytes) -> bytes: - """Generic Basic key data field, GP CardSpec v2.3 Table 11-68): - tag || L1 || KCB || <1-byte length> KCV""" - return cls.KeyDataBasic.build({'key_type': key_type, 'kcb': kcb, 'kcv': kcv}) - - @classmethod - def encode_key_data_psk(cls, clear_key: bytes, ciphered_key: bytes, kcv: bytes) -> bytes: - """Single PSK TLS '85' key data field per GP Amendment B 1.2, 3.9.1 / Table 3-13: - 85 | L1 | | | - - framing is like Basic Format, but the kcb is always GP CardSpec Table 11-70 - so always with the length of the clear text key value, even without padding! - - 'ciphered_key' is DEK(block-padded clear key), no additional length prefix.""" - kcb = bertlv_encode_len(len(clear_key)) + ciphered_key - return cls.encode_key_data_basic('tls_psk', kcb, kcv) - - @classmethod - def build_put_key_data(cls, kvn: int, keys: List[dict], scp) -> bytes: - """Assemble the PUT KEY data field, mixed PSK + DES DEK is supported: - - new KVN followed by one key data field per key. - - tls_psk keys per GP Amendment B - - other key types generic Basic format - Param 'keys' is a dict: - - 'key_type' (str) - - 'clear_key' (bytes) - - 'kcv' (bytes / empty). - 'scp' may be None (e.g. during personalization, when the DEK may not be required).""" - key_data = kvn.to_bytes(1, 'big') - for k in keys: - clear = k['clear_key'] - if k['key_type'] == 'tls_psk': - # len always part of the data see CardSpec Table 11-70 vs Table 11-71 - if scp: - ciphered = scp.dek_encrypt(scp.pad_to_blocksize(clear)) - else: - ciphered = clear - key_data += cls.encode_key_data_psk(clear, ciphered, k['kcv']) - else: - if scp: - ciphered = scp.encrypt_key(clear) - else: - # (for example) during personalization, DEK might not be required - ciphered = clear - key_data += cls.encode_key_data_basic(k['key_type'], ciphered, k['kcv']) - return key_data - - def put_key(self, old_kvn:int, kvn: int, kid: int, keys: List[dict]) -> bytes: - """Perform the GlobalPlatform PUT KEY command in order to store a new key on the card. - See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.""" - key_data = self.build_put_key_data(kvn, keys, self._cmd.lchan.scc.scp) - # Lc of Table 11-64 is a single byte, while LOAD or STORE DATA splits we can't: - # 11.8.2.3.3 splits a key at component boundaries -> not helping here - max_cmd_len = self._cmd.lchan.scc.max_cmd_len - if len(key_data) > max_cmd_len: - raise ValueError('key data field of %u bytes exceeds the maximum command length of %u ' - '(limited by the overhead of the current secure channel); use fewer ' - 'keys per command, a single key component that large needs STORE DATA' % - (len(key_data), max_cmd_len)) - hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data)) - data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data) + "00") - return data get_status_parser = argparse.ArgumentParser() get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()), @@ -789,7 +939,7 @@ class ADF_SD(CardADF): def do_get_status(self, opts): """Perform GlobalPlatform GET STATUS command in order to retrieve status information on Issuer Security Domain, Executable Load File, Executable Module or Applications.""" - grd_list = self.get_status(opts.subset, opts.aid) + grd_list = ADF_SD.get_status(self._cmd.lchan.scc, opts.subset, opts.aid, self.gp_version()) for grd in grd_list: self._cmd.poutput_json(grd.to_dict()) @@ -799,73 +949,9 @@ class ADF_SD(CardADF): so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown. Cached, it cannot change during a session.""" if not hasattr(self, '_gp_version'): - self._gp_version = None - try: - data, _sw = self._cmd.lchan.scc.get_data(cla=0x80, tag=CardData.tag) - self._gp_version = decode_gp_version(h2b(data)) - except (SwMatchError, ValueError) as e: - log.warning("Could not determine GlobalPlatform version: %s", e) + self._gp_version = ADF_SD.gp_version(self._cmd.lchan.scc) return self._gp_version - def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]: - aid = ApplicationAID(decoded=aid_search_qualifier) - # GPC CardSpec v2.3.1 Table 11-35 says only the AID search tag is mandatory, tag list is - # Optional and not present in the older v2.1.1, where section 9.4.2.3 defines the data - # field as the search qualifier. - # Cards like the sja5 implementing that old GP version reject anything else with 6A80 - # from v2.1.1 Table 9-26 so only send a tag list to a card that announces v2.2 or later. - # - # Not sending one is not a problem on older cards, the tag list only gives us data beyond - # what 11.4.3.1 gives us anyway, for example the associated SD AID which matters on an eUICC - # where entries belong to different SD. - version = self.gp_version() - log.debug("Card Recognition Data reports GlobalPlatform %s", - '.'.join(str(v) for v in version) if version else 'unknown') - if version is not None and version >= (2, 2): - try: - return self._get_status(subset, aid.to_tlv() + get_status_tag_list(subset)) - except SwMatchError as e: - # Retry if v2.2 or later but rejected the tag list anyway. - # 6A80 and 6A88 are the error conditions GET STATUS defines in table 11-39. - # Retrying beats not ending up with a list again... - if e.sw_actual not in ('6a80', '6a88'): - raise - log.warning("Card reports GlobalPlatform %s but answered %s to the GET STATUS tag list; " - "retrying with the default search", - '.'.join(str(v) for v in version), e.sw_actual) - return self._get_status(subset, aid.to_tlv(), empty_on_6a88=True) - - def _get_status(self, subset:str, cmd_data:bytes, - empty_on_6a88: bool = False) -> List[GpRegistryRelatedData]: - subset_hex = b2h(build_construct(StatusSubset, subset)) - p2 = 0x02 # GPC v2.3.1 11.4.2.2 table 11-34, b2: response data structure per table 11-36 - grd_list = [] - while True: - hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data)) - data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data) + "00") - if sw == '6a88': - # Table 11-39 "Referenced data not found". After collecting all pages this can - # only mean "nothing more matches" -> listing is complete. On the first page - # it is ambiguous, empty result or bad command data field, so leave that to get_status() - # which knows if a tag list was sent. - if grd_list or empty_on_6a88: - return grd_list - raise SwMatchError(sw, ['9000', '6310']) - if sw not in ['9000', '6310']: - # Never return a silently truncated registry - raise SwMatchError(sw, ['9000', '6310']) - remainder = h2b(data) - while len(remainder): - # tlv sequence, each element is one GpRegistryRelatedData() - grd = GpRegistryRelatedData() - _dec, remainder = grd.from_tlv(remainder) - grd_list.append(grd) - if sw == '9000': - return grd_list - # 6310 = more data available, table 11-38: reissue as get next occurrence(s), b1 of - # table 11-34. Keeps b2 unchanged. - p2 |= 0x01 - set_status_parser = argparse.ArgumentParser() set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()), help='Defines the scope of the requested status change') @@ -879,14 +965,7 @@ class ADF_SD(CardADF): """Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the Issuer Security Domain, Supplementary Security Domain or Application. This normally requires prior authentication with a Secure Channel Protocol.""" - self.set_status(opts.scope, opts.status, opts.aid) - - def set_status(self, scope:str, status:str, aid:Hexstr = ''): - SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte), - 'scope'/SetStatusScope, 'status'/CLifeCycleState, - 'aid'/Prefixed(Int8ub, COptional(GreedyBytes))) - apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid}) - _data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu)) + ADF_SD.set_status(self._cmd.lchan.scc, opts.scope, opts.status, opts.aid) inst_perso_parser = argparse.ArgumentParser() inst_perso_parser.add_argument('application_aid', type=is_hexstr, help='Application AID') @@ -896,7 +975,8 @@ class ADF_SD(CardADF): """Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security Domain that the following STORE DATA commands are meant for a specific AID (specified here).""" # Section 11.5.2.3.6 / Table 11-47 - self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid)) + ADF_SD.install(self._cmd.lchan.scc, 0x20, 0x00, "0000%02x%s000000" % + (len(opts.application_aid)//2, opts.application_aid)) inst_inst_parser = argparse.ArgumentParser() inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='', @@ -931,7 +1011,7 @@ class ADF_SD(CardADF): # convert from list to "true-dict" as required by construct.FlagsEnum decoded['privileges'] = {x: True for x in decoded['privileges']} ifi_bytes = build_construct(InstallForInstallCD, decoded) - self.install(p1, 0x00, b2h(ifi_bytes)) + ADF_SD.install(self._cmd.lchan.scc, p1, 0x00, b2h(ifi_bytes)) inst_load_parser = argparse.ArgumentParser() inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True, @@ -956,11 +1036,7 @@ class ADF_SD(CardADF): 'load_parameters'/Prefixed(Int8ub, GreedyBytes), 'load_token'/Prefixed(Int8ub, GreedyBytes)) ifl_bytes = build_construct(InstallForLoadCD, vars(opts)) - self.install(0x02, 0x00, b2h(ifl_bytes)) - - def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple: - cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data) - return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex) + ADF_SD.install(self._cmd.lchan.scc, 0x02, 0x00, b2h(ifl_bytes)) del_cc_parser = argparse.ArgumentParser() del_cc_parser.add_argument('aid', type=is_hexstr, @@ -974,7 +1050,7 @@ class ADF_SD(CardADF): File, an Application or an Executable Load File and its related Applications.""" p2 = 0x80 if opts.delete_related_objects else 0x00 aid = ApplicationAID(decoded=opts.aid) - self.delete(0x00, p2, b2h(aid.to_tlv())) + ADF_SD.delete(self._cmd.lchan.scc, 0x00, p2, b2h(aid.to_tlv())) del_key_parser = argparse.ArgumentParser() del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)') @@ -995,11 +1071,7 @@ class ADF_SD(CardADF): cmd += "d001%02x" % opts.key_id if opts.key_ver is not None: cmd += "d201%02x" % opts.key_ver - self.delete(0x00, p2, cmd) - - def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple: - cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data) - return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex) + ADF_SD.delete(self._cmd.lchan.scc, 0x00, p2, cmd) load_parser = argparse.ArgumentParser() load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True) @@ -1014,40 +1086,15 @@ class ADF_SD(CardADF): """Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and without ciphering.)""" if opts.from_hex is not None: - self.load(h2b(opts.from_hex), opts.chunk_len) + ADF_SD.load(self._cmd.lchan.scc, h2b(opts.from_hex), opts.chunk_len) elif opts.from_file is not None: - self.load(opts.from_file.read(), opts.chunk_len) + ADF_SD.load(self._cmd.lchan.scc, opts.from_file.read(), opts.chunk_len) elif opts.from_cap_file is not None: cap = CapFile(opts.from_cap_file) - self.load(cap.get_loadfile(), opts.chunk_len) + ADF_SD.load(self._cmd.lchan.scc, cap.get_loadfile(), opts.chunk_len) else: raise ValueError('load source not specified!') - def load(self, contents:bytes, chunk_len:Optional[int] = None): - # scc.max_cmd_len knows the overhead the currently active SCP - # 240 is the old default, keep it for now. - max_chunk_len = self._cmd.lchan.scc.max_cmd_len - if chunk_len is None: - chunk_len = min(240, max_chunk_len) - elif not 1 <= chunk_len <= max_chunk_len: - raise ValueError('chunk_len must be in range 1..%u (limited by the overhead of the current secure channel)' % - max_chunk_len) - # build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case - remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents - # transfer this in various chunks to the card - total_size = len(remainder) - block_nr = 0 - while len(remainder): - block = remainder[:chunk_len] - remainder = remainder[chunk_len:] - # build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56 - p1 = 0x00 if len(remainder) else 0x80 - p2 = block_nr % 256 - block_nr += 1 - cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block)) - _rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex) - self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!" % (total_size, block_nr)) - install_cap_parser = argparse.ArgumentParser(usage='%(prog)s FILE [--install-parameters | --install-parameters-*]') install_cap_parser.add_argument('cap_file', type=str, metavar='FILE', help='JAVA-CARD CAP file to install') @@ -1110,7 +1157,7 @@ class ADF_SD(CardADF): self._cmd.poutput("step #1: install for load...") self.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid)) self._cmd.poutput("step #2: load...") - self.load(load_file, opts.chunk_len) + ADF_SD.load(self._cmd.lchan.scc, load_file, opts.chunk_len) self._cmd.poutput("step #3: install_for_install (and make selectable)...") self.do_install_for_install("--load-file-aid %s --module-aid %s --application-aid %s --install-parameters %s --make-selectable" % (load_file_aid, module_aid, application_aid, install_parameters)) @@ -1150,7 +1197,7 @@ class ADF_SD(CardADF): host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8) kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek)) scp02 = SCP02(card_keys=kset) - self._establish_scp(scp02, host_challenge, opts.security_level) + ADF_SD.establish_scp(self._cmd.lchan.scc, scp02, host_challenge, opts.security_level) est_scp03_parser = deepcopy(est_scp02_parser) est_scp03_parser.description = None @@ -1178,27 +1225,15 @@ class ADF_SD(CardADF): host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode) kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek)) scp03 = SCP03(card_keys=kset, s_mode = s_mode) - self._establish_scp(scp03, host_challenge, opts.security_level) - - def _establish_scp(self, scp, host_challenge, security_level): - # perform the common functionality shared by SCP02 and SCP03 establishment - init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge) - init_update_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu)) - scp.parse_init_update_resp(h2b(init_update_resp)) - ext_auth_apdu = scp.gen_ext_auth_apdu(security_level) - _ext_auth_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu)) - self._cmd.poutput("Successfully established a %s secure channel" % str(scp)) - # store a reference to the SCP instance - self._cmd.lchan.scc.scp = scp + ADF_SD.establish_scp(self._cmd.lchan.scc, scp03, host_challenge, opts.security_level) self._cmd.update_prompt() - def do_release_scp(self, _opts): """Release a previously establiehed secure channel.""" if not self._cmd.lchan.scc.scp: self._cmd.poutput("Cannot release SCP as none is established") return - self._cmd.lchan.scc.scp = None + ADF_SD.release_scp(self._cmd.lchan.scc) self._cmd.update_prompt() diff --git a/tests/unittests/test_globalplatform.py b/tests/unittests/test_globalplatform.py index d329c457..1a320476 100644 --- a/tests/unittests/test_globalplatform.py +++ b/tests/unittests/test_globalplatform.py @@ -332,9 +332,6 @@ class PutKey_PSK_Test(unittest.TestCase): """Tests for the PUT KEY command data field encoding, in particular the PSK TLS ('85') key data field defined by GlobalPlatform Amendment B (Remote Application Management over HTTP) Table 3-13.""" - # the PUT KEY encoder we exercise - C = ADF_SD.AddlShellCommands - # SCP80 TLS-PSK example key from the do_put_key docstring (16 bytes) PSK_CLEAR = h2b('303132333435363738393a3b3c3d3e3f') # its DEK ciphertext + Table 3-13 KCV with SCP02 session set up below @@ -359,7 +356,7 @@ class PutKey_PSK_Test(unittest.TestCase): clear = self.PSK_CLEAR ciphered = h2b('aabbccddeeff00112233445566778899') # arbitrary 16-byte ciphertext kcv = hashlib.sha1(clear).digest()[:3] - field = self.C.encode_key_data_psk(clear, ciphered, kcv) + field = ADF_SD.encode_key_data_psk(clear, ciphered, kcv) # 85 L1 L2 <---------- ciphered -----------> 03 <-kcv-> self.assertEqual(b2h(field),'85' '11' '10' 'aabbccddeeff00112233445566778899' '03' + b2h(kcv)) self.assertEqual(b2h(field),'851110aabbccddeeff0011223344556677889903' + '06125d') @@ -368,15 +365,15 @@ class PutKey_PSK_Test(unittest.TestCase): # Full PUT KEY data field (KVN 0x40 + single PSK key) enciphered with the SCP02 DEK. keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)}] - data = self.C.build_put_key_data(0x40, keys, self.scp02) + data = ADF_SD.build_put_key_data(0x40, keys, self.scp02) self.assertEqual(b2h(data), '40' '85' '11' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV)) def test_wrong_basic_format_differs(self): # regression test, the generic "Basic format" does NOT match Table 3-13 for a PSK key # rejected by card with with 6a88 - wrong_basic = self.C.encode_key_data_basic('tls_psk', self.PSK_CIPHERED, b'') - right_psk = self.C.encode_key_data_psk(self.PSK_CLEAR, self.PSK_CIPHERED, self.PSK_KCV) + wrong_basic = ADF_SD.encode_key_data_basic('tls_psk', self.PSK_CIPHERED, b'') + right_psk = ADF_SD.encode_key_data_psk(self.PSK_CLEAR, self.PSK_CIPHERED, self.PSK_KCV) self.assertEqual(b2h(wrong_basic), '8510' + b2h(self.PSK_CIPHERED) + '00') self.assertEqual(b2h(right_psk), '8511' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV)) self.assertNotEqual(wrong_basic, right_psk) @@ -386,11 +383,11 @@ class PutKey_PSK_Test(unittest.TestCase): for kcb_len, exp_len_field in [(127, '7f'), (128, '8180'), (129, '8181'), (256, '820100')]: with self.subTest(kcb_len=kcb_len): kcb = bytes(kcb_len) - field = self.C.encode_key_data_basic('rsa_modulus_n', kcb, b'') + field = ADF_SD.encode_key_data_basic('rsa_modulus_n', kcb, b'') self.assertEqual(b2h(field), 'a2' + exp_len_field + b2h(kcb) + '00') # 85 field of Amendment B Table 3-13 uses the same coding # single byte inner length (clear key < 128) == block kcb_len bytes long - psk = self.C.encode_key_data_psk(bytes(120), bytes(kcb_len - 1), b'') + psk = ADF_SD.encode_key_data_psk(bytes(120), bytes(kcb_len - 1), b'') self.assertEqual(b2h(psk)[:2 + len(exp_len_field)], '85' + exp_len_field) def test_basic_format_unchanged(self): @@ -399,8 +396,8 @@ class PutKey_PSK_Test(unittest.TestCase): ('aes', h2b('000102030405060708090a0b0c0d0e0f'))]: ciph = self.scp02.encrypt_key(clear) kcv = compute_kcv(kt, clear) - via_construct = build_construct(self.C.KeyDataBasic, {'key_type': kt, 'kcb': b2h(ciph), 'kcv': b2h(kcv)}) - via_helper = self.C.encode_key_data_basic(kt, ciph, kcv) + via_construct = build_construct(ADF_SD.KeyDataBasic, {'key_type': kt, 'kcb': b2h(ciph), 'kcv': b2h(kcv)}) + via_helper = ADF_SD.encode_key_data_basic(kt, ciph, kcv) self.assertEqual(via_helper, via_construct) def test_psk_padding_no_double_length(self): @@ -413,7 +410,7 @@ class PutKey_PSK_Test(unittest.TestCase): with self.subTest(keylen=keylen): clear = bytes(range(keylen)) padded_len = keylen + (-keylen % 8) - field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear, + field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear, 'kcv': compute_kcv('tls_psk', clear)}], self.scp02)[1:] self.assertEqual(field[0], 0x85) l1 = field[1] @@ -429,7 +426,7 @@ class PutKey_PSK_Test(unittest.TestCase): # then stored as key material and rejected thanks to the KCV clear = h2b('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d') # 30, not %8 kcv = compute_kcv('tls_psk', clear) - field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear, + field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear, 'kcv': kcv}], self.scp02)[1:] self.assertEqual(len(clear), 30) self.assertEqual(field[2], 30) # L2 == clear key length, not 32 @@ -437,7 +434,7 @@ class PutKey_PSK_Test(unittest.TestCase): def test_kcv_suppressed(self): # --suppress-key-check -> KCV length 00 and no KCV bytes - field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, + field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': b''}], self.scp02)[1:] self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CIPHERED) + '00') @@ -448,7 +445,7 @@ class PutKey_PSK_Test(unittest.TestCase): dek = h2b('404142434445464748494a4b4c4d4e4f') keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)}, {'key_type': 'des', 'clear_key': dek, 'kcv': compute_kcv('des', dek)}] - data = self.C.build_put_key_data(0x40, keys, self.scp02) + data = ADF_SD.build_put_key_data(0x40, keys, self.scp02) b = data self.assertEqual(b[0], 0x40) # KVN @@ -473,7 +470,7 @@ class PutKey_PSK_Test(unittest.TestCase): def test_no_scp_leaves_key_clear(self): # During personalization (no SCP) the key is not enciphered, framing still follows Table 3-13. - field = self.C.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, + field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': self.PSK_KCV}], None)[1:] self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CLEAR) + '03' + b2h(self.PSK_KCV)) @@ -482,17 +479,16 @@ class PutKey_Length_Test(unittest.TestCase): """Tests for the length of the PUT KEY command APDU. Lc of GP CardSpec v2.3 Table 11-64 is a single byte, so an oversized key data field cannot be sent.""" - class PutKeyOnly(ADF_SD.AddlShellCommands): - """ADF_SD.AddlShellCommands with a canned scc to drive put_key()""" + class _FakeSccForPutKey(): + """mock scc: replays hardcoded status word + records the APDUs sent.""" def __init__(self, scp=None, max_cmd_len=255): - super().__init__() self.sent = [] - self.scc = SimpleNamespace(scp=scp, max_cmd_len=max_cmd_len, - send_apdu_checksw=lambda pdu: (self.sent.append(pdu), ('', '9000'))[1]) + self.scp = scp + self.max_cmd_len = max_cmd_len - @property - def _cmd(self): - return SimpleNamespace(lchan=SimpleNamespace(scc=self.scc)) + def send_apdu_checksw(self, apdu, sw='9000'): + self.sent.append(apdu) + return ('', '9000') # KVN, key type, two byte BER length of the key component block, KCV length; KCV suppressed FRAMING = 1 + 1 + 2 + 1 @@ -503,9 +499,9 @@ class PutKey_Length_Test(unittest.TestCase): def test_lc_matches_data_field(self): # largest key component block that still fits without a secure channel - sd = self.PutKeyOnly() - sd.put_key(0, 0x40, 1, self.key(255 - self.FRAMING)) - apdu = sd.sent[0] + scc = self._FakeSccForPutKey() + ADF_SD.put_key(scc, 0, 0x40, 1, self.key(255 - self.FRAMING)) + apdu = scc.sent[0] self.assertEqual(apdu[:8], '80D80001') lc = int(apdu[8:10], 16) self.assertEqual(lc, 255) # Lc ... @@ -514,20 +510,20 @@ class PutKey_Length_Test(unittest.TestCase): def test_oversized_key_data_raises(self): # real world fat example: RSA-2048 modulus does not fit, led to 3 nibble Lc 106, # which silently shifted and broke the whole APDU by half a byte. - sd = self.PutKeyOnly() + scc = self._FakeSccForPutKey() with self.assertRaises(ValueError) as ctx: - sd.put_key(0, 0x40, 1, self.key(256)) + ADF_SD.put_key(scc, 0, 0x40, 1, self.key(256)) self.assertIn('262', str(ctx.exception)) self.assertIn('255', str(ctx.exception)) - self.assertEqual(sd.sent, []) # nothing was sent to the card + self.assertEqual(scc.sent, []) # nothing was sent to the card def test_secure_channel_overhead_lowers_the_limit(self): # scc.max_cmd_len shrinks by the C-MAC + encryption padding of active SCP - sd = self.PutKeyOnly(max_cmd_len=239) - sd.put_key(0, 0x40, 1, self.key(239 - self.FRAMING)) - self.assertEqual(int(sd.sent[0][8:10], 16), 239) + scc = self._FakeSccForPutKey(max_cmd_len=239) + ADF_SD.put_key(scc, 0, 0x40, 1, self.key(239 - self.FRAMING)) + self.assertEqual(int(scc.sent[0][8:10], 16), 239) with self.assertRaises(ValueError): - sd.put_key(0, 0x40, 1, self.key(239 - self.FRAMING + 1)) + ADF_SD.put_key(scc, 0, 0x40, 1, self.key(239 - self.FRAMING + 1)) class Install_param_Test(unittest.TestCase): @@ -655,13 +651,6 @@ class Load_ChunkLen_Test(unittest.TestCase): payload = b'\xaa' * 500 # actual real world case LOAD TLV: C4 + 8201f4 + 500 = 504 total - def _sd(self, scc): - cmd = type('_Cmd', (), {'lchan': type('_Lchan', (), {'scc': scc})(), - 'poutput': lambda self, *args: None})() - # cmd2 CommandSet has a r/o _cmd property -> shadow it - _SD = type('_SD', (ADF_SD.AddlShellCommands,), {'_cmd': cmd}) - return _SD.__new__(_SD) - def _blocks(self, scc): """Get (p1, p2, lc) from LOAD APDU""" for apdu in scc.sent: @@ -671,7 +660,7 @@ class Load_ChunkLen_Test(unittest.TestCase): def test_default_no_scp(self): """Without SCP the old 240 byte block size is kept, no idea what else might rely on this number""" scc = _FakeSccForLoad(max_cmd_len=255) - self._sd(scc).load(self.payload) + ADF_SD.load(scc, self.payload) blocks = list(self._blocks(scc)) self.assertEqual([b[2] for b in blocks], [240, 240, 24]) self.assertEqual([b[0] for b in blocks], [0x00, 0x00, 0x80]) # P1: last block flagged @@ -680,24 +669,24 @@ class Load_ChunkLen_Test(unittest.TestCase): def test_default_scp02_level3(self): """max_cmd_len 239 (SCP02 lvl 3) squeezes the blocks""" scc = _FakeSccForLoad(max_cmd_len=239) - self._sd(scc).load(self.payload) + ADF_SD.load(scc, self.payload) self.assertEqual([b[2] for b in list(self._blocks(scc))], [239, 239, 26]) def test_explicit_chunk_len(self): scc = _FakeSccForLoad(max_cmd_len=255) - self._sd(scc).load(self.payload, chunk_len=100) + ADF_SD.load(scc, self.payload, chunk_len=100) self.assertEqual([b[2] for b in list(self._blocks(scc))], [100] * 5 + [4]) def test_explicit_chunk_len_too_large(self): scc = _FakeSccForLoad(max_cmd_len=239) with self.assertRaises(ValueError): - self._sd(scc).load(self.payload, chunk_len=240) + ADF_SD.load(scc, self.payload, chunk_len=240) self.assertEqual(scc.sent, []) # nothing sent! def test_explicit_chunk_len_zero(self): scc = _FakeSccForLoad(max_cmd_len=255) with self.assertRaises(ValueError): - self._sd(scc).load(self.payload, chunk_len=0) + ADF_SD.load(scc, self.payload, chunk_len=0) def test_end_to_end_scp02_level3(self): """original failure: 286 byte CAP + SCP02 lvl 3""" @@ -707,7 +696,7 @@ class Load_ChunkLen_Test(unittest.TestCase): scp02.gen_ext_auth_apdu() scp02.security_level = 0x03 scc = _FakeSccForLoad(max_cmd_len=255 - scp02.overhead, scp=scp02) - self._sd(scc).load(b'\x5a' * 286) + ADF_SD.load(scc, b'\x5a' * 286) self.assertEqual(len(scc.sent), 2) # 289 byte TLV in blocks of 239 for wrapped in scc.wrapped: self.assertLessEqual(wrapped[4], 255) @@ -771,106 +760,100 @@ class GetStatus_Pagination_Test(unittest.TestCase): ENTRY_1 = 'e3074f05a000000151' ENTRY_2 = 'e3074f05a000000152' - def _sd(self, responses, card_data=CARD_DATA_V211): - scc = _FakeScc(responses, card_data) - cmd = type('_Cmd', (), {'lchan': type('_Lchan', (), {'scc': scc})()})() - # cmd2 strikes again, CommandSet exposes _cmd as a read only property, needs shadowing - _SD = type('_SD', (ADF_SD.AddlShellCommands,), {'_cmd': cmd}) - return _SD.__new__(_SD), scc - def _aids(self, grd_list): return [b2h(grd.to_dict()['gp_registry_related_data'][0]['application_aid']) for grd in grd_list] def test_single_page(self): - sd, scc = self._sd([(self.ENTRY_1, '9000')]) - grd_list = sd.get_status('applications') + scc = _FakeScc([(self.ENTRY_1, '9000')]) + grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f24002024f0000']) self.assertEqual(self._aids(grd_list), ['a000000151']) def test_two_pages(self): """6310 -> reissue with P2 bit 1 set -> 9000, both pages in result""" - sd, scc = self._sd([(self.ENTRY_1, '6310'), (self.ENTRY_2, '9000')]) - grd_list = sd.get_status('applications') + scc = _FakeScc([(self.ENTRY_1, '6310'), (self.ENTRY_2, '9000')]) + grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f24002024f0000', '80f24003024f0000']) self.assertEqual(self._aids(grd_list), ['a000000151', 'a000000152']) def test_three_pages_keep_p2_next_occurrence(self): - sd, scc = self._sd([(self.ENTRY_1, '6310'), (self.ENTRY_2, '6310'), (self.ENTRY_1, '9000')]) - grd_list = sd.get_status('applications') + scc = _FakeScc([(self.ENTRY_1, '6310'), (self.ENTRY_2, '6310'), (self.ENTRY_1, '9000')]) + grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual([a[6:8] for a in scc.sent], ['02', '03', '03']) self.assertEqual(len(grd_list), 3) def test_no_match_returns_empty(self): """6A88 "referenced data not found" is empty result not failure.""" - sd, _scc = self._sd([('', '6a88')]) - self.assertEqual(sd.get_status('applications'), []) + scc = _FakeScc([('', '6a88')]) + self.assertEqual(ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)), []) def test_v211_card_gets_no_tag_list(self): """v2.1.1 section 9.4.2.3 has no tag list,not send a tag list""" - sd, scc = self._sd([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V211) - sd.get_status('applications') + scc = _FakeScc([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V211) + ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f24002024f0000']) self.assertNotIn('5c', scc.sent[0][8:]) def test_v22_card_gets_a_tag_list(self): - sd, scc = self._sd([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) - sd.get_status('applications') + scc = _FakeScc([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) + ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00']) def test_unknown_version_gets_no_tag_list(self): """If the card will not say, assume the conservative form that works everywhere.""" - sd, scc = self._sd([(self.ENTRY_1, '9000')], card_data=None) - sd.get_status('applications') + scc = _FakeScc([(self.ENTRY_1, '9000')], card_data=None) + ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f24002024f0000']) def test_v22_card_rejecting_tag_list_falls_back(self): """card announcing v2.2+ that still answers 6A80 to the tag list.""" - sd, scc = self._sd([('', '6a80'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) - grd_list = sd.get_status('applications') + scc = _FakeScc([('', '6a80'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) + grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00', '80f24002024f0000']) self.assertEqual(self._aids(grd_list), ['a000000151']) def test_aid_search_qualifier(self): - sd, scc = self._sd([(self.ENTRY_1, '9000')]) - sd.get_status('applications', 'a000000087') + scc = _FakeScc([(self.ENTRY_1, '9000')]) + ADF_SD.get_status(scc, 'applications', 'a000000087', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f24002074f05a00000008700']) def test_6a80_is_reported_on_a_v211_card(self): """no tag list -> 6A80 is error""" - sd, _scc = self._sd([('', '6a80')], card_data=CARD_DATA_V211) + scc = _FakeScc([('', '6a80')], card_data=CARD_DATA_V211) with self.assertRaises(SwMatchError) as ctx: - sd.get_status('applications') + ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(ctx.exception.sw_actual, '6a80') def test_unexpected_sw_is_not_silently_truncated(self): """partial is not complete result""" - sd, _scc = self._sd([(self.ENTRY_1, '6310'), ('', '6982')]) + scc = _FakeScc([(self.ENTRY_1, '6310'), ('', '6982')]) with self.assertRaises(SwMatchError) as ctx: - sd.get_status('applications') + ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(ctx.exception.sw_actual, '6982') def test_v22_card_answering_6a88_to_the_tag_list_falls_back(self): """6A88 is the other GET STATUS error condition of table 11-39, section 11.4.2.3 says we may get get an error status. 6A88 to the tag-list attempt should be retried without it or we get nothing""" - sd, scc = self._sd([('', '6a88'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) - grd_list = sd.get_status('applications') + scc = _FakeScc([('', '6a88'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) + grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)) self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00', '80f24002024f0000']) self.assertEqual(self._aids(grd_list), ['a000000151']) def test_v22_card_with_a_genuinely_empty_subset(self): """...and when the retry answers 6A88, the list really is empty.""" - sd, scc = self._sd([('', '6a88'), ('', '6a88')], card_data=CARD_DATA_V22) - self.assertEqual(sd.get_status('applications'), []) + scc = _FakeScc([('', '6a88'), ('', '6a88')], card_data=CARD_DATA_V22) + self.assertEqual(ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)), []) self.assertEqual(len(scc.sent), 2) def test_6a88_after_a_page_keeps_that_page(self): """6A88 is "no more matches" after we have data, we're done""" - sd, _scc = self._sd([(self.ENTRY_1, '6310'), ('', '6a88')], card_data=CARD_DATA_V22) - self.assertEqual(self._aids(sd.get_status('applications')), ['a000000151']) + scc = _FakeScc([(self.ENTRY_1, '6310'), ('', '6a88')], card_data=CARD_DATA_V22) + self.assertEqual(self._aids(ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))), + ['a000000151']) if __name__ == "__main__": unittest.main()