Working with smart cards and PKI stuff is an interesting field. You can see the state-of-art of computer security and how it can be used in the real environment from real users. But, sometimes, debugging and testing applications that work with smart cards is a real pain, especially when you have to deal with negative test cases and, as it often happens, you don't have many test smart cards to play with. What if you accidentally block a PIN? Or your CSP issues a wrong command, leaving the card in an inconsistent state? These and many other issues are quite common in this field, so one of the first things I realized when I started to work with smart cards was that I needed an emulator: something to play with without the risk of doing any damage. In this article, I will not speak about smart card OS emulation (perhaps it will be covered in the future...), but about a driver for a virtual smart card reader.
Searching the internet for virtual drivers leads you to find many interesting resources, but not the “guide for dummies” that I was hoping to find. I’m not an expert in driver developing; this is not by any means an article on “how to write drivers”. I’m just explaining my approach to a new subject, hoping that it will be useful for someone.
Having tried both approaches, I think that developing a driver is better, having learned some base lessons on how to do it (or having this article as a guide :) ).
It needed just a few clicks on Google to realize that, to keep things easy, I had to use UMDF (User Mode Driver Framework) as a basis for the development of the driver. From my point of view, and my understanding of the subject, the main reasons are:
These are the reasons that led me to use UMDF. Considering the little effort and the satisfaction with the result, I think it was a good choice.
The code is base on the UMDFSkeleton
sample of WDK 7.1. I will first comment on the important points of the code, then I will explain the installation procedure.
As an addiction, the virtual card reader will communicate with a desktop application to provide the virtual smart card behavior; so, we'll see some IPC between a UMDF driver and a user process.
As I said, UMDF simplifies the development of a driver a lot. You just need to write some COM (actually, COM-like) objects implementing some core interfaces and that's it. Let's take a look at how it all works.
A user mode driver is like a COM object. So, like a COM object, we are building a DLL that exposes a DllGetClassObject
function, that will be called by the UMDF framework to obtain a ClassFactory
to create the actual driver object.
With ATL, it is very easy to create COM objects, so we'll use it to further simplify our job. The only function exposed by the DLL is:
Nothing strange here. The object we are creating (CMyDriver
) must implement the IDriverEntry
interface, that defines the main entry points of our driver. We can use the OnInitialize
method to do all initialization stuff before the actual job begins, but it is not needed in our case.
Some users asked me to have more than one instance of the virtual reader; in this updated version I added this possibility using a single instance of the driver. A configuration file "BixVReader.ini" is used to read how many virtual reader should be registered with the driver, and some more configuration parameters, as we'll se in the following "Update" sections.
We don't want synchronization issues, so we use SetLockingConstraint(WdfDeviceLevel)
: only one event handler of the device can run at a given moment. Then we ask the IWDFDriver
object to create a IWDFDevice
.
These objects are the actual objects maintained by UMDF through which we interact with the underlying driver and device. Since these objects are tightly coupled, in CMyDevice
we keep a reference to the IWDFDevice
object.
Moreover, we need to call CreateDeviceInterface
to create an interface for the device of a type specified by a GUID for each virtual reader managed by the driver. In our case, a Smart Card Reader. Each interface has a name assigned ("DEVx" in our case) to distinguish the recipient of a particular request in the queue (more on this later). This interface is automatically enabled by the framework.
We should note, at this point, that our CMyDevice
objects implement some interfaces:
IPnpCallbackHardware
IPnpCallback
IRequestCallbackCancel
In the CreateDevice
call, we passed spCallback
(a pointer to the IUnknown
interface of CMyDriver
), to inform the UMDF framework that we want to be notified about some events. The framework, according to the interfaces implemented by the callback object, calls its methods when specific events are fired.
IPnpCallbackHardware
contains methods to manage hardware insertion and removal.
IPnpCallback
contains methods to manage lifetime events on the driver.
IRequestCallbackCancel
contains method to manage deletion of I/O Request received by the device. We'll see it in detail later.
The first notification received by our driver is OnPrepareHardware
: the hardware is ready and the driver should prepare to use it:
HRESULT CMyDevice::OnPrepareHardware(
__in IWDFDevice* pWdfDevice
)
{
inFunc
SectionLogger a(__FUNCTION__);
m_pWdfDevice = pWdfDevice;
HRESULT hr = CMyQueue::CreateInstance(m_pWdfDevice, this);
return hr;
}
We create the default queue for this driver, and attach a callback interface to it (a CMyQueue
object that implements IQueueCallbackDeviceIoControl
, that will receive I/O events notifications). A driver queue receives all I/O requests from the system when applications try to interact with our device.
Update:
The life cycle of a driver has several states that should be managed to avoid incorrect behaviours: a device starts in an idle state and is switched to a working state, and in case of various system events (shutdown, hibernation) can be disabled and re-enabled. Without going in deeper detail, the state we are interested in is called D0 state (device fully functional). We'll intercept transitions to and from this state to perform tasks concerned with device functionality. For out virtual driver this just means that we start and stop inter-process communication. We'll see later how this works. The UMDF framewrok exposes two functions to do this job, wich are OnD0Entry and OnD0Exit:
HRESULT CMyDevice::OnD0Entry(IN IWDFDevice* pWdfDevice,IN WDF_POWER_DEVICE_STATE previousState) {
SectionLogger a(__FUNCTION__);
UNREFERENCED_PARAMETER(pWdfDevice);
UNREFERENCED_PARAMETER(previousState);
numInstances=GetPrivateProfileInt(L"Driver",L"NumReaders",1,L"BixVReader.ini");
readers.resize(numInstances);
for (int i=0;i<numInstances;i++) {
wchar_t section[300];
char sectionA[300];
swprintf(section,L"Reader%i",i);
sprintf(sectionA,"Reader%i",i);
int rpcType=GetPrivateProfileInt(section,L"RPC_TYPE",0,L"BixVReader.ini");
if (rpcType==0)
readers[i]=new PipeReader();
else if (rpcType==1)
readers[i]=new TcpIpReader();
readers[i]->instance=i;
readers[i]->device=this;
GetPrivateProfileStringA(sectionA,"VENDOR_NAME","Bix",readers[i]->vendorName,300,"BixVReader.ini");
GetPrivateProfileStringA(sectionA,"VENDOR_IFD_TYPE","VIRTUAL_CARD_READER",readers[i]->vendorIfdType,300,"BixVReader.ini");
readers[i]->deviceUnit=GetPrivateProfileInt(section,L"DEVICE_UNIT",i,L"BixVReader.ini");
readers[i]->protocol=0;
readers[i]->init(section);
DWORD pipeThreadID;
readers[i]->serverThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ServerFunc,readers[i],0,&pipeThreadID);
}
return S_OK;
}
HRESULT CMyDevice::OnD0Exit(IN IWDFDevice* pWdfDevice,IN WDF_POWER_DEVICE_STATE newState) {
SectionLogger a(__FUNCTION__);
UNREFERENCED_PARAMETER(pWdfDevice);
UNREFERENCED_PARAMETER(newState);
shutDown();
return S_OK;
}
void CMyDevice::shutDown() {
SectionLogger a(__FUNCTION__);
for (int i=0;i<numInstances;i++) {
readers[i]->shutdown();
delete readers[i];
}
numInstances=0;
}
In the updated version of the driver we are also reading the configuration of the driver from BixVReader.ini. The parameters we can set are:
- NumReaders: the number of virtual readers
- RPC_PORT_BASE: the base port for Tcp/Ip communication
- RPC_TYPE: are we using pipes or tcp/ip to communicate with the virtual reader app?
- VENDOR_NAME: part of the the reader name returned by SCardListReaders
- VENDOR_IFD_TYPE: part of the the reader name returned by SCardListReaders
- DEVICE_UNIT: part of the the reader name returned by SCardListReaders (name, ifd type and device unit are concatenated to obtain the final reader name)
- PIPE_NAME: named pipe for requests from driver to virtual reader (if pipes are used)
- PIPE_EVENT_NAME: named pipe for events notification from virtual reader to driver (if pipes are used)
- TCP_PORT: port number for requests from driver to virtual reader (if tpc/ip is used)
- TCP_EVENT_PORT: port number for events notification from virtual reader to driver (if tpc/ip is used)
The BixVReader.ini file has to be placed in %SystemRoot% to be found by the driver. If this file is not present, or some parameters are not specificed, the following default values are used:
- NumReaders: 1 virtual reader
- RPC_PORT_BASE: 29500
- RPC_TYPE: pipes
- VENDOR_NAME: "Bix"
- VENDOR_IFD_TYPE: "VIRTUAL_SCARD_READER"
- DEVICE_UNIT: the index number of the virtual reader
- PIPE_NAME: "SCardSimulatorDriver" followed by the reader index
- PIPE_EVENT_NAME: "SCardSimulatorDriverEvents" followed by the reader index
- TCP_PORT: PortBase + (reader index * 2)
- TCP_EVENT_PORT: PortBase + (reader index * 2) +1
This is a sample BixVReader.ini file:
[Driver]
NumReaders=2
[Reader0]
RPC_TYPE=0
VENDOR_NAME=VirtualCard
VENDOR_IFD_TYPE=BixReader
DECIVE_UNIT=0
[Reader1]
RPC_TYPE=1
VENDOR_NAME=VirtualCard
VENDOR_IFD_TYPE=BixReader
DECIVE_UNIT=1
In this sample we have 2 readers, Named "VirtualCard BixReader 0" and "VirtualCard BixReader 1". Number 0 accepts connections from named pipes, number 1 from Tcp/Ip.
All these parameters are kept in a Reader class, that implements all the reader specific functions.
At this point, our driver is ready to receive requests and send appropriate responses to the system. This happens by means of calls made to the CMyQueue::OnDeviceIoControl
:
STDMETHODIMP_ (void) CMyQueue::OnDeviceIoControl(
__in IWDFIoQueue* pQueue,
__in IWDFIoRequest* pRequest,
__in ULONG ControlCode,
SIZE_T InputBufferSizeInBytes,
SIZE_T OutputBufferSizeInBytes
)
{
m_pParentDevice->ProcessIoControl(pQueue,pRequest,ControlCode,InputBufferSizeInBytes,OutputBufferSizeInBytes);
}
This functions simply passes the request to the device class:
void CMyDevice::ProcessIoControl(__in IWDFIoQueue* pQueue,
__in IWDFIoRequest* pRequest,
__in ULONG ControlCode,
SIZE_T inBufSize,
SIZE_T outBufSize)
{
inFunc
SectionLogger a(__FUNCTION__);
UNREFERENCED_PARAMETER(pQueue);
wchar_t log[300];
swprintf(log,L"[BixVReader][IOCT]IOCTL %08X - In %i Out %i",ControlCode,inBufSize,outBufSize);
OutputDebugString(log);
int instance=0;
{
CComPtr<IWDFFile> pFileObject;
pRequest->GetFileObject(&pFileObject);
if (pFileObject != NULL)
{
DWORD logLen=300;
pFileObject->RetrieveFileName(log,&logLen);
instance=_wtoi(log+(logLen-2));
}
}
Reader &reader=*readers[instance];
if (ControlCode==IOCTL_SMARTCARD_GET_ATTRIBUTE) {
reader.IoSmartCardGetAttribute(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_IS_PRESENT) {
reader.IoSmartCardIsPresent(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_GET_STATE) {
reader.IoSmartCardGetState(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_IS_ABSENT) {
reader.IoSmartCardIsAbsent(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_POWER) {
reader.IoSmartCardPower(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_SET_ATTRIBUTE) {
reader.IoSmartCardSetAttribute(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_SET_PROTOCOL) {
reader.IoSmartCardSetProtocol(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_TRANSMIT) {
reader.IoSmartCardTransmit(pRequest,inBufSize,outBufSize);
return;
}
swprintf(log,L"[BixVReader][IOCT]ERROR_NOT_SUPPORTED:%08X",ControlCode);
OutputDebugString(log);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
return;
}
OnDeviceIoControl
is called when the driver receives a request, and it just dispatches the request to the CMyDevice
object. ControlCode
contains the IO control code of the request, and through the pRequest
object, we gain access to the associated input and output memory buffers.
Update
Since more than virtual one reader are present, we need to determine which one this I/O request addresses. This is done using the GetFileObject function, and the RetrieveFileName method of the IWDFFile interface. The file name ends with the "DEVx" string we set in the interface creation. The string parsing method is quite rude, but works, if you have less than 10 readers ( ...more 10 readers? What you need to do with them??). The request is then forwarded to the Reader object corresponding to the desired virtual reader.
The memory buffers are accessed through IWDFMemory
objects. The interface is quite straightforward, and doesn't need many explications.
In the code, there are some helper functions to set and get an integer, a buffer or a string
to and from the output and input buffers.
I/O Control Codes
Let's see which are the I/O control codes that a Smart Card Reader driver can receive:
IOCTL_SMARTCARD_GET_ATTRIBUTE
Quite easy. We just need to answer some easy questions: which is the vendor name, the reader name, the device unit (in case we have more than one reader with the same name), the communication protocol we support (according to the ATR) and the ATR string of the inserted card. The ATR is the only element that requires to communicate with the virtual card, as we'll see later.
These I/O requests are immediately completed, so pRequest->CompleteWithInformation
is called at the end of the ProcessIoControl
method.
Update
Since we added complexity with different reader types, reader names and RPC methods, we provide some information to the virtual smart card application with the reader configuration; so I implemented some custom values to read configuration data of a virtual reader:
void Reader::IoSmartCardGetAttribute(IWDFIoRequest* pRequest,SIZE_T inBufSize,SIZE_T outBufSize) {
UNREFERENCED_PARAMETER(inBufSize);
wchar_t log[300]=L"";
char temp[300];
DWORD code=getInt(pRequest);
swprintf(log,L"[BixVReader][GATT] - code %0X",code);
OutputDebugString(log);
switch(code) {
case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA009):
OutputDebugString(L"[BixVReader][GATT]RPC_TYPE");
setInt(device,pRequest,rpcType);
return;
case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00a):
if (rpcType==0) {
PipeReader *pipe=(PipeReader *)this;
OutputDebugString(L"[BixVReader][GATT]PIPE_NAME");
sprintf(temp,"%S",pipe->pipeName);
setString(device,pRequest,(char*)temp,(int)outBufSize);
}
else {
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
}
return;
case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00b):
if (rpcType==0) {
PipeReader *pipe=(PipeReader *)this;
OutputDebugString(L"[BixVReader][GATT]EVENT_PIPE_NAME");
sprintf(temp,"%S",pipe->pipeEventName);
setString(device,pRequest,(char*)temp,(int)outBufSize);
}
else {
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
}
return;
case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00c):
if (rpcType==1) {
TcpIpReader *tcpIp=(TcpIpReader *)this;
OutputDebugString(L"[BixVReader][GATT]PORT");
setInt(device,pRequest,tcpIp->port);
}
else {
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
}
return;
case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00d):
if (rpcType==1) {
TcpIpReader *tcpIp=(TcpIpReader *)this;
OutputDebugString(L"[BixVReader][GATT]EVENT_PORT");
setInt(device,pRequest,tcpIp->eventPort);
}
else {
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
}
return;
case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00e):
if (rpcType==1) {
TcpIpReader *tcpIp=(TcpIpReader *)this;
OutputDebugString(L"[BixVReader][GATT]BASE_PORT");
setInt(device,pRequest,tcpIp->portBase);
tcpIp;
}
else {
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
}
return;
case SCARD_ATTR_CHARACTERISTICS:
OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_CHARACTERISTICS");
setInt(device,pRequest,0);
return;
case SCARD_ATTR_VENDOR_NAME:
OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_VENDOR_NAME");
setString(device,pRequest,vendorName,(int)outBufSize);
return;
case SCARD_ATTR_VENDOR_IFD_TYPE:
OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_VENDOR_IFD_TYPE");
setString(device,pRequest,vendorIfdType,(int)outBufSize);
return;
case SCARD_ATTR_DEVICE_UNIT:
OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_DEVICE_UNIT");
setInt(device,pRequest,deviceUnit);
return;
case SCARD_ATTR_ATR_STRING:
OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_ATR_STRING");
BYTE ATR[100];
DWORD ATRsize;
if (!QueryATR(ATR,&ATRsize))
{
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(STATUS_NO_MEDIA, 0);
return;
}
setBuffer(device,pRequest,ATR,ATRsize);
return;
case SCARD_ATTR_CURRENT_PROTOCOL_TYPE:
OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_CURRENT_PROTOCOL_TYPE");
setInt(device,pRequest,protocol);
return;
default: {
swprintf(log,L"[BixVReader][GATT]ERROR_NOT_SUPPORTED:%08X",code);
OutputDebugString(log);
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
}
}
}
To read these configuration values you need to connect to the reader in SCARD_DIRECT mode and send some SCardGetAttribute commands. This code should return the RpcType used by the first virtual reader:
SCardConnect(hContext,"Bix VIRTUAL_CARD_READER 0",SCARD_SHARE_DIRECT, 0,&hCard, &dwProtocol);
DWORD iRpcType;
DWORD iRpcTypeLen=sizeof(DWORD);
SCardGetAttrib(hCard,SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA009), (LPBYTE)&iRpcType, &iRpcTypeLen);
The data types returned by SCardGetAttrib
for custom attributes are the following:
- RPC_PORT_BASE: DWORD
- RPC_TYPE: DWORD
- VENDOR_NAME: Variable length string
- VENDOR_IFD_TYPE: Variable length string
- DEVICE_UNIT: DWORD
- PIPE_NAME: Variable length string
- PIPE_EVENT_NAME: Variable length string
- TCP_PORT: DWORD
- TCP_EVENT_PORT: DWORD
IOCTL_SMARTCARD_IS_PRESENT and IOCTL_SMARTCARD_IS_ABSENT
This is a bit trickier. We are asked if a smart card is in the reader. This is the code:
void Reader::IoSmartCardIsPresent(IWDFIoRequest* pRequest,SIZE_T inBufSize,SIZE_T outBufSize) {
UNREFERENCED_PARAMETER(inBufSize);
UNREFERENCED_PARAMETER(outBufSize);
OutputDebugString(L"[BixVReader][IPRE]IOCTL_SMARTCARD_IS_PRESENT");
if (CheckATR()) {
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(STATUS_SUCCESS, 0);
}
else {
SectionLocker lock(device->m_RequestLock);
waitInsertIpr=pRequest;
IRequestCallbackCancel *callback;
device->QueryInterface(__uuidof(IRequestCallbackCancel),(void**)&callback);
pRequest->MarkCancelable(callback);
callback->Release();
}
}
We try to communicate with the virtual card to ask its ATR; if the request succeeds, there's a card in the reader, otherwise not. In the first case, we just complete the I/O request to confirm that a card is present.
In the second case, we are actually starting to monitor the reader for card insertion. The I/O request is left pending (if we do not call CompleteWithInformation
, the UMDF framework automatically handles the pending request), and it will be completed as soon as a card is inserted. We just store the pointer to the pending request in waitInsertIpr
to remember that this request is still open.
Moreover ,we should call pRequest->MarkCancelable
to inform the framework that this request is cancellable (in case the device is deactivated, or when the system is shut down). CMyDevice
implements IRequestCallbackCancel
, so it can be notified of the deletion request.
For IOCTL_SMARTCARD_IS_ABSENT
it is obviously the opposite: the I/O request is completed when the smart card is removed.
IOCTL_SMARTCARD_GET_STATE
We are queried for the device state. This is quite easy, in our case: we just support two states: card absent and card present with protocol negotiated. In a real driver, we should of course handle more precise states. We just ask the virtual card ATR to check if it is present or not.
IOCTL_SMARTCARD_POWER
The card should be reset or unpowered. In case of a reset, we also return the ATR (if the virtual card is present).
IOCTL_SMARTCARD_SET_ATTRIBUTE
We could just ignore it. We just return SUCCESS
in case the SCARD_ATTR_DEVICE_IN_USE
parameter is set.
IOCTL_SMARTCARD_TRANSMIT
A command APDU should be sent to the smart card, and we should return the response. Not difficult, just some stuff to communicate to and from the virtual smart card handling process. We should remember to remove the SCARD_IO_REQUEST
structure before the APDU, and insert it before the response.
IPC with the Virtual Smart Card Process
Obviously, a driver can't have a user interface. But if I need to change the behavior of the virtual smart card, perhaps load and save its state, or just simulate its insertion and removal from the virtual reader, I definitely need a user interface to do it! So, the virtual reader driver should happily communicate with the outer world, sending requests and receiving responses to a process that will simulate the virtual smart card behavior. But - because there's always a but - perhaps we should remember that a driver, even a user mode driver, is not exactly a simple application. In this case, the problem is that this application lives in Session 0, isolated from the rest of the Session 1 - world.
I will not explain in detail the concept of Session 0 and 1, and the isolation of Session 0 in Vista and later OS (also because I'm not an authority in this subject). I will just say that a Session 0 and a Session 1 process can't communicate in some IPC modes: no thread and window messages, no Memory Mapped Files and no global names, so no shared synchronization objects (I also don't want the session 1 application to be elevated)... so, I see two alternatives:
Update
In the first version of the article only named pipes were implemented. Now I implemented both. They have almost the same behaviour, so, according to your domain, you can have both alternatives.
First, we see the pipe server function:
DWORD PipeReader::startServer() {
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
CreateMyDACL(&sa);
wchar_t temp[300];
swprintf(temp,L"\\\\.\\pipe\\%s",pipeName);
HANDLE _pipe=CreateNamedPipe(temp,PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,PIPE_TYPE_BYTE,PIPE_UNLIMITED_INSTANCES,0,0,0,&sa);
swprintf(temp,L"\\\\.\\pipe\\%s",pipeEventName);
HANDLE _eventpipe=CreateNamedPipe(temp,PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,PIPE_TYPE_BYTE,PIPE_UNLIMITED_INSTANCES,0,0,0,&sa);
wchar_t log[300];
swprintf(log,L"[BixVReader]Pipe created:%s:%08x",pipeName,_pipe);
OutputDebugString(log);
while (true) {
BOOL ris=ConnectNamedPipe(_pipe,NULL);
if (ris==0) {
swprintf(log,L"[BixVReader]Pipe NOT connected:%x",GetLastError());
OutputDebugString(log);
}
else {
swprintf(log,L"[BixVReader]Pipe connected");
OutputDebugString(log);
}
ris=ConnectNamedPipe(_eventpipe,NULL);
if (ris==0) {
swprintf(log,L"[BixVReader]Event Pipe NOT connected:%x",GetLastError());
OutputDebugString(log);
}
else {
swprintf(log,L"[BixVReader]Event Pipe connected");
OutputDebugString(log);
}
pipe=_pipe;
eventpipe=_eventpipe;
if (waitInsertIpr!=NULL) {
SectionLocker lock(device->m_RequestLock);
if (initProtocols()) {
if (waitInsertIpr->UnmarkCancelable()==S_OK)
waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
waitInsertIpr=NULL;
state=SCARD_SWALLOWED;
}
}
while (true) {
DWORD command=0;
DWORD read=0;
if (!ReadFile(eventpipe,&command,sizeof(DWORD),&read,NULL)) {
state=SCARD_ABSENT;
OutputDebugString(L"[BixVReader]Pipe error");
powered=0;
pipe=NULL;
eventpipe=NULL;
if (waitRemoveIpr!=NULL) { SectionLocker lock(device->m_RequestLock);
OutputDebugString(L"[BixVReader]complete Wait Remove");
if (waitRemoveIpr->UnmarkCancelable()==S_OK) {
OutputDebugString(L"[BixVReader]Wait Remove Unmarked");
waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
OutputDebugString(L"[BixVReader]Wait Remove Completed");
}
waitRemoveIpr=NULL;
}
if (waitInsertIpr!=NULL) { SectionLocker lock(device->m_RequestLock);
OutputDebugString(L"[BixVReader]cancel Wait Remove");
if (waitInsertIpr->UnmarkCancelable()==S_OK) {
OutputDebugString(L"[BixVReader]Wait Insert Unmarked");
waitInsertIpr->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_CANCELLED), 0);
OutputDebugString(L"[BixVReader]Wait Insert Cancelled");
}
waitInsertIpr=NULL;
}
DisconnectNamedPipe(_pipe);
DisconnectNamedPipe(_eventpipe);
break;
}
OutputDebugString(L"[BixVReader]Pipe data");
if (command==0)
powered=0;
if (command==0 && waitRemoveIpr!=NULL) { SectionLocker lock(device->m_RequestLock);
state=SCARD_ABSENT;
if (waitRemoveIpr->UnmarkCancelable()==S_OK) {
waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
}
waitRemoveIpr=NULL;
}
else if (command==1 && waitInsertIpr!=NULL) { SectionLocker lock(device->m_RequestLock);
state=SCARD_SWALLOWED;
initProtocols();
if (waitInsertIpr->UnmarkCancelable()==S_OK) {
waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
}
waitInsertIpr=NULL;
}
}
}
OutputDebugString(L"[BixVReader]Pipe quit!!!");
return 0;
}
The TcpIp server is almost identical:
DWORD TcpIpReader::startServer() {
breakSocket=false;
wchar_t log[300];
while (true) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(socket, &readfds);
timeval tv = { 0 };
tv.tv_sec = 5;
while(true) {
if (breakSocket)
return 0;
FD_SET(socket, &readfds);
int ret = select(0, &readfds, NULL, NULL, &tv);
if (ret > 0)
break;
if (ret<0) {
wchar_t log[100];
DWORD err=WSAGetLastError();
swprintf(log,L"[BixVReader]wsa err:%x",err);
OutputDebugString(log);
if (err==0x2736) {
socket=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0);
sockaddr_in Service;
Service.sin_family = AF_INET;
Service.sin_addr.s_addr = inet_addr("127.0.0.1");
Service.sin_port = htons((u_short)(port));
bind(socket, (SOCKADDR *) &Service, sizeof (Service));
listen(socket, 1);
FD_ZERO(&readfds);
FD_SET(socket, &readfds);
}
}
}
SOCKET AcceptEventSocket;
AcceptSocket = accept(socket, NULL, NULL);
closesocket(socket);
socket=NULL;
if (AcceptSocket == INVALID_SOCKET)
return 0;
swprintf(log,L"[BixVReader]Socket connected:%i",AcceptSocket);
OutputDebugString(log);
FD_ZERO(&readfds);
FD_SET(eventsocket, &readfds);
while(true) {
if (breakSocket)
return 0;
FD_SET(eventsocket, &readfds);
int ret = select(0, &readfds, NULL, NULL, &tv);
if (ret > 0)
break;
if (ret<0) {
DWORD err=WSAGetLastError();
swprintf(log,L"[BixVReader]wsa err:%x",err);
OutputDebugString(log);
if (err==0x2736) {
eventsocket=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0);
sockaddr_in eventService;
eventService.sin_family = AF_INET;
eventService.sin_addr.s_addr = inet_addr("127.0.0.1");
eventService.sin_port = htons((u_short)(eventPort));
bind(eventsocket, (SOCKADDR *) &eventService, sizeof (eventService));
listen(eventsocket, 1);
FD_ZERO(&readfds);
FD_SET(eventsocket, &readfds);
}
}
}
AcceptEventSocket = accept(eventsocket, NULL, NULL);
closesocket(eventsocket);
eventsocket=NULL;
if (AcceptEventSocket == INVALID_SOCKET)
return 0;
swprintf(log,L"[BixVReader]Event Socket connected:%i",AcceptEventSocket);
OutputDebugString(log);
if (waitInsertIpr!=NULL) {
if (initProtocols()) {
SectionLocker lock(device->m_RequestLock);
if (waitInsertIpr->UnmarkCancelable()==S_OK)
waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
waitInsertIpr=NULL;
state=SCARD_SWALLOWED;
}
}
while (true) {
DWORD command=0;
int read=0;
if ((read=recv(AcceptEventSocket,(char*)&command,sizeof(DWORD),MSG_WAITALL))<=0) {
state=SCARD_ABSENT;
OutputDebugString(L"[BixVReader]Socket error");
powered=0;
::shutdown(AcceptSocket,SD_BOTH);
::shutdown(AcceptEventSocket,SD_BOTH);
if (waitRemoveIpr!=NULL) {
OutputDebugString(L"[BixVReader]complete Wait Remove");
SectionLocker lock(device->m_RequestLock);
if (waitRemoveIpr->UnmarkCancelable()==S_OK) {
OutputDebugString(L"[BixVReader]Wait Remove Unmarked");
waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
OutputDebugString(L"[BixVReader]Wait Remove Completed");
}
waitRemoveIpr=NULL;
}
if (waitInsertIpr!=NULL) {
OutputDebugString(L"[BixVReader]cancel Wait Remove");
SectionLocker lock(device->m_RequestLock);
if (waitInsertIpr->UnmarkCancelable()==S_OK) {
OutputDebugString(L"[BixVReader]Wait Insert Unmarked");
waitInsertIpr->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_CANCELLED), 0);
OutputDebugString(L"[BixVReader]Wait Insert Cancelled");
}
waitInsertIpr=NULL;
}
break;
}
OutputDebugString(L"[BixVReader]Socket data");
if (command==0)
powered=0;
if (command==0 && waitRemoveIpr!=NULL) {
SectionLocker lock(device->m_RequestLock);
state=SCARD_ABSENT;
if (waitRemoveIpr->UnmarkCancelable()==S_OK)
waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
waitRemoveIpr=NULL;
}
else if (command==1 && waitInsertIpr!=NULL) {
SectionLocker lock(device->m_RequestLock);
state=SCARD_SWALLOWED;
initProtocols();
if (waitInsertIpr->UnmarkCancelable()==S_OK)
waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
waitInsertIpr=NULL;
}
}
}
OutputDebugString(L"[BixVReader]Socket quit!!!");
return 0;
}
First of all, we create two Named Pipes (or Tcp/Ip sockets). Why two? Easy: The first (SCardSimulatorDriver
) is for requests from the driver to the virtual smart card; the second (SCardSimulatorDriverEvents
) is for event notifications from the virtual smart card to the driver (insertion and removal).
With ConnectNamedPipe
, we wait for a client to connect. The call is blocking, so until someone opens the pipe, the thread stands waiting.
The utility function CreateMyDACL
(straight from MSDN) is used to set the appropriate DACL for the Named Pipe. In fact, if we used NULL
as DACL, the object would inherit the default settings of the parent process, and it would become inaccessible to client applications. Our custom DACL grants access to authenticated users, so all user processes are allowed to connect.
In case of Tcp/Ip, we close the listening socket soon after a client connects, since this is a single-client scenario.
When the connection is established, we check if a card is already inserted and we are monitoring (waitInsertIpr
and CheckATR()
); in this case, in fact, it is not sure that the virtual card would notify the driver, since there's no insertion event. The driver would continue to believe that there's no card inserted.
Update:
This first version of this driver supported just T=1 protocol. Now it parses the ATR to look for TDi bytes that define accepted protocols. So, when a client connects, we ask its ATR to know if a specific protocol is available.
Then the main communication loop starts. The driver thread stands waiting for events from the virtual smart card, calling ReadFile
. The data sent on this pipe is trivial: a single DWORD
with value 0
for removal and 1
for insertion. When an event arrives, if we are waiting for that event (waitRemoveIpr
or waitInsertIpr
), we complete that I/O request accordingly. Since these requests were marked as cancellable, we need to unmak them with UnmarkCancelable
and, if possible, complete them (UnmarkCancelable
could fail if it is too late and the request was already cancelled) (note that in the OnCancel
callback, we don't need to call UnmarkCancelable
).
If a ReadFile
call fails, it means that we lost the connection with the virtual card. Perhaps the application was closed, and so the pipes (or sockets). In this case, we simply notify the removal of the card (if requested) and we restart waiting for a new client to connect.
Let's see what happens when the driver needs to send a request to the virtual smart card; the method QueryTransmit
does this job:
bool PipeReader::QueryTransmit(BYTE *APDU,int APDUlen,BYTE *Resp,int *Resplen) {
if (pipe==NULL)
return false;
DWORD command=2;
DWORD read=0;
if (!WriteFile(pipe,&command,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
return false;
}
DWORD dwAPDUlen=(DWORD)APDUlen;
if (!WriteFile(pipe,&dwAPDUlen,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
return false;
}
if (!WriteFile(pipe,APDU,APDUlen,&read,NULL)) {
pipe=NULL;
return false;
}
FlushFileBuffers(pipe);
DWORD dwRespLen=0;
if (!ReadFile(pipe,&dwRespLen,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
return false;
}
if (!ReadFile(pipe,Resp,dwRespLen,&read,NULL)) {
pipe=NULL;
return false;
}
(*Resplen)=(int)dwRespLen;
return true;
}
First we check if a pipe is connected; otherwise, we return a fail. Then we write on the pipe the APDU command. The protocol is very simple:
- a
DWORD
containing the command code (TRANSMIT=2
) - a
DWORD
containing the length of the APDU - the APDU buffer
We don't need anything more... we just wait for the response to come. we read a DWORD
containing the length of the response, and the response buffer. That's it. If any operation in the pipe fails, probably the communication is broken, so we return a fail and we wait for a new connection to come.
Every time we write something on the pipe, we should always call FlushFileBuffers
to be sure that the buffer is sent to the other side of the pipe and is not buffered; otherwise, the listening application could not receive our data.
Notes
This implementation of IPC is very simple. A bit too
simple. The I/O requests are synchronized, but the events received from the virtual smart cards are not. so, before setting the pipe handle to NULL
, it would be wise to acquire a lock on it... I'm lazy, it works 99% of times and I just need it for testing purposes... so I didn't do it. Shame on me.
Update: I'm not lazy anymore!
From the very first version of this driver I always found a flaw that puzzeled me: sometimes the virtual reader simply disappeared from the reader list returned by SCardListReaders. I tried many times to fix it without success, until I took a look at
this sample from Microsoft WinDDK 8.1. In this sample, every
IWDFIoRequest related operation is surrounded by a CriticalSection Enter/Leave. It was Columbus's egg! I did the same and the stability of the driver gratly improved. I never discovered where and why such a concurrent access happens, but I'm convinced that multithreading requires a leap of faith. So, in this version the access to the request object is synchronized, and the driver is more stable.
Virtual Smart Card Application
Update
During the update of this article I wrote a full .net implementation of an ISO7816 virtual smart card.
Since the virtual smart card project is rather complex, I decided to separate it in a different article. The article is in draft but I hope to release it as soon as possible.
Compile, Install and Debug
No surprise, to compile the driver you need to install WDK 7.1. Just 620MB to download from Microsoft and we are ready to build the driver. Compiling a driver is not exactly the same as compiling a regular DLL. For example, the subsystem is not WINDOWS but is NATIVE; and the library path should be set accordingly to the architecture for which the driver is being compiled. This is the reason why we don't have a Visual Studio Solution for it, but we'll use the build environment that comes with the WDK, already correctly configured, from a command line. If you installed the WDK correctly, in the start menu, you have the links to various environments: Start > Programs > Windows Drivers Kit > WDK 7600.16385.1 > Build Environments > %OS version% > %TargetCPU% Free/Checked Build Environment.
The Checked Build environment is like a Debug build: optimizations are turned off and conditional code for debugging is included.
The Free Build is like a Release build: the code is optimized and debugging code is disabled.
Note that even in a Free Build, you can use a user mode debugger to step through your code.
To compile the virtual driver, open the correct Build Environment, cd
to the directory that contains the code and the subdir for the correct OS version (tested by now on Win 7 and Win XP - the settings of the sources
file are slightly different), and just type build
.
Et voilà, the DLL is built! ... as long as there's no compilation error. Note that in this build environments, warnings are treated as errors!. They are not reported by the build
tool, but they are dumped to a file named, for example, "buildfre_win7_amd64.wrn", if you are building a driver for windows 7, x64, Free Build.
To configure the build
tool, modify the sources
file, adding new source files or libraries to link against if needed.
Once the driver if built, you can find the DLL in the folder (if Win7, x64, Free) objfre_win7_amd64\amd64, together with the .inf file for installation.
To install the driver, we'll use the DevCon utility that comes with WDK (in %WinDDK%\tools\devcon\%Architecture%). Also, we need to copy the file WUDFUpdate_01009.dll (in %WinDDK%\redist\wdf\%Architecture%) to the path where our DLL is located.
To install the driver, run from an elevated shell:
devcon install BixVReader.inf root\BixVirtualReader
The syntax of devcon install
is:
devcon [-r] install <inf> <hwid>
Where <inf>
is the path to the .inf file, and <hwid>
is the hardware ID of the device that we are installing. It should match the ID in the .inf file at the line:
%ReaderDeviceName%=VReader_Install,root\BixVirtualReader
The -r
switch asks to reboot only if a reboot is required.
If the installation succeeds, the user is asked for confirmation, since the driver is not signed, and the virtual reader is installed.
You can download the zip archive with the compiled binaries for Windows 7, 32 and 64 bits and Windows XP. In the zip file, you can find the DLL and the inf file, but you'll need devcon.exe for your architecture (it is not redistributable, but is part of WDK). The driver was ONLY tested under Win7 x64
If you start the Virtual Smart Card application, you should be able to connect to the virtual card. In Win7, as soon as you insert the card, some minidrivers will try to communicate with the card, sending various commands (as long as the driver simulates just one reader. See here "Smart card solutions that are not compatible with Plug and Play" - Multislot smart card readers that create only one device node for all available slots in the smart card reader. This is exactly the situation of the minidriver ). Since the card always answers 9000 (that is "OK"), a minidriver could be faked to match it, and you'll see a known card in the device manager! In the log listbox
of the application, you'll see the APDUs sent to the card.
Debugging the driver is actually very easy: since it's a user-mode driver, you can use a user-mode debugger: Visual Studio, for example. All you have to do is attach to a running process, and select WUDFHost.exe. Since you could have more than one UMDF driver in your system, you could also have more than one instance of WUDFHost.exe. Which is the correct one? You can use Procexp from Sysinternals to search which processes load your DLL, and check the PID of the correct WUDFHost.exe. Remember that both Procexp and Visual Studio should be Run as Administrator, otherwise they will not be able to attach to the driver process.
When you need to re-compile and re-test your driver, you need to update the driver DLL file.
You can do it simply launching the command:
devcon update BixVReader.inf root\BixVirtualReader
from the same shell used to install it.
Note that the device will be disabled, updated and reenabled, but if you have pending I/O requests, the system will not be able to disable it, and you will need to reboot the system for changes to take place.
In that case, you can do the following:
- Open Device Manager
- Disable the virtual device
- If there are still pending I/O requests, kill the process of WUDFHost.exe (the one that hosts BixVReader.dll)
- Update the driver with the command line above
- Enable the device
As soon as you enable the device, the driver is loaded and executed. If you need to debug your driver from the very beginning, you can set the registry key
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\{193a1820-d9ac-4997-8c55-be817523f6aa}/HostProcessDbgBreakOnStartto the number of seconds that you want the host process to wait for a debugger to connect before starting.
It is useful, for debugging purposes, to have a trace or a kind of output from the driver. Even if WDK has a built in tracing system, through the WPP trace, I preferred to use a simple OutputDebugString
. Just for speed of development, I preferred to use a well-known method instead of learning a new one, but this is just personal taste... Using Dbgview, also from Sysinternals, Run as Administrator, or attaching a debugger, you can easily see the trace from your driver.
The .INF File
The .INF file used to install the virtual device is almost identical to the one from the UMDFSkeleton
example. Just one row was added:
UmdfKernelModeClientPolicy=AllowKernelModeClients
To allow a kernel-mode driver to load above the user-mode driver and to deliver requests from the kernel-mode to the user-mode driver.
I'm not exactly sure of which kernel mode driver runs above the virtual reader driver, but removing this line from the inf file, we simply do not get any I/O request notifications in our Queue object.
Conclusion
As I already said above, this is not by far an article on how a driver should be made. I'm quite sure that an expert driver developer would scream looking at my code. This is just a development tool for who, like me, works often on smart cards and needs to "play" with them. I hope that it will be useful for someone, and of course I wait for some serious driver developer to tell me how it should have been done. :)