#! /usr/bin/python """ pinpadtest.py - a tool to test variable length pin entry with pinpad Copyright (C) 2011, 2012, 2013 Free Software Initiative of Japan Author: NIIBE Yutaka This file is a part of Gnuk, a GnuPG USB Token implementation. Gnuk 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 3 of the License, or (at your option) any later version. Gnuk 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, see . """ import sys # Assume only single CCID device is attached to computer with card from smartcard.CardType import AnyCardType from smartcard.CardRequest import CardRequest from smartcard.util import toHexString from getpass import getpass CM_IOCTL_GET_FEATURE_REQUEST = (0x42000000 + 3400) CM_IOCTL_VENDOR_IFD_EXCHANGE = (0x42000000 + 1) FEATURE_VERIFY_PIN_DIRECT = 0x06 FEATURE_MODIFY_PIN_DIRECT = 0x07 BY_ADMIN = 3 BY_USER = 1 PIN_MIN_DEFAULT = 6 # min of OpenPGP card PIN_MAX_DEFAULT = 15 # max of VASCO DIGIPASS 920 def s2l(s): return [ ord(c) for c in s ] def confirm_pin_setting(single_step): if single_step: return 0x01 # bConfirmPIN: new PIN twice else: return 0x03 # bConfirmPIN: old PIN and new PIN twice class Card(object): def __init__(self, add_a_byte, pinmin, pinmax, fixed): cardtype = AnyCardType() cardrequest = CardRequest(timeout=10, cardType=cardtype) cardservice = cardrequest.waitforcard() self.connection = cardservice.connection self.verify_ioctl = -1 self.modify_ioctl = -1 self.another_byte = add_a_byte self.pinmin = pinmin self.pinmax = pinmax self.fixed = fixed def get_features(self): p = self.connection.control(CM_IOCTL_GET_FEATURE_REQUEST, []) i = 0 while i < len(p): code = p[i] l = p[i+1] i = i + 2 if l == 4: ioctl = (p[i] << 24) | (p[i+1] << 16) | (p[i+2] << 8) | p[i+3] i = i + l else: i = i + l continue if code == FEATURE_VERIFY_PIN_DIRECT: self.verify_ioctl = ioctl elif code == FEATURE_MODIFY_PIN_DIRECT: self.modify_ioctl = ioctl if self.verify_ioctl == -1: raise ValueError, "Not supported" def cmd_select_openpgp(self): apdu = [0x00, 0xa4, 0x04, 0x00, 6, 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01 ] response, sw1, sw2 = self.connection.transmit(apdu) if sw1 == 0x61: # More data response, sw1, sw2 = self.connection.transmit([0x00, 0xc0, 0, 0, sw2]) elif not (sw1 == 0x90 and sw2 == 0x00): raise ValueError, ("cmd_select_openpgp %02x %02x" % (sw1, sw2)) def possibly_add_dummy_byte(self): if self.another_byte: return [ 0 ] else: return [] def cmd_vega_alpha_disable_empty_verify(self): apdu = [ 0xB5, # -| 0x01, # | Pre-command parameters 0x00, # -| 0x03, # retry counter value (fixed value) 0x00 # enable 3s timeout ] data = self.connection.control(CM_IOCTL_VENDOR_IFD_EXCHANGE, apdu) def cmd_verify_pinpad(self, who): apdu = [0x00, 0x20, 0x00, 0x80+who ] pin_verify = [ 0x00, # bTimeOut 0x00, # bTimeOut2 0x82, # bmFormatString: Byte, pos=0, left, ASCII. self.fixed, # bmPINBlockString 0x00, # bmPINLengthFormat self.pinmax, # wPINMaxExtraDigit Low (PINmax) self.pinmin, # wPINMaxExtraDigit High (PINmin) 0x02, # bEntryValidationCondition 0x01, # bNumberMessage 0x00, # wLangId Low 0x00, # wLangId High 0x00, # bMsgIndex 0x00, # bTeoPrologue[0] 0x00, # bTeoPrologue[1] ] if self.fixed > 0: apdu += [ self.fixed ] apdu += [ 255 ] * self.fixed else: apdu += self.possibly_add_dummy_byte() pin_verify += [ len(apdu) ] # bTeoPrologue[2] pin_verify += [ len(apdu), 0, 0, 0 ] + apdu data = self.connection.control(self.verify_ioctl,pin_verify) sw1 = data[0] sw2 = data[1] if not (sw1 == 0x90 and sw2 == 0x00): raise ValueError, ("cmd_verify_pinpad %02x %02x" % (sw1, sw2)) def send_modify_pinpad(self, apdu, single_step, command): if self.modify_ioctl == -1: raise ValueError, "Not supported" pin_modify = [ 0x00, # bTimerOut 0x00, # bTimerOut2 0x82, # bmFormatString: Byte, pos=0, left, ASCII. self.fixed, # bmPINBlockString 0x00, # bmPINLengthFormat 0x00, # bInsertionOffsetOld self.fixed, # bInsertionOffsetNew self.pinmax, # wPINMaxExtraDigit Low (PINmax) self.pinmin, # wPINMaxExtraDigit High (PINmin) confirm_pin_setting(single_step), 0x02, # bEntryValidationCondition 0x03, # bNumberMessage 0x00, # wLangId Low 0x00, # wLangId High 0x00, # bMsgIndex1 0x01, # bMsgIndex2 0x02, # bMsgIndex3 0x00, # bTeoPrologue[0] 0x00, # bTeoPrologue[1] ] if self.fixed > 0: apdu += [ 2*self.fixed ] apdu += [ 255 ] * (2*self.fixed) else: apdu += self.possibly_add_dummy_byte() pin_modify += [ len(apdu) ] # bTeoPrologue[2] pin_modify += [ len(apdu), 0, 0, 0 ] + apdu data = self.connection.control(self.modify_ioctl,pin_modify) sw1 = data[0] sw2 = data[1] if not (sw1 == 0x90 and sw2 == 0x00): raise ValueError, ("%s %02x %02x" % (command, sw1, sw2)) def cmd_reset_retry_counter(self, who, data): if who == BY_ADMIN: apdu = [0x00, 0x2c, 0x02, 0x81, len(data) ] + data # BY_ADMIN else: apdu = [0x00, 0x2c, 0x00, 0x81, len(data) ] + data # BY_USER with resetcode response, sw1, sw2 = self.connection.transmit(apdu) if not (sw1 == 0x90 and sw2 == 0x00): raise ValueError, ("cmd_reset_retry_counter %02x %02x" % (sw1, sw2)) # Note: CCID specification doesn't permit this (only 0x20 and 0x24) def cmd_reset_retry_counter_pinpad(self, who): if who == BY_ADMIN: apdu = [0x00, 0x2c, 0x02, 0x81] # BY_ADMIN self.send_modify_pinpad(apdu, True, "cmd_reset_retry_counter_pinpad") else: apdu = [0x00, 0x2c, 0x00, 0x81] # BY_USER with resetcode self.send_modify_pinpad(apdu, False, "cmd_reset_retry_counter_pinpad") def cmd_put_resetcode(self, data): apdu = [0x00, 0xda, 0x00, 0xd3, len(data) ] + data # BY_ADMIN response, sw1, sw2 = self.connection.transmit(apdu) if not (sw1 == 0x90 and sw2 == 0x00): raise ValueError, ("cmd_put_resetcode %02x %02x" % (sw1, sw2)) # Note: CCID specification doesn't permit this (only 0x20 and 0x24) def cmd_put_resetcode_pinpad(self): apdu = [0x00, 0xda, 0x00, 0xd3] self.send_modify_pinpad(apdu, True, "cmd_put_resetcode_pinpad") def cmd_change_reference_data_pinpad(self, who, is_exchange): if is_exchange: apdu = [0x00, 0x24, 1, 0x80+who] else: apdu = [0x00, 0x24, 0x00, 0x80+who] self.send_modify_pinpad(apdu, is_exchange, "cmd_change_reference_data_pinpad") COVADIS_VEGA_ALPHA="COVADIS VEGA-ALPHA (000000F5) 00 00" # We need to set ifdDriverOptions in /etc/libccid_Info.plist: # # ifdDriverOptions # 0x0001 # # 1: DRIVER_OPTION_CCID_EXCHANGE_AUTHORIZED # the CCID Exchange command is allowed. You can use it through # SCardControl(hCard, IOCTL_SMARTCARD_VENDOR_IFD_EXCHANGE, ...) def main(who, method, add_a_byte, pinmin, pinmax, change_by_two_steps, fixed): card = Card(add_a_byte, pinmin, pinmax, fixed) card.connection.connect() ident = card.connection.getReader() print "Reader/Token:", ident print "ATR:", toHexString( card.connection.getATR() ) if ident == COVADIS_VEGA_ALPHA: card.cmd_vega_alpha_disable_empty_verify() card.get_features() card.cmd_select_openpgp() if method == "verify": if who == BY_USER: print "Please input User's PIN" else: print "Please input Admin's PIN" card.cmd_verify_pinpad(who) elif method == "change": if change_by_two_steps: if who == BY_USER: print "Please input User's PIN" else: print "Please input Admin's PIN" card.cmd_verify_pinpad(who) if who == BY_USER: print "Please input New User's PIN twice" else: print "Please input New Admin's PIN twice" card.cmd_change_reference_data_pinpad(who, True) else: if who == BY_USER: print "Please input User's PIN" print "and New User's PIN twice" else: print "Please input Admin's PIN" print "and New Admin's PIN twice" card.cmd_change_reference_data_pinpad(who, False) elif method == "unblock": if change_by_two_steps: # It means using keyboard for new PIN if who == BY_USER: resetcode=s2l(getpass("Please input reset code from keyboard: ")) newpin=s2l(getpass("Please input New User's PIN from keyboard: ")) card.cmd_reset_retry_counter(who,resetcode+newpin) else: print "Please input Admin's PIN" card.cmd_verify_pinpad(BY_ADMIN) newpin=s2l(getpass("Please input New User's PIN from keyboard: ")) card.cmd_reset_retry_counter(who,newpin) else: if who == BY_USER: print "Please input reset code" print "and New User's PIN twice" else: print "Please input Admin's PIN" card.cmd_verify_pinpad(BY_ADMIN) print "Please input New User's PIN twice" card.cmd_reset_retry_counter_pinpad(who) elif method == "put": if change_by_two_steps: # It means using keyboard for new PIN print "Please input Admin's PIN" card.cmd_verify_pinpad(BY_ADMIN) resetcode=s2l(getpass("Please input New Reset Code from keyboard: ")) card.cmd_put_resetcode(resetcode) else: print "Please input Admin's PIN" card.cmd_verify_pinpad(BY_ADMIN) print "Please input New Reset Code twice" card.cmd_put_resetcode_pinpad() else: raise ValueError, method card.connection.disconnect() print "OK." return 0 def print_usage(): print "pinpad-test: testing pinentry of PC/SC card reader" print " help:" print "\t--help:\t\tthis message" print " method:\t\t\t\t\t\t\t[verify]" print "\t--verify:\tverify PIN" print "\t--change:\tchange PIN (old PIN, new PIN twice)" print "\t--change2:\tchange PIN by two steps (old PIN, new PIN twice)" print "\t--unblock:\tunblock PIN (admin PIN/resetcode, new PIN twice)" print "\t--unblock2:\tunblock PIN (admin PIN:pinpad, new PIN:kbd)" print "\t--put:\t\tsetup resetcode (admin PIN, new PIN twice)" print "\t--put2::\t\tsetup resetcode (admin PIN:pinpad, new PIN:kbd)" print " options:" print "\t--fixed N:\tUse fixed length input" print "\t--admin:\tby administrator\t\t\t[False]" print "\t--add:\t\tadd a dummy byte at the end of APDU\t[False]" print "\t--pinmin:\tspecify minimum length of PIN\t\t[6]" print "\t--pinmax:\tspecify maximum length of PIN\t\t[15]" print "EXAMPLES:" print " $ pinpad-test # verify user's PIN " print " $ pinpad-test --admin # verify admin's PIN " print " $ pinpad-test --change # change user's PIN " print " $ pinpad-test --change --admin # change admin's PIN " print " $ pinpad-test --change2 # change user's PIN by two steps" print " $ pinpad-test --change2 --admin # change admin's PIN by two steps" print " $ pinpad-test --unblock # change user's PIN by reset code" print " $ pinpad-test --unblock --admin # change user's PIN by admin's PIN" print " $ pinpad-test --put # setup resetcode " if __name__ == '__main__': who = BY_USER method = "verify" add_a_byte = False pinmin = PIN_MIN_DEFAULT pinmax = PIN_MAX_DEFAULT change_by_two_steps = False fixed=0 while len(sys.argv) >= 2: option = sys.argv[1] sys.argv.pop(1) if option == '--admin': who = BY_ADMIN elif option == '--change': method = "change" elif option == '--change2': method = "change" change_by_two_steps = True elif option == '--unblock': method = "unblock" elif option == '--unblock2': method = "unblock" change_by_two_steps = True elif option == '--add': add_a_byte = True elif option == '--fixed': fixed = int(sys.argv[1]) sys.argv.pop(1) elif option == '--pinmin': pinmin = int(sys.argv[1]) sys.argv.pop(1) elif option == '--pinmax': pinmax = int(sys.argv[1]) sys.argv.pop(1) elif option == '--put': method = "put" elif option == '--put2': method = "put" change_by_two_steps = True elif option == "verify": method = "verify" elif option == '--help': print_usage() exit(0) else: raise ValueError, option main(who, method, add_a_byte, pinmin, pinmax, change_by_two_steps, fixed) # Failure # 67 00: Wrong length; no further indication # 69 82: Security status not satisfied: pin doesn't match # 69 85: Conditions of use not satisfied # 6b 00: Wrong parameters P1-P2 # 6b 80 # 64 02: PIN different # General # OpenPGP card v2 doesn't support CHANGE REFERENCE DATA in exchanging # mode (with P1 == 01, replacing PIN). # FAIL: --change2 fails with 6b 00 (after input of PIN) # FAIL: --change2 --admin fails with 6b 00 (after input of PIN) # "FSIJ Gnuk (0.16-34006F06) 00 00" # Works well except --change2 # It could support --put and --unblock, but currently it's disabled. # "Vasco DIGIPASS 920 [CCID] 00 00" # OK: --verify # OK: --verify --admin # OK: --change # OK: --change --admin # OK: --unblock # FAIL: --unblock --admin fails with 69 85 (after input of PIN) # FAIL: --put fails with 6b 80 (before input of resetcode) # OK: --put2 # FAIL: --unblock2 fails with 69 85 # FAIL: --unblock2 --admin fails with 69 85 (after input of PIN) # 0c4b:0500 Reiner SCT Kartensysteme GmbH # "REINER SCT cyberJack RFID standard (7592671050) 00 00" # OK: --verify # OK: --verify --admin # OK: --change # OK: --change --admin # OK: --unblock # OK: --unblock --admin # FAIL: --put fails with 69 85 # Gemalto GemPC Pinpad 00 00 # It asks users PIN with --add but it results 67 00 # It seems that it doesn't support variable length PIN # Firmware version: GemTwRC2-V2.10-GL04 # 072f:90d2 Advanced Card Systems, Ltd # ACS ACR83U 01 00 # --verify failed with 6b 80 # 08e6:34c2 Gemplus # Gemalto Ezio Shield PinPad 01 00 # works well # FAIL: --unblock2 fails with 6d 00 # 076b:3821 OmniKey AG CardMan 3821 # OmniKey CardMan 3821 01 00 # Works well with --pinmax 31 --pinmin 1 # 046a:003e Cherry GmbH SmartTerminal ST-2xxx # Cherry ST-2000 # Needs --add to function properly