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 |
|
|
(No Response) |
|
|
|
(No Response) |
|
|
|
(No Response) |
|
|
|
|
(ATR) |
|
(APDU) |
|
(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))