class SmartcardOS(object): """Base class for a smart card OS""" mf = make_property("mf", "master file") SAM = make_property("SAM", "secure access module") def getATR(self): """Returns the ATR of the card as string of characters""" return "" def powerUp(self): """Powers up the card""" self.mf.current = self.mf pass def powerDown(self): """Powers down the card""" pass def reset(self): """Performs a warm reset of the card (no power down)""" self.mf.current = self.mf pass def execute(self, msg): """Returns response to the given APDU as string of characters :param msg: the APDU as string of characters """ return ""
class nPA_SE(Security_Environment): eac_step = make_property("eac_step", "next step to performed for EAC") def __init__(self, MF, SAM): Security_Environment.__init__(self, MF, SAM) self.at = nPA_AT_CRT() # This breaks support for 3DES self.cct.blocklength = 16 self.cct.algorithm = "CC" self.eac_step = 0 self.sec = None self.eac_ctx = None self.cvca = None self.car = None self.ca_key = None self.disable_checks = False def _set_SE(self, p2, data): sw, resp = Security_Environment._set_SE(self, p2, data) if self.at.algorithm == "PACE": if self.at.keyref_is_pin(): if self.sam.counter <= 0: print("Must use PUK to unblock") return 0x63c0, "" if self.sam.counter == 1 and not self.sam.active: print("Must use CAN to activate") return 0x63c1, "" self.eac_step = 0 elif self.at.algorithm == "TA": if self.eac_step != 4: raise SwError(SW["ERR_AUTHBLOCKED"]) elif self.at.algorithm == "CA": if self.eac_step != 5: raise SwError(SW["ERR_AUTHBLOCKED"]) return sw, resp def general_authenticate(self, p1, p2, data): if (p1, p2) != (0x00, 0x00): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) if self.eac_step == 0 and self.at.algorithm == "PACE": return self.__eac_pace_step1(data) elif self.eac_step == 1 and self.at.algorithm == "PACE": return self.__eac_pace_step2(data) elif self.eac_step == 2 and self.at.algorithm == "PACE": return self.__eac_pace_step3(data) elif self.eac_step == 3 and self.at.algorithm == "PACE": return self.__eac_pace_step4(data) elif self.eac_step == 5 and self.at.algorithm == "CA": return self.__eac_ca(data) elif self.eac_step == 6: # TODO implement RI # "\x7c\x22\x81\x20\" is some prefix and the rest is our RI return SW["NORMAL"], b"\x7c\x22\x81\x20\x48\x1e\x58\xd1\x7c\x12" + \ b"\x9a\x0a\xb4\x63\x7d\x43\xc7\xf7\xeb\x2b" + \ b"\x06\x10\x6f\x26\x90\xe3\x00\xc4\xe7\x03" + \ b"\x54\xa0\x41\xf0\xd3\x90" raise SwError(SW["ERR_INCORRECTPARAMETERS"]) @staticmethod def __unpack_general_authenticate(data): data_structure = [] structure = unpack(data) for tlv in structure: tag, length, value = tlv if tag == 0x7c: data_structure = value else: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) return data_structure @staticmethod def __pack_general_authenticate(data): tlv_data = bertlv_pack(data) return bertlv_pack([[0x7c, len(tlv_data), tlv_data]]) def __eac_pace_step1(self, data): tlv_data = nPA_SE.__unpack_general_authenticate(data) if tlv_data != []: raise SwError(SW["WARN_NOINFO63"]) if self.at.keyref_is_mrz(): self.PACE_SEC = PACE_SEC(self.sam.mrz, eac.PACE_MRZ) elif self.at.keyref_is_can(): self.PACE_SEC = PACE_SEC(self.sam.can, eac.PACE_CAN) elif self.at.keyref_is_pin(): if self.sam.counter <= 0: print("Must use PUK to unblock") return 0x63c0, b"" if self.sam.counter == 1 and not self.sam.active: print("Must use CAN to activate") return 0x63c1, b"" self.PACE_SEC = PACE_SEC(self.sam.eid_pin, eac.PACE_PIN) self.sam.counter -= 1 if self.sam.counter <= 1: self.sam.active = False elif self.at.keyref_is_puk(): if self.sam.counter_puk <= 0: raise SwError(SW["WARN_NOINFO63"]) self.PACE_SEC = PACE_SEC(self.sam.puk, eac.PACE_PUK) self.sam.counter_puk -= 1 else: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.sec = self.PACE_SEC.sec if not self.eac_ctx: eac.EAC_init() self.EAC_CTX = EAC_CTX() self.eac_ctx = self.EAC_CTX.ctx eac.CA_disable_passive_authentication(self.eac_ctx) ef_card_security = self.mf.select('fid', 0x011d) ef_card_security_data = ef_card_security.data eac.EAC_CTX_init_ef_cardsecurity(ef_card_security_data, self.eac_ctx) if self.ca_key: ca_pubkey = eac.CA_get_pubkey(self.eac_ctx, ef_card_security_data) if 1 != eac.CA_set_key(self.eac_ctx, self.ca_key, ca_pubkey): eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) else: # we don't have a good CA key, so we simply generate an # ephemeral one comp_pubkey = eac.TA_STEP3_generate_ephemeral_key(self.eac_ctx) pubkey = eac.CA_STEP2_get_eph_pubkey(self.eac_ctx) if not comp_pubkey or not pubkey: eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) # save public key in EF.CardSecurity (and invalidate the # signature) # FIXME this only works for the default EF.CardSecurity. # Better use an ASN.1 parser to do this manipulation ef_card_security = self.mf.select('fid', 0x011d) ef_card_security_data = ef_card_security.data ef_card_security_data = \ ef_card_security_data[:61+4+239+2+1] + pubkey + \ ef_card_security_data[61+4+239+2+1+len(pubkey):] ef_card_security.data = ef_card_security_data nonce = eac.PACE_STEP1_enc_nonce(self.eac_ctx, self.sec) if not nonce: eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) resp = nPA_SE.__pack_general_authenticate([[0x80, len(nonce), nonce]]) self.eac_step += 1 return 0x9000, resp def __eac_pace_step2(self, data): tlv_data = nPA_SE.__unpack_general_authenticate(data) pubkey = eac.PACE_STEP3A_generate_mapping_data(self.eac_ctx) if not pubkey: eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) for tag, length, value in tlv_data: if tag == 0x81: eac.PACE_STEP3A_map_generator(self.eac_ctx, value) else: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.eac_step += 1 return 0x9000, \ nPA_SE.__pack_general_authenticate([[0x82, len(pubkey), pubkey]]) def __eac_pace_step3(self, data): tlv_data = nPA_SE.__unpack_general_authenticate(data) self.my_pace_eph_pubkey = \ eac.PACE_STEP3B_generate_ephemeral_key(self.eac_ctx) if not self.my_pace_eph_pubkey: eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) eph_pubkey = self.my_pace_eph_pubkey for tag, length, value in tlv_data: if tag == 0x83: self.pace_opp_pub_key = value eac.PACE_STEP3B_compute_shared_secret(self.eac_ctx, self.pace_opp_pub_key) else: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.eac_step += 1 return 0x9000, \ nPA_SE.__pack_general_authenticate([[0x84, len(eph_pubkey), eph_pubkey]]) def __eac_pace_step4(self, data): tlv_data = nPA_SE.__unpack_general_authenticate(data) eac.PACE_STEP3C_derive_keys(self.eac_ctx) my_token = \ eac.PACE_STEP3D_compute_authentication_token(self.eac_ctx, self.pace_opp_pub_key) token = b"" for tag, length, value in tlv_data: if tag == 0x85: token = value else: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) ver = eac.PACE_STEP3D_verify_authentication_token(self.eac_ctx, token) if not my_token or ver != 1: eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) print("Established PACE channel") if self.at.keyref_is_can(): if (self.sam.counter == 1): self.sam.active = True print("PIN resumed") elif self.at.keyref_is_pin(): self.sam.active = True self.sam.counter = 3 elif self.at.keyref_is_puk(): self.sam.active = True self.sam.counter = 3 print("PIN unblocked") self.eac_step += 1 self.at.algorithm = "TA" self.new_encryption_ctx = eac.EAC_ID_PACE result = [[0x86, len(my_token), my_token]] if self.at.chat: if self.cvca: self.car = CVC(self.cvca).get_chr() result.append([0x87, len(self.car), self.car]) if (self.disable_checks): eac.TA_disable_checks(self.eac_ctx) if not eac.EAC_CTX_init_ta(self.eac_ctx, None, self.cvca): eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) return 0x9000, nPA_SE.__pack_general_authenticate(result) def __eac_ca(self, data): tlv_data = nPA_SE.__unpack_general_authenticate(data) pubkey = "" for tag, length, value in tlv_data: if tag == 0x80: pubkey = value else: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) if eac.CA_STEP4_compute_shared_secret(self.eac_ctx, pubkey) != 1: eac.print_ossl_err() raise SwError(SW["ERR_NOINFO69"]) nonce, token = eac.CA_STEP5_derive_keys(self.eac_ctx, pubkey) if not nonce or not token: eac.print_ossl_err() raise SwError(SW["WARN_NOINFO63"]) self.eac_step += 1 print("Generated Nonce and Authentication Token for CA") # TODO activate SM self.new_encryption_ctx = eac.EAC_ID_CA return 0x9000, \ nPA_SE.__pack_general_authenticate([[0x81, len(nonce), nonce], [0x82, len(token), token]]) def verify_certificate(self, p1, p2, data): if (p1, p2) != (0x00, 0xbe): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) cert = bertlv_pack([[0x7f21, len(data), data]]) if 1 != eac.TA_STEP2_import_certificate(self.eac_ctx, cert): eac.print_ossl_err() raise SwError(SW["ERR_NOINFO69"]) print("Imported Certificate") return b"" def external_authenticate(self, p1, p2, data): """ Authenticate the terminal to the card. Check whether Terminal correctly encrypted the given challenge or not """ if self.dst.keyref_public_key: # TODO check if this is the correct CAR id_picc = eac.EAC_Comp(self.eac_ctx, eac.EAC_ID_PACE, self.my_pace_eph_pubkey) # FIXME auxiliary_data might be from an older run of PACE if hasattr(self.at, "auxiliary_data"): auxiliary_data = self.at.auxiliary_data else: auxiliary_data = None if 1 != eac.TA_STEP6_verify(self.eac_ctx, self.at.iv, id_picc, auxiliary_data, data): eac.print_ossl_err() print("Could not verify Terminal's signature") raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) print("Terminal's signature verified") self.eac_step += 1 return 0x9000, b"" raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) def compute_cryptographic_checksum(self, p1, p2, data): checksum = eac.EAC_authenticate(self.eac_ctx, data) if not checksum: eac.print_ossl_err() raise SwError(SW["ERR_NOINFO69"]) return checksum def encipher(self, p1, p2, data): padded = vsCrypto.append_padding(self.cct.blocklength, data) cipher = eac.EAC_encrypt(self.eac_ctx, padded) if not cipher: eac.print_ossl_err() raise SwError(SW["ERR_NOINFO69"]) return cipher def decipher(self, p1, p2, data): plain = eac.EAC_decrypt(self.eac_ctx, data) if not plain: eac.print_ossl_err() raise SwError(SW["ERR_NOINFO69"]) return plain def protect_response(self, sw, result): """ This method protects a response APDU using secure messaging mechanisms :returns: the protected data and the SW bytes """ return_data = b"" if result: # Encrypt the data included in the RAPDU encrypted = self.encipher(0x82, 0x80, result) encrypted = b"\x01" + encrypted encrypted_tlv = bertlv_pack([ (SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"], len(encrypted), encrypted) ]) return_data += encrypted_tlv sw_str = inttostring(sw) length = len(sw_str) tag = SM_Class["PLAIN_PROCESSING_STATUS"] tlv_sw = bertlv_pack([(tag, length, sw_str)]) return_data += tlv_sw if self.cct.algorithm is None: raise SwError(SW["CONDITIONSNOTSATISFIED"]) elif self.cct.algorithm == "CC": tag = SM_Class["CHECKSUM"] padded = vsCrypto.append_padding(self.cct.blocklength, return_data) auth = self.compute_cryptographic_checksum(0x8E, 0x80, padded) length = len(auth) return_data += bertlv_pack([(tag, length, auth)]) elif self.cct.algorithm == "SIGNATURE": tag = SM_Class["DIGITAL_SIGNATURE"] hash = self.hash(0x90, 0x80, return_data) auth = self.compute_digital_signature(0x9E, 0x9A, hash) length = len(auth) return_data += bertlv_pack([(tag, length, auth)]) return sw, return_data def compute_digital_signature(self, p1, p2, data): # TODO Signing with brainpoolP256r1 or any other key needs some more # effort ;-) return b'\x0D\xB2\x9B\xB9\x5E\x97\x7D\x42\x73\xCF\xA5\x45\xB7\xED' + \ b'\x5C\x39\x3F\xCE\xCD\x4A\xDE\xDC\x2B\x85\x23\x9F\x66\x52' + \ b'\x10\xC2\x67\xDC\xA6\x35\x94\x2D\x24\xED\xEB\xC8\x34\x6C' + \ b'\x4B\xD1\xA1\x15\xB4\x48\x3A\xA4\x4A\xCE\xFF\xED\x97\x0E' + \ b'\x07\xF3\x72\xF0\xFB\xA3\x62\x8C'
class Iso7816OS(SmartcardOS): mf = make_property("mf", "master file") SAM = make_property("SAM", "secure access module") def __init__(self, mf, sam, ins2handler=None, extended_length=False): self.mf = mf self.SAM = sam if not ins2handler: self.ins2handler = { 0x0c: self.mf.eraseRecord, 0x0e: self.mf.eraseBinaryPlain, 0x0f: self.mf.eraseBinaryEncapsulated, 0x2a: self.SAM.perform_security_operation, 0x20: self.SAM.verify, 0x22: self.SAM.manage_security_environment, 0x24: self.SAM.change_reference_data, 0x46: self.SAM.generate_public_key_pair, 0x82: self.SAM.external_authenticate, 0x84: self.SAM.get_challenge, 0x88: self.SAM.internal_authenticate, 0xa0: self.mf.searchBinaryPlain, 0xa1: self.mf.searchBinaryEncapsulated, 0xa4: self.mf.selectFile, 0xb0: self.mf.readBinaryPlain, 0xb1: self.mf.readBinaryEncapsulated, 0xb2: self.mf.readRecordPlain, 0xb3: self.mf.readRecordEncapsulated, 0xc0: self.getResponse, 0xca: self.mf.getDataPlain, 0xcb: self.mf.getDataEncapsulated, 0xd0: self.mf.writeBinaryPlain, 0xd1: self.mf.writeBinaryEncapsulated, 0xd2: self.mf.writeRecord, 0xd6: self.mf.updateBinaryPlain, 0xd7: self.mf.updateBinaryEncapsulated, 0xda: self.mf.putDataPlain, 0xdb: self.mf.putDataEncapsulated, 0xdc: self.mf.updateRecordPlain, 0xdd: self.mf.updateRecordEncapsulated, 0xe0: self.mf.createFile, 0xe2: self.mf.appendRecord, 0xe4: self.mf.deleteFile, } else: self.ins2handler = ins2handler if extended_length: self.maxle = MAX_EXTENDED_LE else: self.maxle = MAX_SHORT_LE self.lastCommandOffcut = b"" self.lastCommandSW = SW["NORMAL"] el = extended_length # only needed to keep following line short tsft = Iso7816OS.makeThirdSoftwareFunctionTable(extendedLe=el) card_capabilities = self.mf.firstSFT + self.mf.secondSFT + tsft self.atr = Iso7816OS.makeATR( T=1, directConvention=True, TA1=0x13, histChars=inttostring(0x80) + inttostring(0x70 + len(card_capabilities)) + card_capabilities) def getATR(self): return self.atr @staticmethod def makeATR(**args): """Calculate Answer to Reset (ATR) and returns the bitstring. - directConvention (bool): Whether to use direct convention or inverse convention. - TAi, TBi, TCi (optional): Value between 0 and 0xff. Interface Characters (for meaning see ISO 7816-3). Note that if no transmission protocol is given, it is automatically selected with T=max{j-1|TAj in args OR TBj in args OR TCj in args}. - T (optional): Value between 0 and 15. Transmission Protocol. Note that if T is set, TAi/TBi/TCi for i>T are omitted. - histChars (optional): Bitstring with 0 <= len(histChars) <= 15. Historical Characters T1 to T15 (for meaning see ISO 7816-4). T0, TDi and TCK are automatically calculated. """ # first byte TS if args["directConvention"]: atr = b"\x3b" else: atr = b"\x3f" if "T" in args: T = args["T"] else: T = 0 # find maximum i of TAi/TBi/TCi in args maxTD = 0 i = 15 while i > 0: if ("TA" + str(i) in args or "TB" + str(i) in args or "TC" + str(i) in args): maxTD = i - 1 break i -= 1 if maxTD == 0 and T > 0: maxTD = 2 # insert TDi into args (TD0 is actually T0) for i in range(0, maxTD + 1): if i == 0 and "histChars" in args: args["TD0"] = len(args["histChars"]) else: args["TD" + str(i)] = T if i < maxTD: args["TD" + str(i)] |= 1 << 7 if "TA" + str(i + 1) in args: args["TD" + str(i)] |= 1 << 4 if "TB" + str(i + 1) in args: args["TD" + str(i)] |= 1 << 5 if "TC" + str(i + 1) in args: args["TD" + str(i)] |= 1 << 6 # initialize checksum TCK = 0 # add TDi, TAi, TBi and TCi to ATR (TD0 is actually T0) for i in range(0, maxTD + 1): atr = atr + b"%c" % args["TD" + str(i)] TCK ^= args["TD" + str(i)] for j in ["A", "B", "C"]: if "T" + j + str(i + 1) in args: atr += b"%c" % args["T" + j + str(i + 1)] # calculate checksum for all bytes from T0 to the end TCK ^= args["T" + j + str(i + 1)] # add historical characters if "histChars" in args: atr += args["histChars"] for i in range(0, len(args["histChars"])): byte = args["histChars"][i] if isinstance(byte, str): TCK ^= ord(byte) else: TCK ^= byte # checksum is omitted for T=0 if T > 0: atr += b"%c" % TCK return atr @staticmethod def makeThirdSoftwareFunctionTable(commandChainging=False, extendedLe=False, assignLogicalChannel=0, maximumChannels=0): """ Returns a byte according to the third software function table from the historical bytes of the card capabilities. """ tsft = 0 if commandChainging: tsft |= 1 << 7 if extendedLe: tsft |= 1 << 6 if assignLogicalChannel: if not (0 <= assignLogicalChannel and assignLogicalChannel <= 3): raise ValueError tsft |= assignLogicalChannel << 3 if maximumChannels: if not (0 <= maximumChannels and maximumChannels <= 7): raise ValueError tsft |= maximumChannels return inttostring(tsft) def formatResult(self, seekable, le, data, sw, sm): if not seekable: self.lastCommandOffcut = data[le:] l = len(self.lastCommandOffcut) if l == 0: self.lastCommandSW = SW["NORMAL"] else: self.lastCommandSW = sw sw = SW["NORMAL_REST"] + min(0xff, l) else: if le > len(data): sw = SW["WARN_EOFBEFORENEREAD"] if le is not None: result = data[:le] else: result = data[:0] if sm: sw, result = self.SAM.protect_result(sw, result) return R_APDU(result, inttostring(sw)).render() @staticmethod def seekable(ins): if ins in [ 0xb0, 0xb1, 0xd0, 0xd1, 0xd6, 0xd7, 0xa0, 0xa1, 0xb2, 0xb3, 0xdc, 0xdd ]: return True else: return False def getResponse(self, p1, p2, data): if not (p1 == 0 and p2 == 0): raise SwError(SW["ERR_INCORRECTP1P2"]) return self.lastCommandSW, self.lastCommandOffcut def execute(self, msg): def notImplemented(*argz, **args): """ If an application tries to use a function which is not implemented by the currently emulated smartcard we raise an exception which should result in an appropriate response APDU being passed to the application. """ raise SwError(SW["ERR_INSNOTSUPPORTED"]) logging.info("Command APDU (%d bytes):\n %s", len(msg), hexdump(msg, indent=2)) try: c = C_APDU(msg) logging.debug("%s", str(c)) except ValueError as e: logging.warning(str(e)) return self.formatResult(False, 0, b"", SW["ERR_INCORRECTPARAMETERS"], False) # Handle Class Byte # {{{ class_byte = c.cla SM_STATUS = None logical_channel = 0 command_chaining = 0 header_authentication = 0 # Ugly Hack for OpenSC-explorer if (class_byte == 0xb0): logging.debug("Open SC APDU") SM_STATUS = "No SM" # If Bit 8,7,6 == 0 then first industry values are used if (class_byte & 0xE0 == 0x00): # Bit 1 and 2 specify the logical channel logical_channel = class_byte & 0x03 # Bit 3 and 4 specify secure messaging secure_messaging = class_byte >> 2 secure_messaging &= 0x03 if (secure_messaging == 0x00): SM_STATUS = "No SM" elif (secure_messaging == 0x01): SM_STATUS = "Proprietary SM" # Not supported ? elif (secure_messaging == 0x02): SM_STATUS = "Standard SM" elif (secure_messaging == 0x03): SM_STATUS = "Standard SM" header_authentication = 1 # If Bit 8,7 == 01 then further industry values are used elif (class_byte & 0x0C == 0x0C): # Bit 1 to 4 specify logical channel. 4 is added, value range is # from four to nineteen logical_channel = class_byte & 0x0f logical_channel += 4 # Bit 6 indicates secure messaging secure_messaging = class_byte >> 6 if (secure_messaging == 0x00): SM_STATUS = "No SM" elif (secure_messaging == 0x01): SM_STATUS = "Standard SM" else: # Bit 8 is set to 1, which is not specified by ISO 7816-4 SM_STATUS = "Proprietary SM" # In both cases Bit 5 specifies command chaining command_chaining = class_byte >> 5 command_chaining &= 0x01 # }}} sm = False try: if SM_STATUS == "Standard SM" or SM_STATUS == "Proprietary SM": c = self.SAM.parse_SM_CAPDU(c, header_authentication) logging.info("Decrypted APDU:\n%s", str(c)) sm = True sw, result = self.ins2handler.get(c.ins, notImplemented)(c.p1, c.p2, c.data) answer = self.formatResult(Iso7816OS.seekable(c.ins), c.effective_Le, result, sw, sm) except SwError as e: logging.debug(traceback.format_exc().rstrip()) logging.info(e.message) sw = e.sw result = b"" answer = self.formatResult(False, 0, result, sw, sm) return answer def powerUp(self): self.mf.current = self.mf def reset(self): self.mf.current = self.mf