summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorSam Chudnick <sam@chudnick.com>2022-06-27 20:41:01 -0400
committerSam Chudnick <sam@chudnick.com>2022-06-27 20:41:01 -0400
commit570d0da295f3e2fcd7b8c80ae2e6c42fc365abdd (patch)
treeccfe8614fc644b08db5d15091c344c77fff44fb1 /server
Initial commit
Diffstat (limited to 'server')
-rwxr-xr-xserver/mfac.py113
-rwxr-xr-xserver/mfad.py246
2 files changed, 359 insertions, 0 deletions
diff --git a/server/mfac.py b/server/mfac.py
new file mode 100755
index 0000000..779fa44
--- /dev/null
+++ b/server/mfac.py
@@ -0,0 +1,113 @@
1#!/usr/bin/env python3
2
3import argparse
4import sqlite3
5import pyotp
6import sys
7
8DB_NAME = "mfa.db"
9KEY_LENGTH = 64
10
11def die(msg):
12 print(msg)
13 sys.exit(1)
14
15def parse_arguments():
16 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")
19
20 parser.add_argument("--add-app",action="store_true",help="Add an application")
21 parser.add_argument("--user",type=str,help="Application username")
22 parser.add_argument("--host",type=str,help="Application hostname")
23 parser.add_argument("--service",type=str,help="Application service name")
24 parser.add_argument("--methods",type=str,nargs="+",help="Allowed MFA methods")
25
26 parser.add_argument("--get-client",action="store_true",help="Get a client key")
27
28 return parser.parse_args()
29
30
31def alias_exists(alias):
32 conn = sqlite3.connect(DB_NAME)
33 c = conn.cursor()
34 c.execute("SELECT * FROM clients WHERE alias=?",(alias,))
35 client = c.fetchall()
36 conn.close()
37 if len(client) == 0:
38 return False
39 elif len(client) == 1:
40 return True
41
42
43def get_client_key(alias):
44 CLIENT_ALIAS_INDEX = 0
45 CLIENT_KEY_INDEX = 1
46 if not alias_exists(alias):
47 die("Error: alias does not exist")
48 else:
49 conn = sqlite3.connect(DB_NAME)
50 c = conn.cursor()
51 c.execute("SELECT * FROM clients WHERE alias=?",(alias,))
52 client = c.fetchone()
53 conn.close()
54 return str(client[CLIENT_KEY_INDEX])
55
56
57def add_client(alias):
58 if not alias_exists(alias):
59 key = pyotp.random_base32(length=64)
60 conn = sqlite3.connect(DB_NAME)
61 c = conn.cursor()
62 c.execute("INSERT INTO clients VALUES (?,?)",(alias,key))
63 conn.commit()
64 conn.close()
65 print("key: " + key)
66 else:
67 die("Error: alias already used")
68
69
70def add_app(username, hostname, service, alias, mfa_methods):
71 if not alias_exists(alias):
72 die("Error: alias does not exist")
73 else:
74 client_key = get_client_key(alias)
75 conn = sqlite3.connect(DB_NAME)
76 c = conn.cursor()
77 c.execute("INSERT INTO applications VALUES (?,?,?,?,?)",
78 (username,hostname,service,alias,mfa_methods))
79 conn.commit()
80 conn.close()
81
82
83def main():
84 args = parse_arguments()
85 # Sanity checks
86 if (args.add_client and args.add_app) or (args.add_client and args.get_client) \
87 or (args.get_client and args.add_app):
88 die("Error: cannot specify multiple actions")
89 if args.add_client and args.alias == None:
90 die("Error: must specify alias to provision a client")
91 if args.get_client and args.alias == None:
92 die("Error: no alias specified")
93 if args.add_app and (args.user == None or args.host == None or \
94 args.service == None or args.alias == None \
95 or args.methods == None):
96 die("Error: --add-app requires all of --user,--host,--service,--alias,--methods")
97
98 if args.add_client:
99 add_client(args.alias)
100 elif args.get_client:
101 key = get_client_key(args.alias)
102 print(key)
103 elif args.add_app:
104 methods = " ".join(args.methods)
105 add_app(args.user,args.host,args.service,args.alias,methods)
106
107
108
109
110if __name__ == '__main__':
111 main()
112
113
diff --git a/server/mfad.py b/server/mfad.py
new file mode 100755
index 0000000..7a2fc40
--- /dev/null
+++ b/server/mfad.py
@@ -0,0 +1,246 @@
1#!/usr/bin/env python3
2import socket
3import os
4import sys
5import time
6import threading
7import pyotp
8import sqlite3
9import re
10
11## Listens for authentication request from PAM module
12## Recevies connection from client
13## Correlates authentication request to client connection
14## Sends MFA prompt to client
15## Evaluates response from client
16## Return pass or fail response to PAM moudle
17
18
19DB_NAME = "mfa.db"
20HEADER_LENGTH = 64
21KEY_LENGTH = 64
22DISCONNECT_LENGTH = ACK_LENGTH = 3
23ACK_MESSAGE = "ACK"
24DISCONNECT_MESSAGE = "BYE"
25FORMAT = "utf-8"
26AUTHED = 0
27DENIED = 1
28
29# 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
31client_connections = dict()
32
33
34def eval_mfa(mfa_methods,client_response):
35 # Evaluates MFA and decides if authenticated or denied
36 # Returns 0 for authenticated on 1 for denied
37 if "push" in mfa_methods and client_response == "allow":
38 return AUTHED
39 elif "totp" in mfa_methods and len(client_response) == 6:
40 # Only attempt to validate if response is a valid TOTP format
41 totp_format = (r'(\d)(\d)(\d)(\d)(\d)(\d)')
42 totp_regex = re.compile(totp_format)
43 matched = totp_regex.match(client_response)
44 if matched:
45 return validate_totp(int(client_response))
46 return DENIED
47
48
49def validate_totp(client_response):
50 pass
51
52
53################################################################################
54
55# Client is registered by admin with secret key stored on server
56# Client is provisioned with secret key and passes secret key to server on
57# connection for identification
58# Client key is used to identify client throughout communication process
59
60# //TODO RSA public/private key pairs for proper authentication
61
62def get_client_key(username,hostname,service):
63 # Correlates a PAM request to a registered client
64 # This is done by checking the PAM request against a preconfigured
65 # database mapping request info (username,hostname,etc...) to clients
66 # Returns a tuple consisting of the key and approved MFA methods
67 DB_USERNAME_INDEX = 0
68 DB_HOSTNAME_INDEX = 1
69 DB_SERVICE_INDEX = 2
70 DB_ALIAS_INDEX = 3
71 DB_MFAMETHODS_INDEX = 4
72
73 CLIENT_ALIAS_INDEX = 0
74 CLIENT_KEY_INDEX = 1
75
76 conn = sqlite3.connect(DB_NAME)
77 c = conn.cursor()
78 c.execute("""SELECT * FROM applications WHERE username=? AND hostname=?
79 AND service=?""",(username,hostname,service))
80 application = c.fetchone()
81 # Return None if no results found
82 if application == None:
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]
90 methods = application[DB_MFAMETHODS_INDEX]
91 return (client_key,methods)
92
93
94
95def prompt_client(client_key, user, host, service, methods, timeout=10):
96 # Prompts client for MFA
97 timer = 0
98 while timer < timeout:
99 if client_key in client_connections.keys():
100 conn = client_connections[client_key][0]
101 # Use try block to catch cases where client was connected and so
102 # is in list but is not currently connected
103 try:
104 # send prompts
105 methodstr = ", ".join(methods)
106 methodstr = "Available methods: " + methodstr
107 prompt_msg = "Login approved for user '" + user + \
108 "' attempting to access service '" + service + \
109 "' on host '" + host + "'?\n" + methodstr
110 prompt_len = len(prompt_msg)
111 length_msg = str(prompt_len)
112 length_msg += ' ' * (HEADER_LENGTH - len(length_msg))
113 conn.send(length_msg.encode(FORMAT))
114 conn.send(prompt_msg.encode(FORMAT))
115 # receive response
116 response_length = int(conn.recv(HEADER_LENGTH).decode(FORMAT))
117 response = conn.recv(response_length).decode(FORMAT)
118 return response
119 except BrokenPipeError:
120 client_connections.pop(client_key)
121 timer += 1
122 time.sleep(1)
123 else:
124 timer +=1
125 time.sleep(1)
126
127 return 0
128
129
130def validate_client(client_key):
131 # Validates a client
132 conn = sqlite3.connect(DB_NAME)
133 c = conn.cursor()
134 c.execute("SELECT * FROM clients WHERE key=?",(client_key,))
135 client = c.fetchall()
136 conn.close()
137 if len(client) == 0:
138 # No client matches provided key, invalid
139 return False
140 elif len(client) == 1:
141 return True
142 else:
143 print("A strange error has occurred")
144 return False
145
146
147
148def handle_client(conn, addr):
149 # Receive key from client
150 key = conn.recv(KEY_LENGTH).decode(FORMAT)
151 # Validate client
152 if not validate_client(key):
153 print("WARNING: client attempted to connect with invalid key")
154 conn.send(DISCONNECT_MESSAGE.encode(FORMAT))
155 conn.close()
156 else:
157 conn.send(ACK_MESSAGE.encode(FORMAT))
158 client_connections[key] = (conn,addr)
159 print("client connected with key " + key)
160
161
162def parse_pam_data(data):
163 # Parses pam data and returns (user,host,service) tuple
164 return tuple(data.split(','))
165
166def handle_pam(conn, addr):
167 # Get request and data from PAM module
168 data_length = int(conn.recv(HEADER_LENGTH).decode(FORMAT))
169 pam_data = conn.recv(data_length).decode(FORMAT)
170 print("Got pam_data: " + pam_data)
171 user,host,service = parse_pam_data(pam_data)
172
173 # Correlate request to client
174 client_key,mfa_methods = get_client_key(user,host,service)
175 mfa_methods = mfa_methods.split(',')
176 if client_key == None:
177 print("No applications found for user="+user+" host="+host+" service="+service)
178 conn.send(str(DENIED).encode(FORMAT))
179 return
180
181 # Prompt client
182 response = prompt_client(client_key,user,host,service,mfa_methods)
183
184 # Evaluate Response
185 auth_type = "push"
186 decision = eval_mfa(auth_type, response)
187
188 # Return response to PAM module
189 # Respone will either be 0 for authenticated and 1 for denied
190 conn.send(str(decision).encode(FORMAT))
191
192
193def listen_client(addr, port):
194 with socket.create_server((addr, port)) as server:
195 while True:
196 conn, addr = server.accept()
197 thread = threading.Thread(target=handle_client,args=(conn,addr))
198 thread.start()
199
200
201def listen_pam(addr, port):
202 with socket.create_server((addr,port)) as pam_server:
203 while True:
204 conn, addr = pam_server.accept()
205 thread = threading.Thread(target=handle_pam,args=(conn,addr))
206 thread.start()
207
208
209################################################################################
210
211def create_db():
212 conn = sqlite3.connect(DB_NAME)
213 c = conn.cursor()
214 c.execute("""CREATE TABLE applications (
215 username text,
216 hostname text,
217 service text,
218 client_key text,
219 mfa_methods text
220 )""")
221 conn.commit()
222 c.execute("""CREATE TABLE clients (
223 alias,
224 key
225 )""")
226 conn.commit()
227 conn.close()
228
229
230def main():
231 global connection_list
232 bind_addr = "127.0.0.1"
233 pam_port = 8000
234 client_port = 8001
235
236 if not os.path.exists(DB_NAME):
237 create_db()
238
239 clients = threading.Thread(target=listen_client,args=(bind_addr,client_port))
240 pam = threading.Thread(target=listen_pam,args=(bind_addr,pam_port))
241 clients.start()
242 pam.start()
243
244
245if __name__ == '__main__':
246 main()