Easy CA management
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
9.0 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. #!/usr/bin/env python3
  2. import hashlib
  3. import json
  4. import os
  5. import os.path
  6. import shutil
  7. import sqlite3
  8. import subprocess
  9. import tempfile
  10. MANAGER_PATH = "/var/lib/ca_manager/private"
  11. REQUESTS_PATH = "/var/lib/ca_manager/requests"
  12. OUTPUT_PATH = "/var/lib/ca_manager/outputs"
  13. RESULTS_PATH = "/var/lib/ca_manager/results"
  14. class SignRequest(object):
  15. def __init__(self, req_id):
  16. self.req_id = req_id
  17. def get_name(self):
  18. raise NotImplementedError()
  19. def get_fields(self):
  20. raise NotImplementedError()
  21. class UserSSHRequest(SignRequest):
  22. def __init__(self, req_id, user_name, root_requested, key_data):
  23. super().__init__(req_id)
  24. self.user_name = user_name
  25. self.root_requested = root_requested
  26. self.key_data = key_data
  27. def get_name(self):
  28. return "User: %s [R:%d]" % (self.user_name, int(self.root_requested))
  29. def get_fields(self):
  30. return [
  31. ("User name", self.user_name),
  32. ("Root access requested", 'yes' if self.root_requested else 'no')
  33. ]
  34. class HostSSHRequest(SignRequest):
  35. def __init__(self, req_id, host_name, key_data):
  36. super().__init__(req_id)
  37. self.host_name = host_name
  38. self.key_data = key_data
  39. def get_name(self):
  40. return "Hostname: %s" % self.host_name
  41. def get_fields(self):
  42. return [
  43. ("Hostname", self.host_name)
  44. ]
  45. class Authority(object):
  46. ca_type = None
  47. def __init__(self, ca_id, name, path):
  48. self.ca_id = ca_id
  49. self.name = name
  50. self.path = path
  51. def generate(self):
  52. raise NotImplementedError()
  53. def sign(self, request):
  54. raise NotImplementedError()
  55. class SSHAuthority(Authority):
  56. ca_type = 'ssh'
  57. key_algorithm = 'ed25519'
  58. user_validity = '+52w'
  59. host_validity = '+52w'
  60. def generate(self):
  61. if os.path.exists(self.path):
  62. raise ValueError("A CA with the same id and type already exists")
  63. subprocess.call(['ssh-keygen',
  64. '-f', self.path,
  65. '-t', self.key_algorithm,
  66. '-C', self.name])
  67. with open(self.path + '.serial', 'w') as stream:
  68. stream.write(str(0))
  69. def sign(self, request):
  70. global OUTPUT_PATH
  71. assert type(request) in [UserSSHRequest, HostSSHRequest]
  72. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  73. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  74. with open(self.path + '.serial', 'r') as stream:
  75. next_serial = int(stream.read())
  76. with open(self.path + '.serial', 'w') as stream:
  77. stream.write(str(next_serial + 1))
  78. with open(pub_key_path, 'w') as stream:
  79. stream.write(request.key_data)
  80. ca_private_key = self.path
  81. if type(request) == UserSSHRequest:
  82. login_names = [request.user_name]
  83. if request.root_requested:
  84. login_names.append('root')
  85. subprocess.call(['ssh-keygen',
  86. '-s', ca_private_key,
  87. '-I', 'user_%s' % request.user_name,
  88. '-n', ','.join(login_names),
  89. '-V', self.user_validity,
  90. '-z', str(next_serial),
  91. pub_key_path])
  92. elif type(request) == HostSSHRequest:
  93. subprocess.call(['ssh-keygen',
  94. '-s', ca_private_key,
  95. '-I', 'host_%s' % request.host_name.replace('.', '_'),
  96. '-h',
  97. '-n', request.host_name,
  98. '-V', self.host_validity,
  99. '-z', str(next_serial),
  100. pub_key_path])
  101. return cert_path
  102. class CAManager(object):
  103. def __init__(self, path):
  104. self.path = path
  105. def __enter__(self):
  106. self.conn = sqlite3.connect(self._get_db_path())
  107. return self
  108. def __exit__(self, exc_type, exc_value, traceback):
  109. if exc_type is not None:
  110. print(exc_type, exc_value)
  111. print(traceback)
  112. self.conn.close()
  113. def _get_db_path(self):
  114. return os.path.join(self.path, 'ca_manager.db')
  115. def _get_ssh_cas_dir(self):
  116. return os.path.join(self.path, 'ssh_cas')
  117. def _get_ssh_ca_path(self, ca_id):
  118. cas_dir = self._get_ssh_cas_dir()
  119. return os.path.join(cas_dir, ca_id)
  120. def create_ssh_ca(self, ca_id, ca_name):
  121. ca_path = self._get_ssh_ca_path(ca_id)
  122. authority = SSHAuthority(ca_id, ca_name, ca_path)
  123. authority.generate()
  124. c = self.conn.cursor()
  125. c.execute("""INSERT INTO cas VALUES (?, ?, 'ssh')""",
  126. (ca_id, ca_name))
  127. self.conn.commit()
  128. def get_cas_list(self):
  129. c = self.conn.cursor()
  130. c.execute("""SELECT id, name, type FROM cas""")
  131. return c.fetchall()
  132. def get_ca(self, ca_id):
  133. c = self.conn.cursor()
  134. c.execute("""SELECT name, type FROM cas WHERE id = ?""", (ca_id, ))
  135. ca_name, ca_type = c.fetchone()
  136. ca_path = self._get_ssh_ca_path(ca_id)
  137. if ca_type == 'ssh':
  138. return SSHAuthority(ca_id, ca_name, ca_path)
  139. def get_requests(self):
  140. global REQUESTS_PATH
  141. req_objs = []
  142. for request_name in os.listdir(REQUESTS_PATH):
  143. request_path = os.path.join(REQUESTS_PATH, request_name)
  144. with open(request_path, 'r') as stream:
  145. req = json.load(stream)
  146. if req['keyType'] == 'ssh_user':
  147. user_name = req['userName']
  148. root_requested = req['rootRequested']
  149. key_data = req['keyData']
  150. req_objs.append(
  151. UserSSHRequest(
  152. request_name, user_name, root_requested, key_data))
  153. return req_objs
  154. def drop_request(self, request):
  155. global REQUESTS_PATH
  156. os.unlink(os.path.join(REQUESTS_PATH, request.req_id))
  157. def init_manager(path):
  158. db_path = os.path.join(path, 'ca_manager.db')
  159. directories = ['ssh_cas']
  160. for dirname in directories:
  161. dirpath = os.path.join(path, dirname)
  162. if not os.path.exists(dirpath):
  163. os.mkdir(dirpath)
  164. if not os.path.exists(db_path):
  165. conn = sqlite3.connect(db_path)
  166. c = conn.cursor()
  167. c.execute("""CREATE TABLE cas (id text, name text, type text)""")
  168. conn.commit()
  169. conn.close()
  170. def main():
  171. global MANAGER_PATH
  172. init_manager(MANAGER_PATH)
  173. menu_entries = [
  174. ("list-cas", "List available CAs"),
  175. ("show-ca", "Show CA info"),
  176. ("gen-ssh-ca", "Generate SSH CA"),
  177. ("sign-request", "Sign request"),
  178. ("help", "Show this message"),
  179. ("quit", "Quit from CA manager")
  180. ]
  181. with CAManager(MANAGER_PATH) as ca_manager:
  182. print("# LILiK CA Manager")
  183. exiting = False
  184. while not exiting:
  185. selection = input('Command> ')
  186. if selection == 'help':
  187. print("Available commands:")
  188. for entry_id, entry_name in menu_entries:
  189. print("%-13s : %s" % (entry_id, entry_name))
  190. elif selection == 'quit':
  191. exiting = True
  192. elif selection == 'list-cas':
  193. list_cas(ca_manager)
  194. elif selection == 'show-ca':
  195. pass
  196. elif selection == 'gen-ssh-ca':
  197. ca_id = input("CA unique id> ")
  198. ca_name = input("CA human-readable name> ")
  199. ca_manager.create_ssh_ca(ca_id, ca_name)
  200. elif selection == 'sign-request':
  201. sign_request(ca_manager)
  202. else:
  203. print("Unrecognized command. Type 'help' to show available "
  204. "commands.")
  205. def list_cas(ca_manager):
  206. for ca_id, ca_name, ca_type in ca_manager.get_cas_list():
  207. print("- [%3s] %-15s (%s)" % (ca_type, ca_id, ca_name))
  208. def sign_request(ca_manager):
  209. global RESULTS_PATH
  210. list_cas(ca_manager)
  211. ca_selection = input('Select a CA> ')
  212. try:
  213. authority = ca_manager.get_ca(ca_selection)
  214. except:
  215. print("Could not find CA '%s'" % ca_selection)
  216. return
  217. requests = ca_manager.get_requests()
  218. for i, request in enumerate(requests):
  219. print("%2d) %s - %s" % (i, request.req_id, request.get_name()))
  220. req_selection = input('Select a request> ')
  221. try:
  222. req_selection = int(req_selection)
  223. req_obj = requests[req_selection]
  224. except:
  225. return
  226. print("Request details:")
  227. for field_name, field_value in req_obj.get_fields():
  228. print("- %s: %s" % (field_name, field_value))
  229. h = hashlib.sha256()
  230. h.update(req_obj.key_data.encode('utf-8'))
  231. print("Request hash: %s" % h.hexdigest())
  232. print("You are about to sign this request with the following CA:")
  233. print("- %s (%s)" % (authority.ca_id, authority.name))
  234. confirm = input('Proceed? (type yes)> ')
  235. if confirm != 'yes':
  236. return
  237. cert_path = authority.sign(request)
  238. ca_manager.drop_request(request)
  239. shutil.copy(cert_path, os.path.join(RESULTS_PATH, request.req_id))
  240. if __name__ == '__main__':
  241. main()