/* * ccid.c Version 0.1 * * Copyright (c) 2005 Gianni Tedesco * * USB class driver for chipcard (smartcard) interface devices * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include /* Take a major from the local/experimental range */ #define CCI_MAJOR 244 static struct class_simple *ccid_class; /* Yes of course, all the world is a PC.... (or a RDR) */ #define RDR_to_PC_NotifySlotChange 0x50 #define RDR_to_PC_HardwareError 0x51 #define PC_to_RDR_LccPowerOn 0x62 #define PC_to_RDR_XfrBlock 0x6f /* FIXME */ #define CCID_MAX_ATR_LEN 0x100 struct ccid_xfr { void * __user out; void * __user in; u32 out_len; u32 in_len; }; struct ccid_desc { u8 bLength; u8 bDescriptorType; u16 bcdCCID; u8 bMaxSlotIndex; u8 bVoltageSupport; u32 dwProtocols; u32 dwDefaultClock; u32 dwMaximumClock; u8 bNumClockSupported; u32 dwDataRate; u32 dwMaxDataRate; u8 bNumDataRatesSupporrted; u32 dwMaxIFSD; u32 dwSynchProtocols; u32 dwMechanical; u32 dwFeatures; u32 dwMaxCCIDMessageLength; u8 bClassGetResponse; u8 bClassEnvelope; u16 wLcdLayout; u8 bPINSupport; u8 bMaxCCIDBusySlots; }__attribute__((packed)); struct ccid_msg { u8 bMessageType; u32 dwLength; u8 bSlot; u8 bSeq; union { struct { u8 bStatus; u8 bError; u8 bApp; }in __attribute__((packed)); struct { u8 bApp[3]; }out __attribute__((packed)); }; }__attribute__((packed)); struct ccid_slot { unsigned int index; int card_present; int minor; atomic_t avail; spinlock_t xfer_lock; struct list_head xfer; wait_queue_head_t wait; struct list_head cci_list; }; struct ccid { struct usb_interface *intf; struct ccid_desc *desc; u8 cmd, rsp, event; u8 event_interval; atomic_t ref; atomic_t seq; struct urb *ev_urb; struct ccid_slot slot[0]; }; struct list_head cci_list = LIST_HEAD_INIT(cci_list); static DECLARE_MUTEX(devlist_sem); static void vendor_extension(const char *thing, u32 code) { printk(KERN_DEBUG "ccid: Unknown %s: Probably a vendor extension " "please report code 0x%.8x\n", thing, code); } static int new_devnode(struct ccid_slot *slot) { static u32 i; char name[32]; slot->minor = i++; snprintf(name, sizeof(name), "ccid%d", slot->minor); if ( !class_simple_device_add(ccid_class, MKDEV(CCI_MAJOR, slot->minor), NULL, name) ) { i--; return 0; } INIT_LIST_HEAD(&slot->cci_list); list_add_tail(&cci_list, &slot->cci_list); return 1; } static struct ccid_slot *find_slot(int minor) { struct ccid_slot *s; list_for_each_entry(s, &cci_list, cci_list) if ( s->minor == minor ) return s; return NULL; } static void free_devnode(struct ccid_slot *slot) { class_simple_device_remove(MKDEV(CCI_MAJOR, slot->minor)); } #undef max #define max(x,y) ({ \ typeof(x) _x = (x); \ typeof(y) _y = (y); \ (void)((void *)&_x == (void *)&_y); \ _x > _y ? x : y; }) /* 2 bits per slot needed for RDR_to_PC_NotifySlotChange */ static size_t slotchange_size(struct ccid *ccid) { size_t buf_sz; buf_sz = 2 * (ccid->desc->bMaxSlotIndex + 1); buf_sz |= (~(buf_sz & 0x7)) & 0x7; buf_sz /= 8; buf_sz = max(buf_sz, 2); return buf_sz; } static struct ccid *slot_ccid(struct ccid_slot *s) { u8 *ret; s = s - s->index; ret = (u8 *)s - offsetof(struct ccid, slot); return (struct ccid *)ret; } static void plugin_chipcard(struct ccid_slot *s) { } static void unplug_chipcard(struct ccid_slot *s) { } static int notify_slot_change(struct ccid *ccid, void *buf, size_t len) { u8 *byte; size_t i; if ( len != slotchange_size(ccid) ) { printk(KERN_ERR "ccid: Bad PC_to_RDR_NotifySlotChange " "length %u != %u\n", len, slotchange_size(ccid)); } byte = buf + 1; len -= 1; for(i=0; i < len; i++) { u8 cur, change, j; for(j=0; j < 8; j+=2) { cur = byte[i] & (0x1 << j); change = byte[i] & (0x2 << j); if ( change == 0 ) continue; if ( cur ) plugin_chipcard(&ccid->slot[i/4 + j/2]); else unplug_chipcard(&ccid->slot[i/4 + j/2]); printk(KERN_DEBUG "ccid: Card %s slot %i\n", cur ? "plugged in to" : "removed from", i/4 + j/2); } } return 0; } static int hw_error(struct ccid *ccid, void *buf, size_t len) { struct { u8 bMessageType; u8 bSlot; u8 bSeq; u8 bHardwareErrorCode; }*err = buf; if ( len < sizeof(*err) ) { printk(KERN_ERR "ccid: Malformed RDR_to_PC_HardwareError\n"); return -EIO; } if ( err->bHardwareErrorCode != 0x01 ) { vendor_extension("RDR_to_PC_HardwareError", err->bHardwareErrorCode); return -EIO; } printk("ccid: Slot %u (seq=0x%.2x) suffered an overcurrent\n", err->bSlot, err->bSeq); return 0; } static void ccid_event(struct urb *urb, struct pt_regs *pt) { struct ccid *ccid = urb->context; void *buf = urb->transfer_buffer; size_t len = urb->actual_length; if ( len < 1 ) return; switch ( *(u8 *)buf ) { case RDR_to_PC_NotifySlotChange: notify_slot_change(ccid, buf, len); break; case RDR_to_PC_HardwareError: hw_error(ccid, buf, len); break; default: vendor_extension("CCID Event", *(u8 *)buf); } } static int select_interface(struct ccid *ccid, int require_event) { struct usb_interface_descriptor *ifd; struct usb_endpoint_descriptor *epd; struct usb_device *dev; int i, e, ret = -EIO; dev = interface_to_usbdev(ccid->intf); for (i = 0; i < ccid->intf->num_altsetting; i++) { ifd = &ccid->intf->altsetting[i].desc; ccid->cmd = ccid->rsp = ccid->event = 0; for (e = 0; e < ifd->bNumEndpoints; e++) { unsigned int type, dir; epd = &ccid->intf->altsetting[i].endpoint[e].desc; type = epd->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK; dir = epd->bEndpointAddress & USB_ENDPOINT_DIR_MASK; if ( type == USB_ENDPOINT_XFER_BULK ) { if ( dir == USB_DIR_IN ) ccid->rsp = epd->bEndpointAddress; else ccid->cmd = epd->bEndpointAddress; }else if ( type == USB_ENDPOINT_XFER_INT ) { ccid->event = epd->bEndpointAddress; ccid->event_interval = epd->bInterval; } } if ( ccid->cmd == 0 ) continue; if ( ccid->rsp == 0 ) continue; if ( require_event && (ccid->event == 0) ) continue; ret = usb_set_interface(dev, ifd->bInterfaceNumber, ifd->bAlternateSetting); if ( ret == 0 ) return 0; } return ret; } static int setup_event_pipe(struct ccid *ccid) { size_t buf_sz; void *buf; int err; if ( !ccid->event ) { int i; /* Cloldplug all chipcard slots */ for(i=0; i <= ccid->desc->bMaxSlotIndex; i++) plugin_chipcard(ccid->slot + i); return 0; } ccid->ev_urb = usb_alloc_urb(0, GFP_ATOMIC); if ( ccid->ev_urb == NULL ) return -ENOMEM; /* and 4 bytes needed by RDR_to_PC_HardwareError */ buf_sz = max(slotchange_size(ccid), 4); buf = kmalloc(buf_sz, GFP_ATOMIC); if ( buf == NULL ) return -ENOMEM; usb_fill_int_urb(ccid->ev_urb, interface_to_usbdev(ccid->intf), usb_rcvintpipe(interface_to_usbdev(ccid->intf), ccid->event), buf, buf_sz, ccid_event, ccid, ccid->event_interval); err = usb_submit_urb(ccid->ev_urb, GFP_ATOMIC); if ( err ) return err; return 0; } static void ccid_get(struct ccid *ccid) { atomic_inc(&ccid->ref); } static void ccid_put(struct ccid *ccid) { if ( !atomic_dec_and_test(&ccid->ref) ) return; if ( ccid->ev_urb ) { kfree(ccid->ev_urb->transfer_buffer); usb_free_urb(ccid->ev_urb); } kfree(ccid); } static void ccid_disconnect(struct usb_interface *intf); static int ccid_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct ccid_desc *d; struct ccid *ccid; int i; int err; err = usb_get_extra_descriptor(intf->cur_altsetting, 0x21, &d); if ( err ) return -EIO; ccid = kmalloc(sizeof(*ccid) + (d->bMaxSlotIndex + 1) * sizeof(*ccid->slot), GFP_ATOMIC); if ( ccid == NULL ) return -ENOMEM; memset(ccid, 0, sizeof(*ccid)); ccid->intf = intf; ccid->desc = d; atomic_set(&ccid->ref, 0); atomic_set(&ccid->seq, 0); ccid_get(ccid); usb_set_intfdata(intf, ccid); err = select_interface(ccid, 1); if ( err ) goto out_err; printk(KERN_INFO "ccid: %u %sremovable chipcard slots at %u/%ubps\n", ccid->desc->bMaxSlotIndex + 1, (ccid->event == 0) ? "" : "non-", le32_to_cpu(ccid->desc->dwDataRate), le32_to_cpu(ccid->desc->dwMaxDataRate)); err = setup_event_pipe(ccid); if ( err ) goto out_err; down(&devlist_sem); for(i=0; i <= ccid->desc->bMaxSlotIndex; i++) { struct ccid_slot *s = ccid->slot + i; s->index = i; s->card_present = 0; atomic_set(&s->avail, 1); init_waitqueue_head(&s->wait); spin_lock_init(&s->xfer_lock); INIT_LIST_HEAD(&s->xfer); if ( !new_devnode(&ccid->slot[i]) ) { err = -EIO; goto out_err; } } up(&devlist_sem); return 0; out_err: ccid_disconnect(intf); return err; } static void ccid_disconnect(struct usb_interface *intf) { struct ccid *ccid = usb_get_intfdata(intf); int i; down(&devlist_sem); for(i=0; i <= ccid->desc->bMaxSlotIndex; i++) free_devnode(&ccid->slot[i]); up(&devlist_sem); ccid_put(ccid); } static struct usb_device_id ccid_ids[] = { { USB_INTERFACE_INFO(0xb, 0, 0) }, { USB_DEVICE(0x04e6, 0xe003) }, /* SCM SPR-532 */ { } }; static struct usb_driver ccid_driver = { .owner = THIS_MODULE, .name = "ccid", .probe = ccid_probe, .disconnect = ccid_disconnect, .id_table = ccid_ids, }; static struct urb *get_xfer_by_ptr(struct ccid_slot *s, struct urb *urb) { unsigned long flags; struct urb *u, *t; spin_lock_irqsave(&s->xfer_lock, flags); list_for_each_entry_safe(u, t, &s->xfer, urb_list) { if ( u == urb ) { list_del(&u->urb_list); spin_unlock_irqrestore(&s->xfer_lock, flags); return u; } } spin_unlock_irqrestore(&s->xfer_lock, flags); return NULL; } static struct urb *get_xfer_by_seq(struct ccid_slot *s, uint8_t seq) { unsigned long flags; struct ccid_msg *msg; struct urb *u, *t; spin_lock_irqsave(&s->xfer_lock, flags); list_for_each_entry_safe(u, t, &s->xfer, urb_list) { msg = u->transfer_buffer; if ( seq == msg->bSeq ) { list_del(&u->urb_list); spin_unlock_irqrestore(&s->xfer_lock, flags); return u; } } spin_unlock_irqrestore(&s->xfer_lock, flags); return NULL; } static void xfer_complete(struct urb *urb, struct pt_regs *pt) { struct ccid_slot *s = urb->context; unsigned long flags; spin_lock_irqsave(&s->xfer_lock, flags); list_add_tail(&urb->urb_list, &s->xfer); spin_unlock_irqrestore(&s->xfer_lock, flags); wake_up(&s->wait); } static struct urb *queue_response_packet(struct ccid_slot *s, int mem_flags) { struct ccid *ccid = slot_ccid(s); struct urb *urb; size_t pkt_sz; struct usb_device *dev; int pipe; void *buf; int ret = -ENOMEM; urb = usb_alloc_urb(0, mem_flags); if ( urb == NULL ) return ERR_PTR(ret); dev = interface_to_usbdev(ccid->intf); pipe = usb_rcvbulkpipe(dev, ccid->rsp); pkt_sz = usb_maxpacket(dev, pipe, 0); buf = kmalloc(pkt_sz, mem_flags); if ( buf == NULL ) goto out; usb_fill_bulk_urb(urb, dev, pipe, buf, pkt_sz, xfer_complete, s); urb->transfer_flags |= URB_ZERO_PACKET; ret = usb_submit_urb(urb, mem_flags); if ( ret ) goto out_free; return urb; out_free: kfree(buf); out: kfree(urb); return ERR_PTR(ret); } static struct urb *queue_command_packet(struct ccid_slot *s, struct ccid_msg *cmd, size_t cmdlen, int mem_flags) { struct ccid *ccid = slot_ccid(s); struct urb *urb; struct usb_device *dev; int pipe, ret; urb = usb_alloc_urb(0, mem_flags); if ( urb == NULL ) return ERR_PTR(-ENOMEM); dev = interface_to_usbdev(ccid->intf); pipe = usb_sndbulkpipe(dev, ccid->cmd); usb_fill_bulk_urb(urb, dev, pipe, cmd, cmdlen, xfer_complete, s); urb->transfer_flags |= URB_ZERO_PACKET; cmd->bSeq = atomic_inc_return(&ccid->seq) & 0xff; ret = usb_submit_urb(urb, GFP_KERNEL); if ( ret ) { kfree(urb); return ERR_PTR(ret); } return urb; } static int ccid_raw_cmd(struct ccid_slot *s, struct ccid_msg *cmd, size_t cmd_len, void * __user out, size_t out_len) { DEFINE_WAIT(wait); struct urb *urb, *c_urb; int ret = 0; c_urb = queue_command_packet(s, cmd, cmd_len, GFP_KERNEL); if ( IS_ERR(c_urb) ) return PTR_ERR(c_urb); /* FIXME: Handle signals */ while ( !(urb = get_xfer_by_ptr(s, c_urb)) ) { prepare_to_wait(&s->wait, &wait, TASK_INTERRUPTIBLE); if ( !(urb = get_xfer_by_ptr(s, c_urb)) ) schedule(); finish_wait(&s->wait, &wait); if ( urb ) break; } kfree(urb); c_urb = queue_response_packet(s, GFP_KERNEL); if ( IS_ERR(c_urb) ) return PTR_ERR(c_urb); while ( !(urb = get_xfer_by_seq(s, cmd->bSeq)) ) { prepare_to_wait(&s->wait, &wait, TASK_INTERRUPTIBLE); if ( !(urb = get_xfer_by_seq(s, cmd->bSeq)) ) schedule(); finish_wait(&s->wait, &wait); if ( urb ) break; } if ( urb->status ) { ret = urb->status; goto out; } if ( urb->actual_length > out_len ) urb->actual_length = out_len; /* TODO: Check for errors and see if we need to queue another urb */ if ( copy_to_user(out, urb->transfer_buffer, urb->actual_length) ) ret = -EFAULT; out: kfree(urb->transfer_buffer); kfree(urb); return ret; } static long cci_ioctl(struct file *f, unsigned int cmd, unsigned long arg) { struct ccid_slot *s = f->private_data; struct ccid_msg msg; int ret = -ENOIOCTLCMD; void __user *p = (void __user *)arg; memset(&msg, 0, sizeof(msg)); msg.bSlot = s->index; switch ( cmd ) { case 0: /* ATR */ msg.bMessageType = PC_to_RDR_LccPowerOn; ret = ccid_raw_cmd(s, &msg, sizeof(msg), p, CCID_MAX_ATR_LEN); break; case 1: { /* Xfer */ struct ccid_xfr buf; struct ccid_msg *xfr; size_t xlen; void *ptr; if ( copy_from_user(&buf, p, sizeof(buf)) ) return -EFAULT; if ( buf.out_len > PAGE_SIZE ) return -ENOMEM; msg.bMessageType = PC_to_RDR_XfrBlock; msg.dwLength = cpu_to_le32(buf.out_len); xlen = sizeof(*xfr) + buf.out_len; xfr = kmalloc(xlen, GFP_KERNEL); if ( xfr == NULL ) return - ENOMEM; ptr = xfr + 1; memcpy(xfr, &msg, sizeof(msg)); if ( copy_from_user(ptr, buf.out, buf.out_len) ) { kfree(xfr); return -EFAULT; } ret = ccid_raw_cmd(s, xfr, xlen, buf.in, buf.in_len); kfree(xfr); break; } default: printk(KERN_WARNING "ccid: invalid ioctl cmd (%u / 0x%.x)\n", cmd, cmd); } return ret; } static int cci_open(struct inode *i, struct file *f) { struct ccid_slot *s; int ret; down(&devlist_sem); s = find_slot(iminor(i)); if ( s == NULL ) { ret = -ENODEV; goto out; } if ( !atomic_dec_and_test(&s->avail) ) { ret = -EBUSY; goto out; } ccid_get(slot_ccid(s)); f->private_data = s; ret = 0; goto out; out: up(&devlist_sem); return ret; } static int cci_release(struct inode *i, struct file *f) { struct ccid_slot *s = f->private_data; atomic_inc(&s->avail); ccid_put(slot_ccid(s)); return 0; } static struct file_operations cci_fops = { .owner = THIS_MODULE, .unlocked_ioctl = cci_ioctl, .open = cci_open, .release = cci_release, }; static int __init ccid_init(void) { int err; err = register_chrdev(CCI_MAJOR, "cci", &cci_fops); if ( err ) return err; ccid_class = class_simple_create(THIS_MODULE, "cci"); if ( ccid_class == NULL ) { err = ENOMEM; goto err_chrdev; } err = usb_register(&ccid_driver); if ( err ) goto err_class; return 0; err_class: class_simple_destroy(ccid_class); err_chrdev: unregister_chrdev(CCI_MAJOR, "cci"); return err; } static void __exit ccid_exit(void) { usb_deregister(&ccid_driver); class_simple_destroy(ccid_class); unregister_chrdev(CCI_MAJOR, "cci"); } module_init(ccid_init); module_exit(ccid_exit); MODULE_AUTHOR("Gianni Tedesco "); MODULE_DESCRIPTION("USB Chip Card Interface Device Driver"); MODULE_LICENSE("GPL");