# # 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, inttostring from SmartcardSAM import CardContainer, SAM, PassportSAM, CryptoflexSAM from pickle import dumps, loads import socket, struct, sys, signal, atexit, traceback import struct 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 def reset(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"]) try: c = C_APDU(msg) except ValueError, e: print e return self.formatResult(0, 0, "", SW["ERR_INCORRECTPARAMETERS"]) #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