Creating a Virtual Smart Card

vpcd communicates over a socked with vpicc usually on port 0x8C7B (configurably via /etc/reader.conf.d/vpcd). So you can connect virtually any program to the virtual smart card reader, as long as you respect the following protocol:

vpcd

vpicc

Length

Command

Length

Response

0x00 0x01

0x00 (Power Off)

(No Response)

0x00 0x01

0x01 (Power On)

(No Response)

0x00 0x01

0x02 (Reset)

(No Response)

0x00 0x01

0x04 (Get ATR)

0xXX 0xXX

(ATR)

0xXX 0xXX

(APDU)

0xXX 0xXX

(R-APDU)

The communication is initiated by vpcd. First the length of the data (in network byte order, i.e. big endian) is sent followed by the data itself.

Examples

Implementing a ISO 7816 like Smart Card

vpicc includes an emulation of a card acting according to ISO 7816. This includes all standard commands for file management and secure messaging.

Let’s assume we want to create a cryptoflex card, that acts mostly according to ISO 7816. In this example we only want to add little things that differ from ISO 7816. But as for most complex software you need to know where you need to hook into. Here we only want to give an overview to the design.

Back to the cryptoflex example. VirtualICC provides the connection to the virtual smart card reader. It fetches an APDU and other requests from the vpcd. In VirtualICC an APDU is only a buffer that is forwarded to the smart card OS. First we modify VirtualICC to recognize a new type "cryptoflex" and to load CryptoflexOS. The CardGenerator is used to create a file system and a SAM specific to the cryptoflex (we come back to this later).

    def __init__(self, datasetfile, card_type, host, port,
                 readernum=None, mitmPath=None, ef_cardsecurity=None, ef_cardaccess=None,
                 ca_key=None, cvca=None, disable_checks=False, esign_key=None,
                 esign_ca_cert=None, esign_cert=None,
                 logginglevel=logging.INFO):
        from os.path import exists

        logging.basicConfig(level=logginglevel,
                            format="%(asctime)s  [%(levelname)s] %(message)s",
                            datefmt="%d.%m.%Y %H:%M:%S")

        self.cardGenerator = CardGenerator(card_type)

        # If a dataset file is specified, read the card's data groups from disk
        if datasetfile is not None:
            if exists(datasetfile):
                logging.info("Reading Data Groups from file %s.",
                             datasetfile)
                self.cardGenerator.readDatagroups(datasetfile)

        MF, SAM = self.cardGenerator.getCard()

        # Generate an OS object of the correct card_type
        if card_type == "iso7816" or card_type == "ePass":
            self.os = Iso7816OS(MF, SAM)
        elif card_type == "nPA":
            from virtualsmartcard.cards.nPA import NPAOS
            self.os = NPAOS(MF, SAM, ef_cardsecurity=ef_cardsecurity,
                            ef_cardaccess=ef_cardaccess, ca_key=ca_key,
                            cvca=cvca, disable_checks=disable_checks,
                            esign_key=esign_key, esign_ca_cert=esign_ca_cert,
                            esign_cert=esign_cert)
        elif card_type == "cryptoflex":
            from virtualsmartcard.cards.cryptoflex import CryptoflexOS
            self.os = CryptoflexOS(MF, SAM)
        elif card_type == "relay":
            from virtualsmartcard.cards.Relay import RelayOS
            from virtualsmartcard.cards.RelayMiddleman import RelayMiddleman
            mitm = loadMitMFromPath(mitmPath) if mitmPath else RelayMiddleman()
            self.os = RelayOS(readernum,mitm=mitm) 
        elif card_type == "handler_test":
            from virtualsmartcard.cards.HandlerTest import HandlerTestOS
            self.os = HandlerTestOS()
        else:
            logging.warning("Unknown cardtype %s. Will use standard card_type \
                            (ISO 7816)", card_type)
            card_type = "iso7816"
            self.os = Iso7816OS(MF, SAM)
        self.type = card_type

        # Connect to the VPCD
        self.host = host
        self.port = port
        if host:
            # use normal connection mode
            try:
                self.sock = self.connectToPort(host, port)
                self.sock.settimeout(None)
                self.server_sock = None
            except socket.error as e:
                logging.critical("Failed to open socket: %s", str(e))
                logging.critical("Is pcscd running at %s? Is vpcd loaded? Is a \
                              firewall blocking port %u?", host, port)
                sys.exit()
        else:
            # use reversed connection mode
            try:
                local_ip = [(s.connect(('9.9.9.9', 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]
                custom_url = 'vicc://%s:%d' % (local_ip, port)
                print('VICC hostname:  %s' % local_ip)
                print('VICC port:      %d' % port)
                print('On your NFC phone with the Android Smart Card Emulator app scan this code:')
                try:
                    import qrcode
                    qr = qrcode.QRCode()
                    qr.add_data(custom_url)
                    qr.print_ascii()
                except ImportError:
                    print('https://api.qrserver.com/v1/create-qr-code/?data=%s' % custom_url)
                (self.sock, self.server_sock, host) = self.openPort(port)
                self.sock.settimeout(None)
            except socket.error as e:
                logging.critical("Failed to open socket: %s", str(e))
                logging.critical("Is pcscd running? Is vpcd loaded and in \
                              reversed connection mode? Is a firewall \
                              blocking port %u?", port)
                sys.exit()

        logging.info("Connected to virtual PCD at %s:%u", host, port)

        atexit.register(self.stop)

Responses from our cryptoflex card look the same as for the 7816 card. But when a command was successfull (or not) there is a little difference in what is returned. So we need to edit formatResult, which is called to encode the SWs and the resulting data.

    def formatResult(self, ins, le, data, sw):
        if le == 0 and len(data):
            # cryptoflex does not inpterpret le==0 as maxle
            self.lastCommandSW = sw
            self.lastCommandOffcut = data
            r = R_APDU(inttostring(SW["ERR_WRONGLENGTH"] +
                       min(0xff, len(data)))).render()
        else:
            if ins == 0xa4 and len(data):
                # get response should be followed by select file
                self.lastCommandSW = sw
                self.lastCommandOffcut = data
                r = R_APDU(inttostring(SW["NORMAL_REST"] +
                           min(0xff, len(data)))).render()
            else:
                r = Iso7816OS.formatResult(self, Iso7816OS.seekable(ins), le,
                                           data, sw, False)

        return r

Note that this also requires some insight knowledge about how Iso7816OS works.

The previously created SAM handles keys, encryption, secure messaging and so on (we will not go into more details here). The file system creates, selects and reads contents of files or directories. File handling for our cryptoflex card is similar to ISO 7816, but the meaning of P1, P2 and the APDU data is completely different when creating a file on the smart card. So we derive CryptoflexMF from MF and modify create to our needs.

        if data[0:2] != b"\xff\xff":
            raise SwError(SW["ERR_INCORRECTPARAMETERS"])

        args = {
                "parent": None,
                "filedescriptor": 0,
                "fid": stringtoint(data[4:6]),
                }
        if data[6] == b"\x01":
            args["data"] = bytes(0)*stringtoint(data[2:4])
            args["filedescriptor"] = FDB["EFSTRUCTURE_TRANSPARENT"]
            new_file = TransparentStructureEF(**args)
        elif data[6] == b"\x02":
            if len(data) > 16:
                args["maxrecordsize"] = stringtoint(data[16])
            elif p2:
                # if given a number of records
                args["maxrecordsize"] = (stringtoint(data[2:4]) / p2)
            args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_FIXED_"
                                         "NOFURTHERINFO"]
            new_file = RecordStructureEF(**args)
        elif data[6] == b"\x03":
            args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_VARIABLE_"
                                         "NOFURTHERINFO"]
            new_file = RecordStructureEF(**args)
        elif data[6] == b"\x04":
            args["filedescriptor"] = FDB["EFSTRUCTURE_CYCLIC_NOFURTHERINFO"]
            new_file = RecordStructureEF(**args)
        elif data[6] == b"\x38":
            if data[12] != b"\x03":
                raise SwError(SW["ERR_INCORRECTPARAMETERS"])
            new_file = DF(**args)
        else:
            logging.error("unknown type: 0x%x" % ord(data[6]))
            raise SwError(SW["ERR_INCORRECTPARAMETERS"])

        return [new_file]

    def recordHandlingDecode(self, p1, p2):

As you can see it is quite simple to extend the virtual smart card to your requirements. Simply overwrite those functions, that differ from ISO 78166. But as said before, the virtual smart card is quite complex and you might have to read some documentation or even source code to find out where it’s best to do your modifications…

Implementing an Other Type of Card

If you have a card entirely different to ISO 7816, you surely want to avoid all magic that is done while parsing a buffer (an APDU). As example we will connect to an other smart card using PC/SC and forward it to vpcd.

As before with the cryptoflex card, we let VirtualICC recognize the new type "relay". RelayOS overwrites all main functions from the template SmartcardOS. Its functions correspond to the commands sent by vpcd. If you know how to use pyscard then the rest is pretty straight forward, but see yourself…

class RelayOS(SmartcardOS):
    """
    This class implements relaying of a (physical) smartcard. The RelayOS
    forwards the command APDUs received from the vpcd to the real smartcard via
    an actual smart card reader and sends the responses back to the vpcd.
    This class can be used to implement relay or MitM attacks.
    """
    def __init__(self, readernum, mitm=RelayMiddleman()):
        """
        Initialize the connection to the (physical) smart card via a given
        reader
        """

        # See which readers are available
        readers = smartcard.System.listReaders()
        if len(readers) <= readernum:
            logging.critical("Invalid number of reader '%u' (only %u available)",
                          readernum, len(readers))
            sys.exit()

        # Connect to the reader and its card
        # XXX this is a workaround, see on sourceforge bug #3083254
        # should better use
        # self.reader = smartcard.System.readers()[readernum]
        self.reader = readers[readernum]
        try:
            self.session = smartcard.Session(self.reader)
        except smartcard.Exceptions.CardConnectionException as e:
            logging.critical("Error connecting to card: %s", str(e))
            sys.exit()

        logging.info("Connected to card in '%s'", self.reader)

        self.mitm = mitm

        atexit.register(self.cleanup)

    def cleanup(self):
        """
        Close the connection to the physical card
        """
        try:
            self.session.close()
        except smartcard.Exceptions.CardConnectionException as e:
            logging.warning("Error disconnecting from card: %s", str(e))

    def getATR(self):
        # when powerDown has been called, fetching the ATR will throw an error.
        # In this case we must try to reconnect (and then get the ATR).
        try:
            atr = self.session.getATR()
        except smartcard.Exceptions.CardConnectionException as e:
            try:
                # Try to reconnect to the card
                self.session.close()
                self.session = smartcard.Session(self.reader)
                atr = self.session.getATR()
            except smartcard.Exceptions.CardConnectionException as e:
                logging.critical("Error getting ATR: %s", str(e))
                sys.exit()

        return "".join([chr(b) for b in atr])

    def powerUp(self):
        # When powerUp is called multiple times the session is valid (and the
        # card is implicitly powered) we can check for an ATR. But when
        # powerDown has been called, the session gets lost. In this case we
        # must try to reconnect (and power the card).
        try:
            self.session.getATR()
        except smartcard.Exceptions.CardConnectionException as e:
            try:
                self.session = smartcard.Session(self.reader)
            except smartcard.Exceptions.CardConnectionException as e:
                logging.critical("Error connecting to card: %s", str(e))
                sys.exit()

    def powerDown(self):
        # There is no power down in the session context so we simply
        # disconnect, which should implicitly power down the card.
        try:
            self.session.close()
        except smartcard.Exceptions.CardConnectionException as e:
            logging.critical("Error disconnecting from card: %s", str(e))
            sys.exit()

    def reset(self):
        self.powerDown()
        self.powerUp()

    def execute(self, msg):
        # sendCommandAPDU() expects a list of APDU bytes
        if isinstance(msg,str):
            apdu = map(ord, msg)
        else:
            apdu = list(msg)

        apdu = self.mitm.handleInPDU(apdu)

        try:
            rapdu, sw1, sw2 = self.session.sendCommandAPDU(apdu)
        except smartcard.Exceptions.CardConnectionException as e:
            logging.critical("Error transmitting APDU: %s", str(e))
            sys.exit()

        # XXX this is a workaround, see on sourceforge bug #3083586
        # should better use
        # rapdu = rapdu + [sw1, sw2]
        if rapdu[-2:] == [sw1, sw2]:
            pass
        else:
            rapdu = rapdu + [sw1, sw2]

        rapdu = self.mitm.handleOutPDU(rapdu)

        # return the response APDU as string
        return "".join(map(chr, rapdu))