# # Copyright (C) 2009 Frank Morgner, Dominik Oepen # # This file is part of virtualsmartcard. # # virtualsmartcard 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 3 of the License, or (at your option) any # later version. # # virtualsmartcard 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 # virtualsmartcard. If not, see . # from ConstantDefinitions import * from TLVutils import * from SWutils import SwError, SW from SmartcardFilesystem import prettyprint_anything, MF, DF, CryptoflexMF, TransparentStructureEF from utils import C_APDU, R_APDU, hexdump, stringtoint, inttostring from SmartcardSAM import CardContainer, SAM, PassportSAM, CryptoflexSAM from pickle import dumps, loads import socket, struct, sys, signal, atexit, traceback class SmartcardOS(object): # {{{ def __init__(self, filename,mf=None,ins2handler=None, maxle=MAX_SHORT_LE,sam=None): self.config_key = "DUMMYKEYDUMMYKEY" #TODO: Let user enter password self.filename = filename self.mf = mf self.SAM = sam if self.filename != None: try: self.load(filename,self.config_key) except ValueError: print "Failed to load configuration %s" % filename if self.SAM == None: print "Using default SAM. Insecure!!!" self.SAM = SAM("testconfig.sam",self.config_key) #FIXME: Replace by defaul_SAM() if self.mf == None: print "Using default MF." self.mf = MF(filedescriptor=FDB["DF"], lifecycle=LCB["ACTIVATED"], dfname=None) self.SAM.set_MF(self.mf) # pgp directory #self.mf.append(DF(parent=self.mf, #fid=4, dfname='\xd2\x76\x00\x01\x24\x01', bertlv_data=[])) # pkcs-15 directories #self.mf.append(DF(parent=self.mf, #fid=1, dfname='\xa0\x00\x00\x00\x01')) #self.mf.append(DF(parent=self.mf, #fid=2, dfname='\xa0\x00\x00\x03\x08\x00\x00\x10\x00')) #self.mf.append(DF(parent=self.mf, #fid=3, dfname='\xa0\x00\x00\x03\x08\x00\x00\x10\x00\x01\x00')) #import imp, os.path #SAM_config = os.path.join(os.path.dirname(imp.find_module("SmartcardSAM")[1]), "testconfig.sam") #Path to initial SAM configuration, stored on disk 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 self.maxle = maxle self.lastCommandOffcut = "" self.lastCommandSW = SW["NORMAL"] card_capabilities = self.mf.firstSFT + self.mf.secondSFT + SmartcardOS.makeThirdSoftwareFunctionTable() self.atr = SmartcardOS.makeATR(T=1, directConvention = True, TA1=0x13, histChars = chr(0x80) + chr(0x70 + len(card_capabilities)) + card_capabilities) def save(self): """ Save the configuration of the current Smartcard (MF + SAM) to disk. To files will be stored: .mf for the mf and .sam for the SAM. Both files will be encrypted. """ if self.filename == None: raise ValueError, "No filename specified" else: mf = dumps(self.mf) path = self.filename + ".mf" mf = self.SAM.saveToDisk(mf,path) path = self.filename + ".sam" self.SAM.saveConfiguration(path) #self.SAM.saveConfiguration(path,"DUMMYKEYDUMMYKEY") def load(self,filename,password): """ Try to load a configuration from the filesystem. MF or SAM are only loaded if they aren't yet specified. """ if self.SAM == None: self.SAM = SAM("testconfig.sam","DUMMYKEYDUMMYKEY",None) #FIXME: replace by default_SAM() path = filename + ".sam" try: self.SAM.loadConfiguration(path,password) except IOError: print "Failed to open %s" % path if self.mf == None: path = filename + ".mf" data = self.SAM.loadFromDisk(path,password) self.mf = loads(data) print "Succesfully loaded MF from %s" % path def powerUp(self): pass def powerDown(self): pass @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 = "\x3b" else: atr = "\x3f" if args.has_key("T"): T = args["T"] else: T = 0 # find maximum i of TAi/TBi/TCi in args maxTD = 0 i = 15 while i > 0: if args.has_key("TA" + str(i)) or args.has_key("TB" + str(i)) or args.has_key("TC" + str(i)): 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 args.has_key("histChars"): args["TD0"] = len(args["histChars"]) else: args["TD"+str(i)] = T if i < maxTD: args["TD"+str(i)] |= 1<<7 if args.has_key("TA" + str(i+1)): args["TD"+str(i)] |= 1<<4 if args.has_key("TB" + str(i+1)): args["TD"+str(i)] |= 1<<5 if args.has_key("TC" + str(i+1)): 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 + "%c" % args["TD" + str(i)] TCK ^= args["TD" + str(i)] for j in ["A", "B", "C"]: if args.has_key("T" + j + str(i+1)): atr += "%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 args.has_key("histChars"): atr += args["histChars"] for i in range(0, len(args["histChars"])): TCK ^= ord( args["histChars"][i] ) # checksum is omitted for T=0 if T > 0: atr += "%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, le, data, sw, sm): if le == None: count = 0 elif le == 0: count = self.maxle else: count = le self.lastCommandOffcut = data[count:] l = len(self.lastCommandOffcut) if l == 0: self.lastCommandSW = SW["NORMAL"] else: self.lastCommandSW = sw sw = SW["NORMAL_REST"] + min(0xff, l) result = data[:count] if sm: sw, result = self.SAM.protect_result(sw,result) return R_APDU(result, inttostring(sw)).render() 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): raise SwError(SW["ERR_INSNOTSUPPORTED"]) c = C_APDU(msg) #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): print "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 = "Propietary 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 >> 5 secure_messaging &= 0x01 if (secure_messaging == 0x00): SM_STATUS = "No SM" elif (secure_messaging == 0x01): SM_STATUS = "Standard SM" #In both cases Bit 5 specifiys command chaining command_chaining = class_byte >> 5 command_chaining &= 0x01 #}}} try: if SM_STATUS == "Standard SM": c = self.SAM.parse_SM_CAPDU(c,header_authentication) elif SM_STATUS == "Propietary SM": raise SwError("ERR_SECMESSNOTSUPPORTED") sw, result = self.ins2handler.get(c.ins, notImplemented)(c.p1, c.p2, c.data) if SM_STATUS == "Standard SM": answer = self.formatResult(c.le, result, sw, True) else: answer = self.formatResult(c.le, result, sw, False) except SwError, e: print e.message #traceback.print_exception(*sys.exc_info()) sw = e.sw result = "" answer = self.formatResult(c.le, result, sw, False) return answer # }}} class PassportOS(SmartcardOS): """ The PassportOS emulates a Passport Application according to ICAO MRTD standard. It generates a data structure ... It also integrates a SAM derived from the standard SmartcardSAM, providing the Basic Access Control (BAC) mechanisms. """ def __init__(self, filename,mf=None, ins2handler=None, maxle=MAX_SHORT_LE): if filename == None and mf == None: mf = MF() else: pass #TODO: Load data from disk self.generate_data_structure(mf) self.SAM = PassportSAM(mf) SmartcardOS.__init__(self, None, mf=mf, ins2handler=ins2handler, maxle=maxle,sam=self.SAM) def generate_data_structure(self,mf): #MRZ1 = "P>8: r = inttostring(SW["NORMAL"]) # be fully successfull if at least one databyte has been returned if len(r) > 2 and r[-2] == chr(SW["NORMAL_REST"]>>8): return r[:-2] + inttostring(SW["NORMAL"]) return r # }}} class VirtualICC(object): # {{{ def __init__(self, filename,os=None, lenlen=3, host="localhost", port=35963): if os == None: self.os = SmartcardOS(filename) else: self.os = os self.filename = filename self.sock = self.connectToPort(host, port) self.sock.settimeout(None) self.lenlen = lenlen signal.signal(signal.SIGINT, self.signalHandler) atexit.register(self.signalHandler) atexit.register(self.saveCard) def saveCard(self): if self.filename != None: print "Saving smartcard configuration to %s" % self.filename self.os.save() else: print "No filename specified, card configuration will not be saved." def signalHandler(self, signal=None, frame=None): self.sock.close() sys.exit() @staticmethod def connectToPort(host, port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) return sock def __sendToVPICC(self, msg): size = inttostring(len(msg), self.lenlen) self.sock.sendall("%s%s" % (size, msg)) def __recvFromVPICC(self): # receive message size size = self.sock.recv(self.lenlen) size = stringtoint(size) # receive and return message if size == 0: return "" else: msg = self.sock.recv(size) return msg def run(self): while True : msg = self.__recvFromVPICC() if msg == "": self.__sendToVPICC(self.os.atr) elif msg == chr(0): # powerdown print "Power Down" self.os.powerDown() elif msg == chr(1): # powerup print "Power Up" self.os.powerUp() else: print "APDU (%d Bytes):\n%s" % (len(msg),hexdump(msg, short=True)) answer = self.os.execute(msg) print "RESP (%d Bytes):\n%s\n" % (len(answer),hexdump(answer, short=True)) self.__sendToVPICC(answer) # }}} if __name__ == "__main__": from optparse import OptionParser parser = OptionParser() parser.add_option("-t", "--type", action="store", type="choice", default='iso7816', choices=['iso7816', 'cryptoflex', 'ePass'], help="Type of Smartcard [default: %default]") parser.add_option("-f", "--file", action="store", type="string", dest="filename", default=None, help="Name of a smartcard stored in the filesystem. The card will be loaded") (options, args) = parser.parse_args() if options.type == 'cryptoflex': vicc = VirtualICC(options.filename,os=CryptoflexOS(options.filename)) elif options.type == 'ePass': #raise ValueError, "Not implemented yet!" vicc = VirtualICC(options.filename,os=PassportOS(options.filename)) else: vicc = VirtualICC(options.filename) vicc.run()