From 14c76e5b936e3c91d88d21e30558d4eee1ab98ef Mon Sep 17 00:00:00 2001 From: b1galez Date: Tue, 14 Dec 2010 10:40:58 +0000 Subject: 3.0 Release, added MySQL support, various fixes. git-svn-id: http://yubico-yubiserve.googlecode.com/svn/trunk@29 fbcee277-3294-991b-8290-beb7048acdd6 --- README | 17 +++++-- dbconf.py | 65 ++++++++++++++++++++++---- src/dump.mysql | 61 +++++++++++++++++++++++++ src/dump.sql | 25 ---------- src/dump.sqlite | 25 ++++++++++ yubikeys.sqlite | Bin 11264 -> 11264 bytes yubiserve.cfg | 9 ++++ yubiserve.py | 138 ++++++++++++++++++++++++++++++++------------------------ 8 files changed, 243 insertions(+), 97 deletions(-) create mode 100644 src/dump.mysql delete mode 100644 src/dump.sql create mode 100755 src/dump.sqlite create mode 100644 yubiserve.cfg diff --git a/README b/README index efee60f..033211b 100644 --- a/README +++ b/README @@ -3,10 +3,11 @@ YubiServe has been written by Alessio Periloso Version 1.0: 21/05/2010 Version 2.0: 19/11/2010 Version 2.9: 13/12/2010 +Version 3.0: 14/12/2010 == Description == This simple service allows to authenticate Yubikeys and OATH Tokens using -only a small sqlite database. +only a small sqlite database (the mysql support is optional!) The code has been released under GNU license (license into LICENSE file) The project is divided into two parts: @@ -17,12 +18,20 @@ The project is divided into two parts: == Installation == Installation is pretty simple, you just have to install few python packages: Under Debian, you can run: -apt-get install python python-sqlite python-crypto python-openssl +apt-get install python python-crypto python-openssl +If you want to add the sqlite support, you should run: +apt-get install python-sqlite +Or, if you want to add the mysql support, you should run: +apt-get install python-mysqldb +If you chosen the mysql support, you must create a database and create the +tables. The mysql dump is at src/dump.mysql. Then, you have to generate the certificate for ssl validation, so if you don't already have a certificate you have to issue the following command to self-sign one: openssl req -new -x509 -keyout yubiserve.pem -out yubiserve.pem -days 365 -nodes -That's all, yes! + +A good idea would be taking a look at yubiserve.cfg, to configure the validation server settings. + After installing the needed packages, you just need to extract the files to a directory, add the keys and launch the server (or, if you prefer you can launch the server before adding the keys, it doesn't matter). @@ -156,4 +165,4 @@ h=vYoG9Av8uG6OqVkmMFuANi4fyWw= That's all. Pretty simple, huh? Of course you can add new keys while the server is already running, without needing it to restart, and of course multiple queries a time are allowed, that's why the server -is multithreaded. \ No newline at end of file +is multithreaded. diff --git a/dbconf.py b/dbconf.py index 894efde..4d4ff0f 100755 --- a/dbconf.py +++ b/dbconf.py @@ -1,6 +1,28 @@ #!/usr/bin/python -import sqlite, time, random, re +import time, random, re, os from sys import argv +try: + import MySQLdb +except ImportError: + pass +try: + import sqlite +except ImportError: + pass + +def parseConfigFile(): # Originally I wrote this function to parse PHP configuration files! + config = open(os.path.dirname(os.path.realpath(__file__)) + '/yubiserve.cfg', 'r').read().splitlines() + keys = {} + for line in config: + match = re.search('(.*?)=(.*);', line) + try: # Check if it's a string or a number + if ((match.group(2).strip()[0] != '"') and (match.group(2).strip()[0] != '\'')): + keys[match.group(1).strip()] = int(match.group(2).strip()) + else: + keys[match.group(1).strip()] = match.group(2).strip('"\' ') + except: + pass + return keys def randomChars(max): retVal = '' @@ -14,7 +36,30 @@ def randomChars(max): retVal += chr(rand+47) return retVal -con = sqlite.connect('yubikeys.sqlite') +config = parseConfigFile() +try: + if MySQLdb != None: + isThereMysql = True +except NameError: + isThereMysql = False +try: + if sqlite != None: + isThereSqlite = True +except NameError: + isThereSqlite = False +if isThereMysql == isThereSqlite == False: + print "Cannot continue without any database support.\nPlease read README.\n\n" + quit() +if config['yubiDB'] == 'mysql' and (config['yubiMySQLHost'] == '' or config['yubiMySQLUser'] == '' or config['yubiMySQLPass'] == '' or config['yubiMySQLName'] == ''): + print "Cannot continue without any MySQL configuration.\nPlease read README.\n\n" + quit() +try: + if config['yubiDB'] == 'sqlite': + con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') + elif config['yubiDB'] == 'mysql': + con = MySQLdb.connect(host=config['yubiMySQLHost'], user=config['yubiMySQLUser'], passwd=config['yubiMySQLPass'], db=config['yubiMySQLName']) +except: + print "There's a problem with the database!\n" cur = con.cursor() if (len(argv)<2): @@ -43,9 +88,9 @@ else: if (cur.rowcount == 0): print 'Key not found.' else: - cur.execute("SELECT * FROM yubikeys WHERE nickname = '" + nickname + "' AND active = 'true'") + cur.execute("SELECT * FROM yubikeys WHERE nickname = '" + nickname + "' AND active = '1'") if (cur.rowcount == 1): - cur.execute("UPDATE yubikeys SET active = 'false' WHERE nickname = '" + nickname + "'") + cur.execute("UPDATE yubikeys SET active = '1' WHERE nickname = '" + nickname + "'") print "Key '" + nickname + "' disabled." con.commit() else: @@ -57,9 +102,9 @@ else: if (cur.rowcount == 0): print 'Key not found.' else: - cur.execute("SELECT * FROM yubikeys WHERE nickname = '" + nickname + "' AND active = 'false'") + cur.execute("SELECT * FROM yubikeys WHERE nickname = '" + nickname + "' AND active = '1'") if (cur.rowcount == 1): - cur.execute("UPDATE yubikeys SET active = 'true' WHERE nickname = '" + nickname + "'") + cur.execute("UPDATE yubikeys SET active = '1' WHERE nickname = '" + nickname + "'") print "Key '" + nickname + "' enabled." con.commit() else: @@ -107,9 +152,9 @@ else: if (cur.rowcount == 0): print 'Key not found.' else: - cur.execute("SELECT * FROM oathtokens WHERE nickname = '" + nickname + "' AND active = 'true'") + cur.execute("SELECT * FROM oathtokens WHERE nickname = '" + nickname + "' AND active = '1'") if (cur.rowcount == 1): - cur.execute("UPDATE oathtokens SET active = 'false' WHERE nickname = '" + nickname + "'") + cur.execute("UPDATE oathtokens SET active = '1' WHERE nickname = '" + nickname + "'") print "Key '" + nickname + "' disabled." con.commit() else: @@ -121,9 +166,9 @@ else: if (cur.rowcount == 0): print 'Key not found.' else: - cur.execute("SELECT * FROM oathtokens WHERE nickname = '" + nickname + "' AND active = 'false'") + cur.execute("SELECT * FROM oathtokens WHERE nickname = '" + nickname + "' AND active = '1'") if (cur.rowcount == 1): - cur.execute("UPDATE oathtokens SET active = 'true' WHERE nickname = '" + nickname + "'") + cur.execute("UPDATE oathtokens SET active = '1' WHERE nickname = '" + nickname + "'") print "Key '" + nickname + "' enabled." con.commit() else: diff --git a/src/dump.mysql b/src/dump.mysql new file mode 100644 index 0000000..f095a25 --- /dev/null +++ b/src/dump.mysql @@ -0,0 +1,61 @@ +SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + +-- +-- Database: `yubikeys` +-- + +-- -------------------------------------------------------- + +-- +-- Table `apikeys` +-- + +CREATE TABLE IF NOT EXISTS `apikeys` ( + `nickname` varchar(16) default NULL, + `secret` varchar(28) default NULL, + `id` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- -------------------------------------------------------- + +-- +-- Table `oathtokens` +-- + +CREATE TABLE IF NOT EXISTS `oathtokens` ( + `nickname` varchar(16) NOT NULL, + `publicname` varchar(12) NOT NULL, + `created` varchar(24) NOT NULL, + `secret` varchar(40) NOT NULL, + `active` tinyint(1) default '1', + `counter` int(11) NOT NULL default '1', + UNIQUE KEY `nickname` (`nickname`), + UNIQUE KEY `publicname` (`publicname`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- -------------------------------------------------------- + +-- +-- Table `yubikeys` +-- + +CREATE TABLE IF NOT EXISTS `yubikeys` ( + `nickname` varchar(16) NOT NULL, + `publicname` varchar(16) NOT NULL, + `created` varchar(24) NOT NULL, + `internalname` varchar(12) NOT NULL, + `aeskey` varchar(32) NOT NULL, + `active` tinyint(1) default '1', + `counter` int(11) NOT NULL default '1', + `time` int(11) NOT NULL default '1', + UNIQUE KEY `nickname` (`nickname`), + UNIQUE KEY `publicname` (`publicname`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + diff --git a/src/dump.sql b/src/dump.sql deleted file mode 100644 index 7cb972e..0000000 --- a/src/dump.sql +++ /dev/null @@ -1,25 +0,0 @@ -BEGIN TRANSACTION; -create table yubikeys( - nickname varchar(16) unique not null, - publicname varchar(16) unique not null, - created varchar(24) not null, - internalname varchar(12) not null, - aeskey varchar(32) not null, - active boolean default true, - counter integer not null default 1, - time integer not null default 1 -); -create table oathtokens( - nickname varchar(16) unique not null, - publicname varchar(12) unique not null, - created varchar(24) not null, - secret varchar(40) not null, - active boolean default true, - counter integer not null default 1 -); -create table apikeys( - nickname varchar(16), - secret varchar(28), - id integer primary key -); -COMMIT; diff --git a/src/dump.sqlite b/src/dump.sqlite new file mode 100755 index 0000000..7cb972e --- /dev/null +++ b/src/dump.sqlite @@ -0,0 +1,25 @@ +BEGIN TRANSACTION; +create table yubikeys( + nickname varchar(16) unique not null, + publicname varchar(16) unique not null, + created varchar(24) not null, + internalname varchar(12) not null, + aeskey varchar(32) not null, + active boolean default true, + counter integer not null default 1, + time integer not null default 1 +); +create table oathtokens( + nickname varchar(16) unique not null, + publicname varchar(12) unique not null, + created varchar(24) not null, + secret varchar(40) not null, + active boolean default true, + counter integer not null default 1 +); +create table apikeys( + nickname varchar(16), + secret varchar(28), + id integer primary key +); +COMMIT; diff --git a/yubikeys.sqlite b/yubikeys.sqlite index 0d4f75c..45334b5 100644 Binary files a/yubikeys.sqlite and b/yubikeys.sqlite differ diff --git a/yubiserve.cfg b/yubiserve.cfg new file mode 100644 index 0000000..06993bf --- /dev/null +++ b/yubiserve.cfg @@ -0,0 +1,9 @@ +yubiservePORT = 8000; +yubiserveSSLPORT = 8001; +yubiserveHOST = '0.0.0.0'; +yubiDB = 'sqlite'; +#yubiDB = 'mysql'; +yubiMySQLHost = 'localhost'; +yubiMySQLUser = 'yubiserve'; +yubiMySQLPass = 'yubipass'; +yubiMySQLName = 'yubikeys'; diff --git a/yubiserve.py b/yubiserve.py index 80bdecd..e77ff63 100755 --- a/yubiserve.py +++ b/yubiserve.py @@ -1,20 +1,40 @@ #!/usr/bin/python -import sqlite, re, os, time, socket +import re, os, time, socket import urlparse, SocketServer, urllib, BaseHTTPServer from Crypto.Cipher import AES from OpenSSL import SSL import hmac, hashlib from threading import Thread +try: + import MySQLdb +except ImportError: + pass +try: + import sqlite +except ImportError: + pass -yubiservePORT = 8000 -yubiserveSSLPORT = yubiservePORT + 1 -yubiserveHOST = '0.0.0.0' # You can use '127.0.0.1' to avoid - # the server to receive queries from - # the outside +def parseConfigFile(): # Originally I wrote this function to parse PHP configuration files! + config = open(os.path.dirname(os.path.realpath(__file__)) + '/yubiserve.cfg', 'r').read().splitlines() + keys = {} + for line in config: + match = re.search('(.*?)=(.*);', line) + try: # Check if it's a string or a number + if ((match.group(2).strip()[0] != '"') and (match.group(2).strip()[0] != '\'')): + keys[match.group(1).strip()] = int(match.group(2).strip()) + else: + keys[match.group(1).strip()] = match.group(2).strip('"\' ') + except: + pass + return keys + +config = parseConfigFile() class OATHValidation(): - status = {'OK': 1, 'BAD_OTP': 2, 'NO_AUTH': 3, 'NO_CLIENT': 5} - validationResult = 0 + def __init__(self, connection): + self.status = {'OK': 1, 'BAD_OTP': 2, 'NO_AUTH': 3, 'NO_CLIENT': 5} + self.validationResult = 0 + self.con = connection def testHOTP(self, K, C, digits=6): counter = ("%x"%C).rjust(16,'0').decode('hex') # Convert it into 8 bytes hex HS = hmac.new(K, counter, hashlib.sha1).digest() @@ -23,9 +43,8 @@ class OATHValidation(): bin_code = int((chr(ord(HS[offset]) & 0x7F) + HS[offset+1:offset+4]).encode('hex'),16) return str(bin_code)[-digits:] def validateOATH(self, OATH, publicID): - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() - cur.execute("SELECT counter, secret FROM oathtokens WHERE publicname = '" + publicID + "' AND active = 'true'") + cur = self.con.cursor() + cur.execute("SELECT counter, secret FROM oathtokens WHERE publicname = '" + publicID + "' AND active = '1'") if (cur.rowcount != 1): validationResult = self.status['BAD_OTP'] return validationResult @@ -37,15 +56,16 @@ class OATHValidation(): K = key.decode('hex') # key for C in range(actualcounter+1, actualcounter+256): if OATH == self.testHOTP(K, C, len(OATH)): - cur.execute("UPDATE oathtokens SET counter = " + str(C) + " WHERE publicname = '" + publicID + "' AND active = 'true'") - con.commit() + cur.execute("UPDATE oathtokens SET counter = " + str(C) + " WHERE publicname = '" + publicID + "' AND active = '1'") + self.con.commit() return self.status['OK'] return self.status['NO_AUTH'] class OTPValidation(): - status = {'OK': 1, 'BAD_OTP': 2, 'REPLAYED_OTP': 3, 'DELAYED_OTP': 4, 'NO_CLIENT': 5} - validationResult = 0 - + def __init__(self, connection): + self.status = {'OK': 1, 'BAD_OTP': 2, 'REPLAYED_OTP': 3, 'DELAYED_OTP': 4, 'NO_CLIENT': 5} + self.validationResult = 0 + self.con = connection def hexdec(self, hex): return int(hex, 16) def modhex2hex(self, string): @@ -89,56 +109,55 @@ class OTPValidation(): if match.group(1) and match.group(2): self.userid = match.group(1) self.token = self.modhex2hex(match.group(2)) - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() - cur.execute('SELECT aeskey, internalname FROM yubikeys WHERE publicname = "' + self.userid + '" AND active = "true"') + cur = self.con.cursor() + cur.execute('SELECT aeskey, internalname FROM yubikeys WHERE publicname = "' + self.userid + '" AND active = "1"') if (cur.rowcount != 1): self.validationResult = self.status['BAD_OTP'] - con.close() return self.validationResult (self.aeskey, self.internalname) = cur.fetchone() self.plaintext = self.aes128ecb_decrypt(self.aeskey, self.token) uid = self.plaintext[:12] if (self.internalname != uid): self.validationResult = self.status['BAD_OTP'] - con.close() return self.validationResult if not (self.CRC() or self.isCRCValid()): self.validationResult = self.status['BAD_OTP'] - con.close() return self.validationResult self.internalcounter = self.hexdec(self.plaintext[14:16] + self.plaintext[12:14] + self.plaintext[22:24]) self.timestamp = self.hexdec(self.plaintext[20:22] + self.plaintext[18:20] + self.plaintext[16:18]) - cur.execute('SELECT counter, time FROM yubikeys WHERE publicname = "' + self.userid + '" AND active = "true"') + cur.execute('SELECT counter, time FROM yubikeys WHERE publicname = "' + self.userid + '" AND active = "1"') if (cur.rowcount != 1): self.validationResult = self.status['BAD_OTP'] - con.close() return self.validationResult (self.counter, self.time) = cur.fetchone() if (self.counter) >= (self.internalcounter): self.validationResult = self.status['REPLAYED_OTP'] - con.close() return self.validationResult if (self.time >= self.timestamp) and ((self.counter >> 8) == (self.internalcounter >> 8)): self.validationResult = self.status['DELAYED_OTP'] - con.close() return self.validationResult except IndexError: self.validationResult = self.status['BAD_OTP'] - con.close() return self.validationResult self.validationResult = self.status['OK'] cur.execute('UPDATE yubikeys SET counter = ' + str(self.internalcounter) + ', time = ' + str(self.timestamp) + ' WHERE publicname = "' + self.userid + '"') - con.commit() - con.close() + self.con.commit() return self.validationResult class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): __base = BaseHTTPServer.BaseHTTPRequestHandler __base_handle = __base.handle - server_version = 'Yubiserve/2.9' - print 'HTTP Server is running.' - + server_version = 'Yubiserve/3.0' + global config + #try: + if config['yubiDB'] == 'sqlite': + con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') + elif config['yubiDB'] == 'mysql': + con = MySQLdb.connect(host=config['yubiMySQLHost'], user=config['yubiMySQLUser'], passwd=config['yubiMySQLPass'], db=config['yubiMySQLName']) + #except: + # print "There's a problem with the database!\n" + # quit() + def getToDict(self, qs): dict = {} for singleValue in qs.split('&'): @@ -170,7 +189,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: if len(query) > 0: getData = self.getToDict(query) - otpvalidation = OTPValidation() + otpvalidation = OTPValidation(self.con) validation = otpvalidation.validateOTP(getData['otp']) self.send_response(200) self.send_header('Content-type', 'text/plain') @@ -186,8 +205,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: if (getData['id'] != None): apiID = re.escape(getData['id']) - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() + cur = self.con.cursor() cur.execute("SELECT secret from apikeys WHERE id = '" + apiID + "'") if cur.rowcount != 0: api_key = cur.fetchone()[0] @@ -210,8 +228,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: if (getData['id'] != None): apiID = re.escape(getData['id']) - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() + cur = self.con.cursor() cur.execute("SELECT secret from apikeys WHERE id = '" + apiID + "'") if cur.rowcount != 0: api_key = cur.fetchone()[0] @@ -224,7 +241,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: getData = self.getToDict(query) if (len(query) > 0) and ((len(getData['otp']) == 6) or (len(getData['otp']) == 8) or (len(getData['otp']) == 18) or (len(getData['otp']) == 20)): - oathvalidation = OATHValidation() + oathvalidation = OATHValidation(self.con) OTP = getData['otp'] if (len(OTP) == 18) or (len(OTP) == 20): publicID = OTP[0:12] @@ -245,8 +262,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: if (getData['id'] != None): apiID = re.escape(getData['id']) - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() + cur = self.con.cursor() cur.execute("SELECT secret from apikeys WHERE id = '" + apiID + "'") if cur.rowcount != 0: api_key = cur.fetchone()[0] @@ -267,8 +283,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: if (getData['id'] != None): apiID = re.escape(getData['id']) - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() + cur = self.con.cursor() cur.execute("SELECT secret from apikeys WHERE id = '" + apiID + "'") if cur.rowcount != 0: api_key = cur.fetchone()[0] @@ -288,8 +303,7 @@ class YubiServeHandler (BaseHTTPServer.BaseHTTPRequestHandler): try: if (getData['id'] != None): apiID = re.escape(getData['id']) - con = sqlite.connect(os.path.dirname(os.path.realpath(__file__)) + '/yubikeys.sqlite') - cur = con.cursor() + cur = self.con.cursor() cur.execute("SELECT secret from apikeys WHERE id = '" + apiID + "'") if cur.rowcount != 0: api_key = cur.fetchone()[0] @@ -318,8 +332,25 @@ class SecureHTTPServer(BaseHTTPServer.HTTPServer): class ThreadingHTTPServer (SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): pass class ThreadingHTTPSServer (SocketServer.ThreadingMixIn, SecureHTTPServer): pass -yubiserveHTTP = ThreadingHTTPServer((yubiserveHOST, yubiservePORT), YubiServeHandler) -yubiserveSSL = ThreadingHTTPSServer((yubiserveHOST, yubiserveSSLPORT), YubiServeHandler) +try: + if MySQLdb != None: + isThereMysql = True +except NameError: + isThereMysql = False +try: + if sqlite != None: + isThereSqlite = True +except NameError: + isThereSqlite = False +if isThereMysql == isThereSqlite == False: + print "Cannot continue without any database support.\nPlease read README.\n\n" + quit() +if config['yubiDB'] == 'mysql' and (config['yubiMySQLHost'] == '' or config['yubiMySQLUser'] == '' or config['yubiMySQLPass'] == '' or config['yubiMySQLName'] == ''): + print "Cannot continue without any MySQL configuration.\nPlease read README.\n\n" + quit() + +yubiserveHTTP = ThreadingHTTPServer((config['yubiserveHOST'], config['yubiservePORT']), YubiServeHandler) +yubiserveSSL = ThreadingHTTPSServer((config['yubiserveHOST'], config['yubiserveSSLPORT']), YubiServeHandler) http_thread = Thread(target=yubiserveHTTP.serve_forever) ssl_thread = Thread(target=yubiserveSSL.serve_forever) @@ -330,16 +361,7 @@ ssl_thread.setDaemon(True) http_thread.start() ssl_thread.start() -while 1: - time.sleep(1) - -""" +print "HTTP Server is running." -try: - yubiserve.serve_forever() - yubiserveSSL.serve_forever() -except KeyboardInterrupt: - print "" - yubiserve.server_close() - yubiserveSSL.server_close() -""" \ No newline at end of file +while 1: + time.sleep(1) \ No newline at end of file -- cgit v1.2.3