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.

302 lines
8.3 KiB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import os.path
  5. import sqlite3
  6. import subprocess
  7. import json
  8. from paths import *
  9. __doc__= """
  10. Module of classes to handle certificate requests
  11. """
  12. class SignRequest(object):
  13. def __init__(self, req_id):
  14. self.req_id = req_id
  15. def __repr__(self):
  16. return ( "%s %s" % ( str(self.__class__.__name__), str(self.req_id) ) )
  17. @property
  18. def name(self):
  19. raise NotImplementedError()
  20. @property
  21. def fields(self):
  22. raise NotImplementedError()
  23. @property
  24. def path(self):
  25. return os.path.join(REQUESTS_PATH, self.req_id)
  26. class RequestLoader(object):
  27. """
  28. Context manager that loads a request from a file
  29. and return a Request type
  30. """
  31. def __init__(self, request_id):
  32. self.request_id = request_id
  33. self.request_dir = REQUESTS_PATH
  34. @property
  35. def path(self):
  36. return os.path.join(self.request_dir, self.request_id)
  37. def __enter__(self):
  38. with open(self.path, 'r') as stream:
  39. request_data = json.load(
  40. stream,
  41. )
  42. requester = request_data.get('userName', None) or request_data.get('hostName', None)
  43. root_requested = request_data.get('rootRequested', False)
  44. key_data = request_data.get('keyData', None)
  45. # attribute cannot be read from
  46. # json, must add after decoding
  47. request_id = self.request_id
  48. values = request_data.values()
  49. if 'ssh_user' in values:
  50. return UserSSHRequest(
  51. request_id,
  52. requester,
  53. root_requested,
  54. key_data,
  55. )
  56. elif 'ssh_host' in values:
  57. return HostSSHRequest(
  58. request_id,
  59. requester,
  60. key_data,
  61. )
  62. elif 'ssl_host' in values:
  63. return HostSSLRequest(
  64. request_id,
  65. requester,
  66. key_data,
  67. )
  68. else:
  69. # ultimate error, cannot be decoded
  70. return SignRequest(request_id)
  71. def __exit__(self, exc_type, exc_value, traceback):
  72. if exc_type is not None:
  73. print(exc_type, exc_value)
  74. print(traceback)
  75. class UserSSHRequest(SignRequest, object):
  76. def __init__(self, req_id, user_name, root_requested, key_data):
  77. super(UserSSHRequest, self).__init__(req_id)
  78. self.user_name = user_name
  79. self.root_requested = root_requested
  80. self.key_data = key_data
  81. @property
  82. def name(self):
  83. return "User: %s [R:%d]" % (self.user_name, int(self.root_requested))
  84. @property
  85. def fields(self):
  86. return [
  87. ("User name", self.user_name),
  88. ("Root access requested", 'yes' if self.root_requested else 'no')
  89. ]
  90. class HostSSLRequest(SignRequest, object):
  91. def __init__(self, req_id, host_name, key_data):
  92. super(HostSSLRequest, self).__init__(req_id)
  93. self.host_name = host_name
  94. self.key_data = key_data
  95. @property
  96. def name(self):
  97. return "Hostname: %s" % self.host_name
  98. @property
  99. def fields(self):
  100. return [
  101. ("Hostname", self.host_name)
  102. ]
  103. class HostSSHRequest(SignRequest, object):
  104. def __init__(self, req_id, host_name, key_data):
  105. super(HostSSHRequest, self).__init__(req_id)
  106. self.host_name = host_name
  107. self.key_data = key_data
  108. @property
  109. def name(self):
  110. return "Hostname: %s" % self.host_name
  111. @property
  112. def fields(self):
  113. return [
  114. ("Hostname", self.host_name)
  115. ]
  116. class Authority(object):
  117. ca_type = None
  118. request_allowed = []
  119. def __init__(self, ca_id, name, ca_dir):
  120. self.ca_id = ca_id
  121. self.name = name
  122. self.ca_dir = ca_dir
  123. @property
  124. def path(self):
  125. return os.path.join(self.ca_dir, self.ca_id)
  126. def generate(self):
  127. raise NotImplementedError()
  128. def sign(self, request):
  129. raise NotImplementedError()
  130. def __repr__(self):
  131. return ( "%s %s" % ( self.__class__.__name__, self.ca_type ) )
  132. class SSHAuthority(Authority):
  133. ca_type = 'ssh'
  134. request_allowed = [ UserSSHRequest, HostSSHRequest, ]
  135. key_algorithm = 'ed25519'
  136. user_validity = '+52w'
  137. host_validity = '+52w'
  138. def generate(self):
  139. if os.path.exists(self.path):
  140. raise ValueError("A CA with the same id and type already exists")
  141. subprocess.check_output(['ssh-keygen',
  142. '-f', self.path,
  143. '-t', self.key_algorithm,
  144. '-C', self.name])
  145. with open(self.path + '.serial', 'w') as stream:
  146. stream.write(str(0))
  147. def sign(self, request):
  148. assert type(request) in self.request_allowed
  149. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  150. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  151. with open(self.path + '.serial', 'r') as stream:
  152. next_serial = int(stream.read())
  153. with open(self.path + '.serial', 'w') as stream:
  154. stream.write(str(next_serial + 1))
  155. with open(pub_key_path, 'w') as stream:
  156. stream.write(request.key_data)
  157. ca_private_key = self.path
  158. if type(request) == UserSSHRequest:
  159. login_names = [ request.user_name, ]
  160. if request.root_requested:
  161. login_names.append('root')
  162. subprocess.check_output(['ssh-keygen',
  163. '-s', ca_private_key,
  164. '-I', 'user_%s' % request.user_name,
  165. '-n', ','.join(login_names),
  166. '-V', self.user_validity,
  167. '-z', str(next_serial),
  168. pub_key_path])
  169. elif type(request) == HostSSHRequest:
  170. subprocess.check_output(['ssh-keygen',
  171. '-s', ca_private_key,
  172. '-I', 'host_%s' % request.host_name.replace('.', '_'),
  173. '-h',
  174. '-n', request.host_name,
  175. '-V', self.host_validity,
  176. '-z', str(next_serial),
  177. pub_key_path])
  178. return cert_path
  179. class SSLAuthority(Authority):
  180. ca_type = 'ssl'
  181. request_allowed = [ HostSSLRequest, ]
  182. ca_key_algorithm = 'des3'
  183. key_length = '4096'
  184. key_algorithm = 'sha256'
  185. ca_validity = '365'
  186. cert_validity = '365'
  187. def generate(self):
  188. if os.path.exists(self.path):
  189. raise ValueError("A CA with the same id and type already exists")
  190. subprocess.check_output(['openssl',
  191. 'genrsa',
  192. '-%s'%self.ca_key_algorithm,
  193. '-out', '%s'%(self.path),
  194. self.key_length])
  195. subprocess.check_output(['openssl',
  196. 'req',
  197. '-new',
  198. '-x509',
  199. '-days', self.ca_validity,
  200. '-key', self.path,
  201. # '-extensions', 'v3_ca'
  202. '-out', "%s.pub"%self.path,
  203. # '-config', "%s.conf"%self.path
  204. ])
  205. with open(self.path + '.serial', 'w') as stream:
  206. stream.write(str(0))
  207. def sign(self, request):
  208. OUTPUT_PATH
  209. assert type(request) in [HostSSLRequest]
  210. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  211. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  212. with open(self.path + '.serial', 'r') as stream:
  213. next_serial = int(stream.read())
  214. with open(self.path + '.serial', 'w') as stream:
  215. stream.write(str(next_serial + 1))
  216. with open(pub_key_path, 'w') as stream:
  217. stream.write(request.key_data)
  218. ca_private_key = self.path
  219. # openssl x509 -req -days 360 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt # print()
  220. subprocess.check_output(['openssl',
  221. 'x509',
  222. '-req',
  223. '-days', self.ca_validity,
  224. '-in', pub_key_path,
  225. '-CA', "%s.pub"%self.path,
  226. '-CAkey', self.path,
  227. '-CAcreateserial',
  228. '-out', cert_path,
  229. '-%s'%self.key_algorithm])
  230. return cert_path