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.

273 lines
7.4 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. # read the json from a TextIO
  40. # but let as_request handle
  41. # the conversion to a Python
  42. # object
  43. request_data = json.load(
  44. stream,
  45. )
  46. def __exit__(self, exc_type, exc_value, traceback):
  47. if exc_type is not None:
  48. print(exc_type, exc_value)
  49. print(traceback)
  50. class UserSSHRequest(SignRequest, object):
  51. def __init__(self, req_id, user_name, root_requested, key_data):
  52. super(UserSSHRequest, self).__init__(req_id)
  53. self.user_name = user_name
  54. self.root_requested = root_requested
  55. self.key_data = key_data
  56. @property
  57. def name(self):
  58. return "User: %s [R:%d]" % (self.user_name, int(self.root_requested))
  59. @property
  60. def fields(self):
  61. return [
  62. ("User name", self.user_name),
  63. ("Root access requested", 'yes' if self.root_requested else 'no')
  64. ]
  65. def __str__(self):
  66. return ("%s %s" % (self.req_id, self.user_name))
  67. class HostSSLRequest(SignRequest, object):
  68. def __init__(self, req_id, host_name, key_data):
  69. super(HostSSLRequest, self).__init__(req_id)
  70. self.host_name = host_name
  71. self.key_data = key_data
  72. @property
  73. def name(self):
  74. return "Hostname: %s" % self.host_name
  75. @property
  76. def fields(self):
  77. return [
  78. ("Hostname", self.host_name)
  79. ]
  80. def __str__(self):
  81. return ("%s %s" % (self.req_id, self.host_name))
  82. class HostSSHRequest(SignRequest, object):
  83. def __init__(self, req_id, host_name, key_data):
  84. super(HostSSHRequest, self).__init__(req_id)
  85. self.host_name = host_name
  86. self.key_data = key_data
  87. @property
  88. def name(self):
  89. return "Hostname: %s" % self.host_name
  90. @property
  91. def fields(self):
  92. return [
  93. ("Hostname", self.host_name)
  94. ]
  95. def __str__(self):
  96. return ("%s %s" % (self.req_id, self.host_name))
  97. class Authority(object):
  98. ca_type = None
  99. def __init__(self, ca_id, name, ca_dir):
  100. self.ca_id = ca_id
  101. self.name = name
  102. self.ca_dir = ca_dir
  103. @property
  104. def path(self):
  105. return os.path.join(self.ca_dir, self.ca_id)
  106. def generate(self):
  107. raise NotImplementedError()
  108. def sign(self, request):
  109. raise NotImplementedError()
  110. def __repr__(self):
  111. return ( "%s %s" % ( self.__class__.__name__, self.ca_type ) )
  112. class SSHAuthority(Authority):
  113. ca_type = 'ssh'
  114. key_algorithm = 'ed25519'
  115. user_validity = '+52w'
  116. host_validity = '+52w'
  117. def generate(self):
  118. if os.path.exists(self.path):
  119. raise ValueError("A CA with the same id and type already exists")
  120. subprocess.check_output(['ssh-keygen',
  121. '-f', self.path,
  122. '-t', self.key_algorithm,
  123. '-C', self.name])
  124. with open(self.path + '.serial', 'w') as stream:
  125. stream.write(str(0))
  126. def sign(self, request):
  127. assert type(request) in [UserSSHRequest, HostSSHRequest]
  128. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  129. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  130. with open(self.path + '.serial', 'r') as stream:
  131. next_serial = int(stream.read())
  132. with open(self.path + '.serial', 'w') as stream:
  133. stream.write(str(next_serial + 1))
  134. with open(pub_key_path, 'w') as stream:
  135. stream.write(request.key_data)
  136. ca_private_key = self.path
  137. if type(request) == UserSSHRequest:
  138. login_names = [request.user_name]
  139. if request.root_requested:
  140. login_names.append('root')
  141. subprocess.check_output(['ssh-keygen',
  142. '-s', ca_private_key,
  143. '-I', 'user_%s' % request.user_name,
  144. '-n', ','.join(login_names),
  145. '-V', self.user_validity,
  146. '-z', str(next_serial),
  147. pub_key_path])
  148. elif type(request) == HostSSHRequest:
  149. subprocess.check_output(['ssh-keygen',
  150. '-s', ca_private_key,
  151. '-I', 'host_%s' % request.host_name.replace('.', '_'),
  152. '-h',
  153. '-n', request.host_name,
  154. '-V', self.host_validity,
  155. '-z', str(next_serial),
  156. pub_key_path])
  157. return cert_path
  158. class SSLAuthority(Authority):
  159. ca_type = 'ssl'
  160. ca_key_algorithm = 'des3'
  161. key_length = '4096'
  162. key_algorithm = 'sha256'
  163. ca_validity = '365'
  164. cert_validity = '365'
  165. def generate(self):
  166. if os.path.exists(self.path):
  167. raise ValueError("A CA with the same id and type already exists")
  168. subprocess.check_output(['openssl',
  169. 'genrsa',
  170. '-%s'%self.ca_key_algorithm,
  171. '-out', '%s'%(self.path),
  172. self.key_length])
  173. subprocess.check_output(['openssl',
  174. 'req',
  175. '-new',
  176. '-x509',
  177. '-days', self.ca_validity,
  178. '-key', self.path,
  179. # '-extensions', 'v3_ca'
  180. '-out', "%s.pub"%self.path,
  181. # '-config', "%s.conf"%self.path
  182. ])
  183. with open(self.path + '.serial', 'w') as stream:
  184. stream.write(str(0))
  185. def sign(self, request):
  186. OUTPUT_PATH
  187. assert type(request) in [HostSSLRequest]
  188. pub_key_path = os.path.join(OUTPUT_PATH, request.req_id + '.pub')
  189. cert_path = os.path.join(OUTPUT_PATH, request.req_id + '-cert.pub')
  190. with open(self.path + '.serial', 'r') as stream:
  191. next_serial = int(stream.read())
  192. with open(self.path + '.serial', 'w') as stream:
  193. stream.write(str(next_serial + 1))
  194. with open(pub_key_path, 'w') as stream:
  195. stream.write(request.key_data)
  196. ca_private_key = self.path
  197. # openssl x509 -req -days 360 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt # print()
  198. subprocess.check_output(['openssl',
  199. 'x509',
  200. '-req',
  201. '-days', self.ca_validity,
  202. '-in', pub_key_path,
  203. '-CA', "%s.pub"%self.path,
  204. '-CAkey', self.path,
  205. '-CAcreateserial',
  206. '-out', cert_path,
  207. '-%s'%self.key_algorithm])
  208. return cert_path