최근 ACR1252U NFC 리더기를 사서 가지고 노는 중이다. 리눅스에서 이 제품을 사용하기 위해서는 PCSCLite와 CCID 드라이버를 설치해야 한다. 그런데 아치리눅스에서 제공되는 드라이버가 2개나 있었다:
$ pacman -Ss ccid community/acsccid 1.1.8-1 PC/SC driver that supports ACS CCID smart card readers. community/ccid 1.4.35-1 [installed] A generic USB Chip/Smart Card Interface Devices driver
개인적으로 미니멀을 추구하기에 ccid 패키지만 설치하고 메뉴얼을 보면서 코딩해봤다. 그런데 SCardControl API를 사용하는 명령들(리더기 버전, 부저, LED 설정 등)이 먹히지 않았다. 리더기 제어 명령 이외의 SCardTransmit API를 사용하는 것들은 문제없었고, 윈도우 환경에서 SDK와 딸려온 데모 프로그램을 사용하면 해당 명령들이 잘 작동했다. 똑같은 API를 사용했는데도 리눅스 환경에서만 작동하지 않으니 환장할 노릇이었다. 이것 때문에 하루를 꼬박 날려먹고 혹시나 싶어서 acsccid 패키지를 설치하니 메뉴얼대로 작동했다.
보통 사람이면 그냥 이걸로 넘어가겠지만 나는 ccid 패키지 만으로 해당 기능을 사용하는 방법을 찾고 싶었다. 분명 ccid와 acsccid 소스코드에 차이점이 있을테니 SCardControl이 호출하는 IFDHControl 함수의 차이를 집중적으로 찾아봤다. acsccid 패키지의 IFDHControl 함수에 아래와 같이 escape command를 실행시키는 코드가 있었다:
// MS CCID I/O control code for escape command if ((IOCTL_CCID_ESCAPE == dwControlCode) || (WINIOCTL_CCID_ESCAPE == dwControlCode)) { unsigned int iBytesReturned; iBytesReturned = RxLength; return_value = CmdEscape(reader_index, TxBuffer, TxLength, RxBuffer, &iBytesReturned, -1); // Infinite *pdwBytesReturned = iBytesReturned; }
IOCTL_CCID_ESCAPE 매크로가 SCARD_CTL_CODE(3500)로 정의되어 있는걸 보니 더욱 확실하다.
이번에는 ccid 패키지의 IFDHControl 함수에서 동일한 기능을 하는 부분을 찾아봤다:
if (IOCTL_SMARTCARD_VENDOR_IFD_EXCHANGE == dwControlCode) { int allowed = (DriverOptions & DRIVER_OPTION_CCID_EXCHANGE_AUTHORIZED); int readerID = ccid_descriptor -> readerID; ... if (!allowed) { DEBUG_INFO1("ifd exchange (Escape command) not allowed"); return_value = IFD_COMMUNICATION_ERROR; } else { unsigned int iBytesReturned; iBytesReturned = RxLength; /* 30 seconds timeout for long commands */ return_value = CmdEscape(reader_index, TxBuffer, TxLength, RxBuffer, &iBytesReturned, 30*1000); *pdwBytesReturned = iBytesReturned; } }
여기서는 IOCTL_SMARTCARD_VENDOR_IFD_EXCHANGE 매크로 값이 컨트롤 코드로 들어오고 DriverOptions에 DRIVER_OPTION_CCID_EXCHANGE_AUTHORIZED 플래그가 설정되어 있다면 escape command로 처리하는 코드가 있었다. 이건 acsccid에도 똑같이 들어있다. 그러므로 DriverOptions 플래그를 설정할 수 있다면 설치된 게 ccid든 acsccid든 상관없이 SCARD_CTL_CODE(1)을 컨트롤 코드로 사용하면 해당 기능을 사용할 수 있다.
DriverOptions값은 드라이버가 사용될 수 있는 장치 리스트 XML 파일인 Info.plist에서 가져온다. 이 값은 기본값이 0으로 어떤 플래그도 설정되어 있지 않다. /usr/lib/pcsc/drivers/ifd-ccid.bundle/Contents/Info.plist 파일의 ifdDriverOptions 바로 아래에 있는 값을 0x0001로 바꾸면 된다:
<key>ifdDriverOptions</key> <string>0x0001</string> <!-- Possible values for ifdDriverOptions 0x01: DRIVER_OPTION_CCID_EXCHANGE_AUTHORIZED the CCID Exchange command is allowed. You can use it through SCardControl(hCard, IOCTL_SMARTCARD_VENDOR_IFD_EXCHANGE, ...)
그리고 SCardControl API를 실행할 때 컨트롤 코드를 SCARD_CTL_CODE(3500) 대신 SCARD_CTL_CODE(1)를 사용하면 된다. 아래는 리더기 버전을 가져오는 코드이다. 코드 크기를 줄이기 위해 에러 처리는 모두 뺐다.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <PCSC/winscard.h> #include <PCSC/reader.h> void print_hex(unsigned char* data,unsigned long size) { for(int i = 0;i < size;i++) printf("%02X ",data[i]); printf("\n"); } int main() { SCARDCONTEXT ctx; SCardEstablishContext(SCARD_SCOPE_SYSTEM,NULL,NULL,&ctx); unsigned long length; SCardListReaders(ctx,NULL,NULL,&length); char* readers = malloc(length); SCardListReaders(ctx,NULL,readers,&length); char* p = readers; char* picc = NULL; while(*p) { if(strstr(p,"PICC") != NULL) picc = p; p += strlen(p) + 1; } SCARDHANDLE card; unsigned long activeProtocol; SCardConnect(ctx,picc,SCARD_SHARE_DIRECT,0,&card,&activeProtocol); free(readers); unsigned char apdu[] = { 0xE0, 0x00, 0x00, 0x18, 0x00}; unsigned char resp_apdu[255]; unsigned long returnedBytes = 0; SCardControl(card,SCARD_CTL_CODE(1),apdu,sizeof(apdu),resp_apdu,sizeof(resp_apdu),&returnedBytes); printf("returned: %ld\n",returnedBytes); print_hex(resp_apdu,returnedBytes); resp_apdu[returnedBytes] = 0; printf("version: %s\n",resp_apdu + 5); SCardDisconnect(card,SCARD_RESET_CARD); SCardReleaseContext(ctx); return 0; }
컴파일은 아래와 같이 한다:
gcc `pkg-config --libs --cflags libpcsclite` -o version version.c
실행 결과는 다음과 같다:
$ ./version returned: 20 E1 00 00 00 0F 41 43 52 31 32 35 32 55 5F 56 31 31 30 2E 30 version: ACR1252U_V110.0