diff --git a/README.md b/README.md index 8da9941..14b9afd 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,16 @@ It is much more general in use than to use it as authenthicator to open door. You need just to run `make`. Additional dependencies: -- [libnfc](https://github.com/nfc-tools/libnfc/releases), already present in Raspbian 8 repositories +- [libnfc](https://github.com/nfc-tools/libnfc/releases), in Debian and Ubuntu as libnfc-dev +- [libfreefare](https://github.com/nfc-tools/libfreefare), in Debian and Ubuntu install libfreefare-bin and libfreefare-dev +- [python-axolotl-curve25519](https://github.com/tgalal/python-axolotl-curve25519), in Ubuntu and Debian install python-axolotl-curve25519 - [SWIG](http://www.swig.org/) - [WiringPi2 pythonic binding](https://github.com/WiringPi/WiringPi2-Python) (for switching lock on Raspberry) +All dependencies except for wiring can be installed via: + +`apt install libnfc-dev libfreefare-bin and libfreefare-dev python-axolotl-curve25519 swig3.0` + ## Howto 1. Create the database @@ -44,17 +50,28 @@ You need just to run `make`. Additional dependencies: 3. Add some users - - either authenthication by UID, e.g.: + - either authentication by UID, e.g.: - brmdoor_adduser.py -c brmdoor_nfc.config -a uid 34795FCC SomeUserName + ./brmdoor_adduser.py -c brmdoor_nfc.config -a uid 34795FCC SomeUserName - - authenthication by Yubikey's HMAC-SHA1 programmed on slot 2 + - authentication by Yubikey's HMAC-SHA1 programmed on slot 2 - brmdoor_adduser.py -c brmdoor_nfc.config -a hmac 40795FCCAB0701 SomeUserName 000102030405060708090a0b0c0d0e0f31323334 + ./brmdoor_adduser.py -c brmdoor_nfc.config -a hmac 40795FCCAB0701 SomeUserName 000102030405060708090a0b0c0d0e0f31323334 - - to program Yubikey slot 2 to use HMAC with given key, use: + - to program Yubikey slot 2 to use HMAC with given key (requires package `yubikey-personalization`), use: ykpersonalize -2 -ochal-resp -ohmac-sha1 -ohmac-lt64 -oserial-api-visible + + - authentication using signed UID as NDEF message on Desfire: + + ./brmdoor_adduser.py -c brmdoor.config -a ndef 04631982cc2280 SomeUserName" + + - you need to generate Ed25519 keypair, store the private key somewhere safe and put the public in config file + + ./generate_ed25519_keypair.py + + - you need to program the Desfire card to have the signature + Finally, run the daemon: diff --git a/brmdoor_adduser.py b/brmdoor_adduser.py index 8f310bb..f7832b7 100755 --- a/brmdoor_adduser.py +++ b/brmdoor_adduser.py @@ -27,7 +27,6 @@ def addUidAuth(cursor, uid_hex, nick): print >> sys.stderr, "UID must be in proper hex encoding" sys.exit(1) - def addHmacAuth(cursor, uid_hex, nick, key_hex): """ Add user authenticated by Yubikey HMAC-SHA1. UID should be in hex, 4, 7 @@ -48,12 +47,28 @@ def addHmacAuth(cursor, uid_hex, nick, key_hex): print >> sys.stderr, "UID and key must be in proper hex encoding" sys.exit(1) +def addNdefAuth(cursor, uid_hex, nick): + """ + Add user authenticated by NDEF message on Desfire. UID should be in hex, 4, 7 or 10 bytes long. + """ + try: + uid_hex.decode("hex") + sql = """INSERT INTO authorized_desfires + (uid_hex, nick) + values (?, ?) + """ + sql_data = (uid_hex, nick) + cursor.execute(sql, sql_data) + except TypeError: + print >> sys.stderr, "UID must be in proper hex encoding" + sys.exit(1) + if __name__ == "__main__": parser = OptionParser() parser.add_option("-c", "--config", action="store", type="string", dest="config", help="Configuration file") parser.add_option("-a", "--authtype", action="store", type="string", dest="authtype", - help="Authenthication type - uid or hmac") + help="Authenthication type - uid, hmac or ndef") (opts, args) = parser.parse_args() if opts.config is None: @@ -61,9 +76,9 @@ if __name__ == "__main__": parser.print_help() sys.exit(1) - if opts.authtype not in ["uid", "hmac"]: + if opts.authtype not in ["uid", "hmac", "ndef"]: print >> sys.stderr, "You must specify authentication type via -a option!" - print >> sys.stderr, "Acceptable choices: uid, hmac" + print >> sys.stderr, "Acceptable choices: uid, hmac, ndef" sys.exit(1) config = BrmdoorConfig(opts.config) @@ -83,6 +98,13 @@ if __name__ == "__main__": print >> sys.stderr, "brmdoor_adduser.py -c brmdoor.config -a hmac 40795FCCAB0701 SomeUserName 000102030405060708090a0b0c0d0e0f31323334" sys.exit(1) addHmacAuth(cursor, args[0], args[1], args[2]) - + elif opts.authtype == "ndef": + if len(args) < 2: + print >> sys.stderr, "You must two additional arguments, hex UID and nick" + print >> sys.stderr, "Example:" + print >> sys.stderr, "brmdoor_adduser.py -c brmdoor.config -a ndef 34795FCC SomeUserName" + sys.exit(1) + addNdefAuth(cursor, args[0], args[1]) + conn.commit() conn.close() diff --git a/brmdoor_authenticator.py b/brmdoor_authenticator.py index 65eeb5d..4478bc3 100644 --- a/brmdoor_authenticator.py +++ b/brmdoor_authenticator.py @@ -4,6 +4,8 @@ import hmac import hashlib import logging +import axolotl_curve25519 as curve + from nfc_smartcard import NFCError @@ -128,7 +130,7 @@ class YubikeyHMACAuthenthicator(object): if not rapdu.valid or rapdu.sw() != 0x9000: raise NFCError("HMAC - response SW is not 0x9000") except NFCError, e: - logging.debug("Yubikey HMAC command failed: %s" % e.what()) + logging.info("Yubikey HMAC command failed: %s" % e.what()) return None if not self.hmacCheck(secretKey, challenge, rapdu.data()): @@ -140,7 +142,62 @@ class YubikeyHMACAuthenthicator(object): def shutdown(self): """Closes connection to database""" self.conn.close() - + +class DesfireEd25519Authenthicator(object): + """ + Reads NDEF message from Desfire and it must be signed binary value of UID + """ + def __init__(self, filename, nfcReader, pubKey): + """ + Connects to database by given filename and later checks UIDs + using the pubkey (given as binary string). + """ + #again autocommit mode + self.conn = sqlite3.connect(filename, isolation_level=None) + self.nfcReader = nfcReader + self.pubKey = pubKey + + def signatureCheck(self, uid, signature): + """ + Returns true iff uid (as binary) is the message signed by signature (binary string) + """ + verified = curve.verifySignature(self.pubKey, uid, signature) == 0 + return verified + + + def checkUIDSignature(self, uid_hex): + """ + Checks if UID is in database. If so, it retrieves NDEF which should be signature of the UID + @param uid_hex: uid to match in hex + @returns UidRecord instance if found, None otherwise + """ + cursor = self.conn.cursor() + sql = "SELECT nick FROM authorized_desfires WHERE UPPER(uid_hex)=?" + sql_data =(uid_hex.upper(),) + + cursor.execute(sql, sql_data) + record = cursor.fetchone() + + if record is None: + return None + + nick = record[0] + + try: + ndefSignature = self.nfcReader.readDesfireNDEF() + if self.signatureCheck(uid_hex.decode("hex"), ndefSignature): + return UidRecord(uid_hex, nick) + else: + logging.info("Signature check failed for Desfire NDEF for UID %s", uid_hex) + return None + except NFCError, e: + logging.info("Desfire read NDEF failed: %s" % e.what()) + return None + + def shutdown(self): + """Closes connection to database""" + self.conn.close() + #test routine if __name__ == "__main__": authenticator = UidAuthenticator("test_uids_db.sqlite") diff --git a/brmdoor_nfc.config.sample b/brmdoor_nfc.config.sample index 9f9221f..a8db66e 100644 --- a/brmdoor_nfc.config.sample +++ b/brmdoor_nfc.config.sample @@ -8,6 +8,8 @@ # Unlocker is just dummy test class. [brmdoor] auth_db_filename = test_uids_db.sqlite +#corresponding private key = 10ee85f987e7682d9acf24ab07ff0e302ee4cdd426f83a055d3a337a4f01314b +desfire_ed25519_pubkey = 4c625187d79fdee97a6af48cb8f854e7f313c8158de94e667e1509bd26617d27 #lock_opened_secs = 5 #unknown_uid_timeout_secs = 5 log_file = - diff --git a/brmdoor_nfc_daemon.py b/brmdoor_nfc_daemon.py index 34ec8ec..6422fe3 100755 --- a/brmdoor_nfc_daemon.py +++ b/brmdoor_nfc_daemon.py @@ -9,7 +9,7 @@ from binascii import hexlify from nfc_smartcard import NFCDevice, NFCError -from brmdoor_authenticator import UidAuthenticator, YubikeyHMACAuthenthicator +from brmdoor_authenticator import UidAuthenticator, YubikeyHMACAuthenthicator, DesfireEd25519Authenthicator import unlocker class BrmdoorConfigError(ConfigParser.Error): @@ -40,6 +40,7 @@ class BrmdoorConfig(object): self.config.read(filename) self.authDbFilename = self.config.get("brmdoor", "auth_db_filename") + self.desfirePubkey = self.config.get("brmdoor", "desfire_ed25519_pubkey") self.lockOpenedSecs = self.config.getint("brmdoor", "lock_opened_secs") self.unknownUidTimeoutSecs = self.config.getint("brmdoor", "unknown_uid_timeout_secs") self.logFile = self.config.get("brmdoor", "log_file") @@ -65,6 +66,7 @@ class NFCScanner(object): """ self.authenticator = UidAuthenticator(config.authDbFilename) self.hmacAuthenticator = None + self.desfireAuthenticator = None self.unknownUidTimeoutSecs = config.unknownUidTimeoutSecs self.lockOpenedSecs = config.lockOpenedSecs @@ -82,6 +84,10 @@ class NFCScanner(object): self.hmacAuthenticator = YubikeyHMACAuthenthicator( config.authDbFilename, self.nfc ) + self.desfireAuthenticator = DesfireEd25519Authenthicator( + config.authDbFilename, self.nfc, + config.desfirePubkey + ) #self.nfc.pollNr = 0xFF #poll indefinitely while True: try: @@ -124,7 +130,15 @@ class NFCScanner(object): logging.info("Unlocking after HMAC for UID %s", record) self.unlocker.unlock() return - + + #test for Desfire NDEF auth + record = self.desfireAuthenticator.checkUIDSignature(uid_hex) + + if record is not None: + logging.info("Unlocking after Desfire NDEF ed25519 check for UID %s", record) + self.unlocker.unlock() + return + logging.info("Unknown UID %s", uid_hex) time.sleep(self.unknownUidTimeoutSecs) diff --git a/create_authenticator_db.py b/create_authenticator_db.py index f25cf4c..272e6ba 100755 --- a/create_authenticator_db.py +++ b/create_authenticator_db.py @@ -28,5 +28,10 @@ if __name__ == "__main__": key_hex TEXT, nick TEXT) """) + cursor.execute("""CREATE TABLE authorized_desfires( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid_hex TEXT, + nick TEXT) + """) conn.commit() conn.close() diff --git a/generate_ed25519_keypair.py b/generate_ed25519_keypair.py new file mode 100755 index 0000000..e6daf78 --- /dev/null +++ b/generate_ed25519_keypair.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python2 + +""" +Used to generate keypair for signing NDEF messages for Mifare NDEF authentication +""" + +import os +import axolotl_curve25519 as curve + +from binascii import hexlify + +random32 = os.urandom(32) + +private_key = curve.generatePrivateKey(random32) +public_key = curve.generatePublicKey(private_key) + +print "private key in hex:", hexlify(private_key) +print "public key in hex :", hexlify(public_key) + diff --git a/sign_uid.py b/sign_uid.py new file mode 100644 index 0000000..8aa7a8f --- /dev/null +++ b/sign_uid.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python2 + +import sys +import os +import axolotl_curve25519 as curve + +def signUid(private_key, uid): + """ + Create an Ed25519 signature for UID + :param private_key: Binary representation of Ed25519 key + :param uid: UID, decoded to binary from hex + :return: singature in binary format + """ + random64 = os.urandom(64) + signature = curve.calculateSignature(random64, private_key, uid) + return signature + +if __name__ == "__main__": + + if len(sys.argv) < 3: + print >> sys.stderr, "Usage: sign_uid.py uid_hex ed25519_private_key_hex" + print >> sys.stderr, "Outputs binary signature, you will probably want to redirect it to a file" + sys.exit(1) + + private_key = sys.argv[3].decode("hex") + uid_bin = sys.argv[1].decode("hex") + sys.stdout.write(signUid(private_key, uid_bin)) diff --git a/test_nfc.py b/test_nfc.py index 58ec5c4..65d51fa 100755 --- a/test_nfc.py +++ b/test_nfc.py @@ -51,30 +51,48 @@ print "Available tests: %s" % ", ".join(sorted(tests.keys())) print "Selected test: %s" % apdu_test # select apdus according to test name -hex_apdus = tests[apdu_test] -apdus = [hex_apdu.replace(" ","").decode("hex") for hex_apdu in hex_apdus] +if apdu_test in tests: + hex_apdus = tests[apdu_test] + apdus = [hex_apdu.replace(" ","").decode("hex") for hex_apdu in hex_apdus] -try: - nfc = NFCDevice() - uid = nfc.scanUID() - print "UID", hexlify(uid) - #nfc.close() - #nfc.open() - - print "Now trying to send ISO14443-4 APDUs" try: - #nfc.selectPassiveTarget() - for apdu in apdus: - print "Command APDU:", formatAPDU(apdu) - rapdu = nfc.sendAPDU(apdu) - print "Response APDU valid: %s, SW %04x, data %s" % (rapdu.valid(), rapdu.sw(), hexlify(rapdu.data())) - except NFCError, e: - print "Failed to transmit APDU:", e.what() + nfc = NFCDevice() + uid = nfc.scanUID() + print "UID", hexlify(uid) + #nfc.close() + #nfc.open() - print "Device is opened:", nfc.opened() - print "Closing device" - nfc.close() - print "Device is opened:", nfc.opened() - nfc.unload() -except NFCError, e: - print "Reading UID failed:", e.what() + print "Now trying to send ISO14443-4 APDUs" + try: + #nfc.selectPassiveTarget() + for apdu in apdus: + print "Command APDU:", formatAPDU(apdu) + rapdu = nfc.sendAPDU(apdu) + print "Response APDU valid: %s, SW %04x, data %s" % (rapdu.valid(), rapdu.sw(), hexlify(rapdu.data())) + except NFCError, e: + print "Failed to transmit APDU:", e.what() + + print "Device is opened:", nfc.opened() + print "Closing device" + nfc.close() + print "Device is opened:", nfc.opened() + nfc.unload() + except NFCError, e: + print "Reading UID failed:", e.what() +elif apdu_test == "desfire-ndef4": + try: + nfc = NFCDevice() + uid = nfc.scanUID() + print "UID", hexlify(uid) + #nfc.close() + #nfc.open() + + ndef = nfc.readDesfireNDEF() + print ndef + print "Device is opened:", nfc.opened() + print "Closing device" + nfc.close() + print "Device is opened:", nfc.opened() + nfc.unload() + except NFCError, e: + print "Reading UID failed:", e.what() diff --git a/test_uids_db.sqlite b/test_uids_db.sqlite index b79d94d..8c31693 100644 Binary files a/test_uids_db.sqlite and b/test_uids_db.sqlite differ diff --git a/unlocker.py b/unlocker.py index f51a6c6..dd3e2a0 100644 --- a/unlocker.py +++ b/unlocker.py @@ -1,5 +1,4 @@ import time -import wiringpi2 as wiringpi class Unlocker(object): """Abstract class/interface for Unlocker object. @@ -35,14 +34,16 @@ class UnlockerWiringPi(Unlocker): """ def __init__(self, config): + import wiringpi2 as wiringpi Unlocker.__init__(self, config) - wiringpi.wiringPiSetupGpio() # pin numbers follow P1 GPIO header + wiringpi.wiringPiSetupGpio() # pin numbers follow P1 GPIO header self.lockPin = self.config.getint("UnlockerWiringPi", "lock_pin") wiringpi.pinMode(self.lockPin, 1) #output def unlock(self): """Unlocks lock at configured pin by pulling it high. """ + import wiringpi2 as wiringpi wiringpi.digitalWrite(self.lockPin, 1) time.sleep(self.lockOpenedSecs) wiringpi.digitalWrite(self.lockPin, 0) @@ -52,5 +53,6 @@ class UnlockerWiringPi(Unlocker): Lock the lock back. Meant to be used when program is shut down so that lock is not left disengaged. """ + import wiringpi2 as wiringpi wiringpi.digitalWrite(self.lockPin, 0) diff --git a/write_signed_ndef_on_desfire.py b/write_signed_ndef_on_desfire.py new file mode 100755 index 0000000..d76be18 --- /dev/null +++ b/write_signed_ndef_on_desfire.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +import sys +import os +import tempfile + +from binascii import hexlify + +from nfc_smartcard import NFCDevice, NFCError +from sign_uid import signUid + +if len(sys.argv) < 2: + print "Usage: write_signed_ndef_on_desfire.py private_key_in_hex" + sys.exit(3) + +tempFile = None +tempFname = None + +try: + print "Opening NFC reader" + nfc = NFCDevice() + nfc.pollNr = 0xFF #poll indefinitely + print "Waiting for Desfire card to appear in the reader" + uid_hex = hexlify(nfc.scanUID()) + key = sys.argv[1].decode("hex") + + print("Got UID %s", uid_hex) + signature = signUid(key, uid_hex.decode("hex")) + (tempFile, tempFname) = tempfile.mkstemp(dir="/tmp") + with tempFile: + tempFile.write(signature) +except NFCError, e: + #this exception happens also when scanUID times out + print("Failed to wait for Desfire card: %s" % e) + sys.exit(1) +except Exception, e: + print("Something went wrong when writing the signature to file:", e) + sys.exit(2) +finally: + nfc.close() + nfc.unload() + if tempFname: + os.unlink(tempFname) + +# We'll just call the command line tools so that we don't need to copy&paste the NDEF writing code to nfc_smartcard.cpp +print "Formatting card" +res = os.system("mifare-desfire-format -y") +if res != 0: + print "Formatting failed" + sys.exit(4) +print "Creating NDEF file/application" +res = os.system("mifare-desfire-create-ndef -y") +if res != 0: + print "Creating NDEF failed" + sys.exit(4) +print "Writing NDEF with signature onto Desfire" +res = os.system("mifare-desfire-create-ndef -y -i '%'" % tempFname) +if res != 0: + print "Writing NDEF failed" + sys.exit(4) + +print "All done"