summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Chudnick <sam@chudnick.com>2022-06-30 04:25:26 -0400
committerSam Chudnick <sam@chudnick.com>2022-06-30 04:25:26 -0400
commit01c24eb1f6f6a54bb780940c7665acd280b42aaf (patch)
tree75403dc126da5aac507985608148b59a4f5c0484
parent570d0da295f3e2fcd7b8c80ae2e6c42fc365abdd (diff)
Added support for TOTP
Added TOTP as an MFA option. Also made a couple of of minor changes. Changed all database connections to use with statement. Read some options from a config file.
-rwxr-xr-xclient/client.py31
-rwxr-xr-xpam/pam.py24
-rwxr-xr-xserver/mfac.py97
-rwxr-xr-xserver/mfad.py120
4 files changed, 180 insertions, 92 deletions
diff --git a/client/client.py b/client/client.py
index abbc9de..b2429b6 100755
--- a/client/client.py
+++ b/client/client.py
@@ -14,8 +14,10 @@ FORMAT = "utf-8"
14 14
15def parse_arguments(): 15def parse_arguments():
16 parser = argparse.ArgumentParser() 16 parser = argparse.ArgumentParser()
17 parser.add_argument("--server",type=str,help="IP of MFA Server",required=True) 17 parser.add_argument("--server",type=str,help="IP of MFA Server")
18 parser.add_argument("--port",type=int,help="Port to connect to",required=True) 18 parser.add_argument("--port",type=int,help="Port to connect to")
19 parser.add_argument("--config",type=str,help="Path to config file",\
20 default="/etc/mfa/mfa.conf")
19 parser.add_argument("--key",type=str,help="Client connection key",required=True) 21 parser.add_argument("--key",type=str,help="Client connection key",required=True)
20 return parser.parse_args() 22 return parser.parse_args()
21 23
@@ -50,13 +52,34 @@ def init_connection(mfa_server, client_port, client_key):
50 return connection 52 return connection
51 53
52 54
55def read_config(config_file):
56 # Read config file for server and port info
57 # Return tuple (server,port)
58 server = ""
59 port = 0
60 with open(config_file) as conf:
61 line = None
62 while line != "":
63 line = conf.readline()
64 if line.startswith("server ="):
65 server = line.split("=")[1].strip()
66 if line.startswith("port ="):
67 port = int(line.split("=")[1].strip())
68 return (server,port)
69
53def main(): 70def main():
54 # Get arguments, exit if unable to connect 71 # Get arguments, exit if unable to connect
55 args = parse_arguments() 72 args = parse_arguments()
56 mfa_server = args.server
57 client_port = args.port
58 client_key = args.key 73 client_key = args.key
59 74
75 # Read server and port from config file but allow command line options
76 # to override those settings
77 mfa_server, client_port = read_config(args.config)
78 if args.server != None:
79 mfa_server = args.server
80 if args.port != None:
81 client_port = args.port
82
60 # Exit if invalid key is provided 83 # Exit if invalid key is provided
61 if len(client_key) != KEY_LENGTH: 84 if len(client_key) != KEY_LENGTH:
62 print("invalid key") 85 print("invalid key")
diff --git a/pam/pam.py b/pam/pam.py
index 28450ee..5a2fee8 100755
--- a/pam/pam.py
+++ b/pam/pam.py
@@ -22,6 +22,11 @@ def parse_arguments():
22 parser = argparse.ArgumentParser() 22 parser = argparse.ArgumentParser()
23 parser.add_argument("--user",type=str,help="PAM username",required=True) 23 parser.add_argument("--user",type=str,help="PAM username",required=True)
24 parser.add_argument("--service",type=str,help="PAM service",required=True) 24 parser.add_argument("--service",type=str,help="PAM service",required=True)
25 parser.add_argument("--host",type=str,help="PAM hostname")
26 parser.add_argument("--config",type=str,help="Path to config file",\
27 default="/etc/mfa/mfa.conf")
28 parser.add_argument("--server",type=str,help="MFA server address")
29 parser.add_argument("--port",type=str,help="MFA server PAM connection port")
25 return parser.parse_args() 30 return parser.parse_args()
26 31
27def init_connection(mfa_server, pam_port): 32def init_connection(mfa_server, pam_port):
@@ -43,7 +48,7 @@ def init_connection(mfa_server, pam_port):
43 return None 48 return None
44 49
45 50
46def read_config(config_file="/etc/mfa/mfa.conf"): 51def read_config(config_file):
47 # Read config file for server and port info 52 # Read config file for server and port info
48 # Return tuple (server,port) 53 # Return tuple (server,port)
49 server = "" 54 server = ""
@@ -68,10 +73,19 @@ def main():
68 service = args.service 73 service = args.service
69 74
70 # Compile data to send to server 75 # Compile data to send to server
71 mfa_server, pam_port = read_config() 76 # Read server and port from config file but allow command line options
72 hostname = None 77 # to override those settings
73 with open("/etc/hostname") as f: 78 mfa_server, pam_port = read_config(args.config)
74 hostname = f.read().strip() 79 if args.server != None:
80 mfa_server = args.server
81 if args.port != None:
82 pam_port = args.port
83 # Get hostname if not given on command line
84 if args.host == None:
85 with open("/etc/hostname") as f:
86 hostname = f.read().strip()
87 else:
88 hostname = args.host
75 data = user + "," + hostname + "," + service 89 data = user + "," + hostname + "," + service
76 90
77 91
diff --git a/server/mfac.py b/server/mfac.py
index 779fa44..884c61c 100755
--- a/server/mfac.py
+++ b/server/mfac.py
@@ -6,7 +6,12 @@ import pyotp
6import sys 6import sys
7 7
8DB_NAME = "mfa.db" 8DB_NAME = "mfa.db"
9KEY_LENGTH = 64 9CLIENT_KEY_LENGTH = 64
10TOTP_KEY_LENGTH = 24
11
12CLIENT_ALIAS_INDEX = 0
13CLIENT_KEY_INDEX = 1
14CLIENT_TOTP_INDEX = 2
10 15
11def die(msg): 16def die(msg):
12 print(msg) 17 print(msg)
@@ -14,9 +19,10 @@ def die(msg):
14 19
15def parse_arguments(): 20def parse_arguments():
16 parser = argparse.ArgumentParser() 21 parser = argparse.ArgumentParser()
17 parser.add_argument("--add-client",action="store_true",help="Add a client")
18 parser.add_argument("--alias",type=str,help="Alias for new client") 22 parser.add_argument("--alias",type=str,help="Alias for new client")
19 23
24 parser.add_argument("--add-client",action="store_true",help="Add a client")
25
20 parser.add_argument("--add-app",action="store_true",help="Add an application") 26 parser.add_argument("--add-app",action="store_true",help="Add an application")
21 parser.add_argument("--user",type=str,help="Application username") 27 parser.add_argument("--user",type=str,help="Application username")
22 parser.add_argument("--host",type=str,help="Application hostname") 28 parser.add_argument("--host",type=str,help="Application hostname")
@@ -25,59 +31,76 @@ def parse_arguments():
25 31
26 parser.add_argument("--get-client",action="store_true",help="Get a client key") 32 parser.add_argument("--get-client",action="store_true",help="Get a client key")
27 33
34 parser.add_argument("--update-totp",action="store_true",help="Update a TOTP")
35
36 parser.add_argument("--get-totp",action="store_true",help="Get client's TOTP")
37
28 return parser.parse_args() 38 return parser.parse_args()
29 39
30 40
31def alias_exists(alias): 41def alias_exists(alias):
32 conn = sqlite3.connect(DB_NAME) 42 client = None
33 c = conn.cursor() 43 with sqlite3.connect(DB_NAME) as conn:
34 c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) 44 c = conn.cursor()
35 client = c.fetchall() 45 c.execute("SELECT * FROM clients WHERE alias=?",(alias,))
36 conn.close() 46 client = c.fetchall()
37 if len(client) == 0: 47 if len(client) == 0:
38 return False 48 return False
39 elif len(client) == 1: 49 elif len(client) == 1:
40 return True 50 return True
41 51
42 52
43def get_client_key(alias): 53def get_client_info(alias,info):
44 CLIENT_ALIAS_INDEX = 0
45 CLIENT_KEY_INDEX = 1
46 if not alias_exists(alias): 54 if not alias_exists(alias):
47 die("Error: alias does not exist") 55 die("Error: alias does not exist")
48 else: 56 else:
49 conn = sqlite3.connect(DB_NAME) 57 client = None
50 c = conn.cursor() 58 with sqlite3.connect(DB_NAME) as conn:
51 c.execute("SELECT * FROM clients WHERE alias=?",(alias,)) 59 c = conn.cursor()
52 client = c.fetchone() 60 c.execute("SELECT * FROM clients WHERE alias=?",(alias,))
53 conn.close() 61 client = c.fetchone()
54 return str(client[CLIENT_KEY_INDEX]) 62 if info == "key":
63 return str(client[CLIENT_KEY_INDEX])
64 elif info == "totp":
65 return str(client[CLIENT_TOTP_INDEX])
66
67
68def update_totp(alias):
69 if alias_exists(alias):
70 totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH)
71 with sqlite3.connect(DB_NAME) as conn:
72 c = conn.cursor()
73 c.execute("UPDATE clients SET totp_secret=? WHERE alias=?",\
74 (totp_secret,alias))
75 print("totp secret: " + totp_secret)
76 else:
77 die("error: alias does not exist")
55 78
56 79
57def add_client(alias): 80def add_client(alias):
58 if not alias_exists(alias): 81 if not alias_exists(alias):
59 key = pyotp.random_base32(length=64) 82 client_key = pyotp.random_base32(CLIENT_KEY_LENGTH)
60 conn = sqlite3.connect(DB_NAME) 83 totp_secret = pyotp.random_base32(TOTP_KEY_LENGTH)
61 c = conn.cursor() 84 with sqlite3.connect(DB_NAME) as conn:
62 c.execute("INSERT INTO clients VALUES (?,?)",(alias,key)) 85 c = conn.cursor()
63 conn.commit() 86 c.execute("INSERT INTO clients VALUES (?,?,?)",\
64 conn.close() 87 (alias,client_key,totp_secret))
65 print("key: " + key) 88
89 print("client key: " + client_key)
90 print("totp secret: " + totp_secret)
91 print("uri: " + pyotp.TOTP(totp_secret).provisioning_uri(alias+"@mfad"))
66 else: 92 else:
67 die("Error: alias already used") 93 die("Error: alias already used")
68 94
69
70def add_app(username, hostname, service, alias, mfa_methods): 95def add_app(username, hostname, service, alias, mfa_methods):
71 if not alias_exists(alias): 96 if not alias_exists(alias):
72 die("Error: alias does not exist") 97 die("Error: alias does not exist")
73 else: 98 else:
74 client_key = get_client_key(alias) 99 client_key = get_client_info(alias, "key")
75 conn = sqlite3.connect(DB_NAME) 100 with sqlite3.connect(DB_NAME) as conn:
76 c = conn.cursor() 101 c = conn.cursor()
77 c.execute("INSERT INTO applications VALUES (?,?,?,?,?)", 102 c.execute("INSERT INTO applications VALUES (?,?,?,?,?)",
78 (username,hostname,service,alias,mfa_methods)) 103 (username,hostname,service,alias,mfa_methods))
79 conn.commit()
80 conn.close()
81 104
82 105
83def main(): 106def main():
@@ -90,6 +113,10 @@ def main():
90 die("Error: must specify alias to provision a client") 113 die("Error: must specify alias to provision a client")
91 if args.get_client and args.alias == None: 114 if args.get_client and args.alias == None:
92 die("Error: no alias specified") 115 die("Error: no alias specified")
116 if args.update_totp and args.alias == None:
117 die("Error: no alias specified")
118 if args.get_totp and args.alias == None:
119 die("Error: no alias specified")
93 if args.add_app and (args.user == None or args.host == None or \ 120 if args.add_app and (args.user == None or args.host == None or \
94 args.service == None or args.alias == None \ 121 args.service == None or args.alias == None \
95 or args.methods == None): 122 or args.methods == None):
@@ -98,11 +125,17 @@ def main():
98 if args.add_client: 125 if args.add_client:
99 add_client(args.alias) 126 add_client(args.alias)
100 elif args.get_client: 127 elif args.get_client:
101 key = get_client_key(args.alias) 128 key = get_client_info(args.alias,"key")
102 print(key) 129 print(key)
103 elif args.add_app: 130 elif args.add_app:
104 methods = " ".join(args.methods) 131 methods = " ".join(args.methods)
105 add_app(args.user,args.host,args.service,args.alias,methods) 132 add_app(args.user,args.host,args.service,args.alias,methods)
133 elif args.update_totp:
134 update_totp(args.alias)
135 elif args.get_totp:
136 secret = get_client_info(args.alias,"totp")
137 print(secret)
138 print(pyotp.TOTP(secret).provisioning_uri(args.alias+"@mfad"))
106 139
107 140
108 141
diff --git a/server/mfad.py b/server/mfad.py
index 7a2fc40..d045e14 100755
--- a/server/mfad.py
+++ b/server/mfad.py
@@ -26,28 +26,54 @@ FORMAT = "utf-8"
26AUTHED = 0 26AUTHED = 0
27DENIED = 1 27DENIED = 1
28 28
29# DB object index constants
30DB_USERNAME_INDEX = 0
31DB_HOSTNAME_INDEX = 1
32DB_SERVICE_INDEX = 2
33DB_ALIAS_INDEX = 3
34DB_MFAMETHODS_INDEX = 4
35
36CLIENT_ALIAS_INDEX = 0
37CLIENT_KEY_INDEX = 1
38CLIENT_SECRET_INDEX = 2
39
29# Stores connected clients as a dictionary with the client key as the dictionary 40# Stores connected clients as a dictionary with the client key as the dictionary
30# key and a tuple of (socket,(addr,port)) as the value 41# key and a tuple of (socket,(addr,port)) as the value
31client_connections = dict() 42client_connections = dict()
32 43
33 44
34def eval_mfa(mfa_methods,client_response): 45def eval_mfa(client_key, mfa_methods, client_response):
46 print("response: " + client_response)
47 print("length: " + str(len(client_response)))
48 print("methods: " + str(mfa_methods))
35 # Evaluates MFA and decides if authenticated or denied 49 # Evaluates MFA and decides if authenticated or denied
36 # Returns 0 for authenticated on 1 for denied 50 # Returns 0 for authenticated on 1 for denied
37 if "push" in mfa_methods and client_response == "allow": 51 if "push" in mfa_methods and client_response == "allow":
38 return AUTHED 52 return AUTHED
39 elif "totp" in mfa_methods and len(client_response) == 6: 53 elif "totp" in mfa_methods and len(client_response) == 6:
40 # Only attempt to validate if response is a valid TOTP format 54 # Only attempt to validate if response is a valid TOTP format
41 totp_format = (r'(\d)(\d)(\d)(\d)(\d)(\d)') 55 totp_format = (r'(\d)(\d)(\d)(\d)(\d)(\d)')
42 totp_regex = re.compile(totp_format) 56 totp_regex = re.compile(totp_format)
43 matched = totp_regex.match(client_response) 57 matched = totp_regex.match(client_response)
44 if matched: 58 if matched:
45 return validate_totp(int(client_response)) 59 return validate_totp(client_key, client_response)
46 return DENIED 60 return DENIED
47 61
48 62
49def validate_totp(client_response): 63def validate_totp(client_key, client_response):
50 pass 64 secret = ""
65 with sqlite3.connect(DB_NAME) as conn:
66 c = conn.cursor()
67 c.execute("SELECT * FROM clients WHERE key=?",(client_key,))
68 client = c.fetchone()
69 secret = client[CLIENT_SECRET_INDEX]
70 totp = pyotp.TOTP(secret)
71 print("Client Response: " + str(client_response))
72 print("Valid TOTP: " + str(totp.now()))
73 if totp.verify(client_response):
74 return AUTHED
75 else:
76 return DENIED
51 77
52 78
53################################################################################ 79################################################################################
@@ -64,28 +90,22 @@ def get_client_key(username,hostname,service):
64 # This is done by checking the PAM request against a preconfigured 90 # This is done by checking the PAM request against a preconfigured
65 # database mapping request info (username,hostname,etc...) to clients 91 # database mapping request info (username,hostname,etc...) to clients
66 # Returns a tuple consisting of the key and approved MFA methods 92 # Returns a tuple consisting of the key and approved MFA methods
67 DB_USERNAME_INDEX = 0 93
68 DB_HOSTNAME_INDEX = 1 94 application = None
69 DB_SERVICE_INDEX = 2 95 client = None
70 DB_ALIAS_INDEX = 3 96 with sqlite3.connect(DB_NAME) as conn:
71 DB_MFAMETHODS_INDEX = 4 97 c = conn.cursor()
72 98 c.execute("""SELECT * FROM applications WHERE username=? AND hostname=?
73 CLIENT_ALIAS_INDEX = 0 99 AND service=?""",(username,hostname,service))
74 CLIENT_KEY_INDEX = 1 100 application = c.fetchone()
75 101 # Return None if no results found
76 conn = sqlite3.connect(DB_NAME) 102 if application == None:
77 c = conn.cursor() 103 return application
78 c.execute("""SELECT * FROM applications WHERE username=? AND hostname=? 104
79 AND service=?""",(username,hostname,service)) 105 alias = application[DB_ALIAS_INDEX]
80 application = c.fetchone() 106 c.execute("SELECT * FROM clients WHERE alias=?",(alias,))
81 # Return None if no results found 107 client = c.fetchone()
82 if application == None: 108
83 return application
84
85 alias = application[DB_ALIAS_INDEX]
86 c.execute("SELECT * FROM clients WHERE alias=?",(alias,))
87 client = c.fetchone()
88 conn.close()
89 client_key = client[CLIENT_KEY_INDEX] 109 client_key = client[CLIENT_KEY_INDEX]
90 methods = application[DB_MFAMETHODS_INDEX] 110 methods = application[DB_MFAMETHODS_INDEX]
91 return (client_key,methods) 111 return (client_key,methods)
@@ -129,11 +149,12 @@ def prompt_client(client_key, user, host, service, methods, timeout=10):
129 149
130def validate_client(client_key): 150def validate_client(client_key):
131 # Validates a client 151 # Validates a client
132 conn = sqlite3.connect(DB_NAME) 152 client = None
133 c = conn.cursor() 153 with sqlite3.connect(DB_NAME) as conn:
134 c.execute("SELECT * FROM clients WHERE key=?",(client_key,)) 154 c = conn.cursor()
135 client = c.fetchall() 155 c.execute("SELECT * FROM clients WHERE key=?",(client_key,))
136 conn.close() 156 client = c.fetchall()
157
137 if len(client) == 0: 158 if len(client) == 0:
138 # No client matches provided key, invalid 159 # No client matches provided key, invalid
139 return False 160 return False
@@ -172,7 +193,7 @@ def handle_pam(conn, addr):
172 193
173 # Correlate request to client 194 # Correlate request to client
174 client_key,mfa_methods = get_client_key(user,host,service) 195 client_key,mfa_methods = get_client_key(user,host,service)
175 mfa_methods = mfa_methods.split(',') 196 mfa_methods = mfa_methods.split(' ')
176 if client_key == None: 197 if client_key == None:
177 print("No applications found for user="+user+" host="+host+" service="+service) 198 print("No applications found for user="+user+" host="+host+" service="+service)
178 conn.send(str(DENIED).encode(FORMAT)) 199 conn.send(str(DENIED).encode(FORMAT))
@@ -182,8 +203,7 @@ def handle_pam(conn, addr):
182 response = prompt_client(client_key,user,host,service,mfa_methods) 203 response = prompt_client(client_key,user,host,service,mfa_methods)
183 204
184 # Evaluate Response 205 # Evaluate Response
185 auth_type = "push" 206 decision = eval_mfa(client_key, mfa_methods, response)
186 decision = eval_mfa(auth_type, response)
187 207
188 # Return response to PAM module 208 # Return response to PAM module
189 # Respone will either be 0 for authenticated and 1 for denied 209 # Respone will either be 0 for authenticated and 1 for denied
@@ -209,22 +229,20 @@ def listen_pam(addr, port):
209################################################################################ 229################################################################################
210 230
211def create_db(): 231def create_db():
212 conn = sqlite3.connect(DB_NAME) 232 with sqlite3.connect(DB_NAME) as conn:
213 c = conn.cursor() 233 c = conn.cursor()
214 c.execute("""CREATE TABLE applications ( 234 c.execute("""CREATE TABLE applications (
215 username text, 235 username text,
216 hostname text, 236 hostname text,
217 service text, 237 service text,
218 client_key text, 238 client_key text,
219 mfa_methods text 239 mfa_methods text
220 )""") 240 )""")
221 conn.commit() 241 c.execute("""CREATE TABLE clients (
222 c.execute("""CREATE TABLE clients ( 242 alias text,
223 alias, 243 key text,
224 key 244 totp_secret text
225 )""") 245 )""")
226 conn.commit()
227 conn.close()
228 246
229 247
230def main(): 248def main():