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.

297 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.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['request'].get('userName', None) or request_data.get('hostName', None)
  43. root_requested = request_data['request'].get('rootRequested', False)
  44. key_data = request_data['request'].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['request'].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. def __init__(self, ca_id, name, ca_dir):
  119. self.ca_id = ca_id
  120. self.name = name
  121. self.ca_dir = ca_dir
  122. @property
  123. def path(self):
  124. return os.path.join(self.ca_dir, self.ca_id)
  125. def generate(self):
  126. raise NotImplementedError()
  127. def sign(self, request):
  128. raise NotImplementedError()
  129. def __repr__(self):
  130. return ( "%s %s" % ( self.__class__.__name__, self.ca_type ) )
  131. class SSHAuthority(Authority):
  132. ca_type = 'ssh'
  133. key_algorithm = 'ed25519'
  134. user_validity = '+52w'
  135. host_validity = '+52w'
  136. def generate(self):
  137. if os.path.exists(self.path):
  138. raise ValueError("A CA with the same id and type already exists")
  139. subprocess.check_output(['ssh-keygen',
  140. '-f', self.path,
  141. '-t', self.key_algorithm,
  142. '-C', self.name])
  143. with open(self.path + '.serial', 'w') as stream:
  144. stream.write(str(0))
  145. def sign(self, request):
  146. assert type(request) in [UserSSHRequest, HostSSHRequest]
  147. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  148. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  149. with open(self.path + '.serial', 'r') as stream:
  150. next_serial = int(stream.read())
  151. with open(self.path + '.serial', 'w') as stream:
  152. stream.write(str(next_serial + 1))
  153. with open(pub_key_path, 'w') as stream:
  154. stream.write(request.key_data)
  155. ca_private_key = self.path
  156. if type(request) == UserSSHRequest:
  157. login_names = [request.user_name]
  158. if request.root_requested:
  159. login_names.append('root')
  160. subprocess.check_output(['ssh-keygen',
  161. '-s', ca_private_key,
  162. '-I', 'user_%s' % request.user_name,
  163. '-n', ','.join(login_names),
  164. '-V', self.user_validity,
  165. '-z', str(next_serial),
  166. pub_key_path])
  167. elif type(request) == HostSSHRequest:
  168. subprocess.check_output(['ssh-keygen',
  169. '-s', ca_private_key,
  170. '-I', 'host_%s' % request.host_name.replace('.', '_'),
  171. '-h',
  172. '-n', request.host_name,
  173. '-V', self.host_validity,
  174. '-z', str(next_serial),
  175. pub_key_path])
  176. return cert_path
  177. class SSLAuthority(Authority):
  178. ca_type = 'ssl'
  179. ca_key_algorithm = 'des3'
  180. key_length = '4096'
  181. key_algorithm = 'sha256'
  182. ca_validity = '365'
  183. cert_validity = '365'
  184. def generate(self):
  185. if os.path.exists(self.path):
  186. raise ValueError("A CA with the same id and type already exists")
  187. subprocess.check_output(['openssl',
  188. 'genrsa',
  189. '-%s'%self.ca_key_algorithm,
  190. '-out', '%s'%(self.path),
  191. self.key_length])
  192. subprocess.check_output(['openssl',
  193. 'req',
  194. '-new',
  195. '-x509',
  196. '-days', self.ca_validity,
  197. '-key', self.path,
  198. # '-extensions', 'v3_ca'
  199. '-out', "%s.pub"%self.path,
  200. # '-config', "%s.conf"%self.path
  201. ])
  202. with open(self.path + '.serial', 'w') as stream:
  203. stream.write(str(0))
  204. def sign(self, request):
  205. OUTPUT_PATH
  206. assert type(request) in [HostSSLRequest]
  207. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  208. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  209. with open(self.path + '.serial', 'r') as stream:
  210. next_serial = int(stream.read())
  211. with open(self.path + '.serial', 'w') as stream:
  212. stream.write(str(next_serial + 1))
  213. with open(pub_key_path, 'w') as stream:
  214. stream.write(request.key_data)
  215. ca_private_key = self.path
  216. # openssl x509 -req -days 360 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt # print()
  217. subprocess.check_output(['openssl',
  218. 'x509',
  219. '-req',
  220. '-days', self.ca_validity,
  221. '-in', pub_key_path,
  222. '-CA', "%s.pub"%self.path,
  223. '-CAkey', self.path,
  224. '-CAcreateserial',
  225. '-out', cert_path,
  226. '-%s'%self.key_algorithm])
  227. return cert_path