Creating a Virtual Smart Card — vsmartcard 2017-01-16 documentation

# 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.

## Documentation to Virtual Smart Card

- virtualsmartcard Package
  - cards Package
    - HandlerTest Module
    - Relay Module
    - cryptoflex Module
    - ePass Module
    - nPA Module
  - tests Package
    - CardGenerator_test Module
    - CryptoUtils_test Module
    - SmartcardSAM_test Module
    - utils_test Module
  - CardGenerator Module
  - ConstantDefinitions Module
  - CryptoUtils Module
  - SEutils Module
  - SWutils Module
  - SmartcardFilesystem Module
  - SmartcardSAM Module
  - TLVutils Module
  - VirtualSmartcard Module
  - utils Module

## 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, the complete details can be found in section Documentation to Virtual Smart Card.

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, 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
                self.os = RelayOS(readernum)
            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.error("Failed to open socket: %s", str(e))
                    logging.error("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(('8.8.8.8', 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.error("Failed to open socket: %s", str(e))
                    logging.error("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)

            signal.signal(signal.SIGINT, self.signalHandler)
            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.

        def create(p1, p2, data):

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

            args = {
                    "parent": None,
                    "filedescriptor": 0,
                    "fid": stringtoint(data[4:6]),
                    }
            if data[6] == "\x01":
                args["data"] = chr(0)*stringtoint(data[2:4])
                args["filedescriptor"] = FDB["EFSTRUCTURE_TRANSPARENT"]
                new_file = TransparentStructureEF(**args)
            elif data[6] == "\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] == "\x03":
                args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_VARIABLE_"
                                             "NOFURTHERINFO"]
                new_file = RecordStructureEF(**args)
            elif data[6] == "\x04":
                args["filedescriptor"] = FDB["EFSTRUCTURE_CYCLIC_NOFURTHERINFO"]
                new_file = RecordStructureEF(**args)
            elif data[6] == "\x38":
                if data[12] != "\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]

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):
            """
            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.error("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.error("Error connecting to card: %s", e.message)
                sys.exit()

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

            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", e.message)

        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.error("Error getting ATR: %s", e.message)
                    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.error("Error connecting to card: %s", e.message)
                    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.error("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
            apdu = map(ord, msg)

            try:
                rapdu, sw1, sw2 = self.session.sendCommandAPDU(apdu)
            except smartcard.Exceptions.CardConnectionException as e:
                logging.error("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]

            # return the response APDU as string
            return "".join([chr(b) for b in rapdu])