b10-certgen_test.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. # Copyright (C) 2012 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. # Note: the main code is in C++, but what we are mostly testing is
  16. # options and behaviour (output/file creation, etc), which is easier
  17. # to test in python.
  18. import unittest
  19. import os
  20. from subprocess import call
  21. import subprocess
  22. import ssl
  23. import stat
  24. def run(command):
  25. """
  26. Small helper function that returns a tuple of (rcode, stdout, stderr) after
  27. running the given command (an array of command and arguments, as passed on
  28. to subprocess).
  29. """
  30. subp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  31. (stdout, stderr) = subp.communicate()
  32. return (subp.returncode, stdout, stderr)
  33. class FileDeleterContext:
  34. """
  35. Simple Context Manager that deletes a given set of files when the context
  36. is left.
  37. """
  38. def __init__(self, files):
  39. self.files = files
  40. def __enter__(self):
  41. pass
  42. def __exit__(self, type, value, traceback):
  43. for f in self.files:
  44. if os.path.exists(f):
  45. os.unlink(f)
  46. class FilePermissionContext:
  47. """
  48. Simple Context Manager that temporarily modifies file permissions for
  49. a given file
  50. """
  51. def __init__(self, f, unset_flags = [], set_flags = []):
  52. """
  53. Initialize file permission context.
  54. See the stat module for possible flags to set or unset.
  55. The flags are changed when the context is entered (i.e.
  56. you can create the context first without any change)
  57. The flags are changed back when the context is left.
  58. Parameters:
  59. f: string, file to change permissions for
  60. unset_flags: list of flags to unset
  61. set_flags: list of flags to set
  62. """
  63. self.file = f
  64. self.orig_mode = os.stat(f).st_mode
  65. new_mode = self.orig_mode
  66. for flag in unset_flags:
  67. new_mode = new_mode & ~flag
  68. for flag in set_flags:
  69. new_mode = new_mode | flag
  70. self.new_mode = new_mode
  71. def __enter__(self):
  72. os.chmod(self.file, self.new_mode)
  73. def __exit__(self, type, value, traceback):
  74. os.chmod(self.file, self.orig_mode)
  75. def read_file_data(filename):
  76. """
  77. Simple text file reader that returns its contents as an array
  78. """
  79. with open(filename) as f:
  80. return f.readlines()
  81. class TestCertGenTool(unittest.TestCase):
  82. TOOL = '../b10-certgen'
  83. def run_check(self, expected_returncode, expected_stdout, expected_stderr, command):
  84. """
  85. Runs the given command, and checks return code, and outputs (if provided).
  86. Arguments:
  87. expected_returncode, return code of the command
  88. expected_stdout, (multiline) string that is checked agains stdout.
  89. May be None, in which case the check is skipped.
  90. expected_stderr, (multiline) string that is checked agains stderr.
  91. May be None, in which case the check is skipped.
  92. """
  93. (returncode, stdout, stderr) = run(command)
  94. self.assertEqual(expected_returncode, returncode, " ".join(command))
  95. if expected_stdout is not None:
  96. self.assertEqual(expected_stdout, stdout.decode())
  97. if expected_stderr is not None:
  98. self.assertEqual(expected_stderr, stderr.decode())
  99. def validate_certificate(self, expected_result, certfile):
  100. """
  101. Validate a certificate, using the quiet option of the tool; it runs
  102. the check option (-c) for the given base name of the certificate (-f
  103. <certfile>), and compares the return code to the given
  104. expected_result value
  105. """
  106. self.run_check(expected_result, '', '',
  107. [self.TOOL, '-q', '-c', certfile])
  108. # Same with long options
  109. self.run_check(expected_result, '', '',
  110. [self.TOOL, '--quiet', '--certfile', certfile])
  111. def test_basic_creation(self):
  112. """
  113. Tests whether basic creation with no arguments (except output
  114. file name) successfully creates a key and certificate
  115. """
  116. keyfile = 'test-keyfile.pem'
  117. certfile = 'test-certfile.pem'
  118. command = [ self.TOOL, '-q', '-w', '-c', certfile, '-k', keyfile ]
  119. self.creation_helper(command, certfile, keyfile)
  120. # Do same with long options
  121. command = [ self.TOOL, '--quiet', '--write', '--certfile=' + certfile, '--keyfile=' + keyfile ]
  122. self.creation_helper(command, certfile, keyfile)
  123. def creation_helper(self, command, certfile, keyfile):
  124. """
  125. Helper method for test_basic_creation.
  126. Performs the actual checks
  127. """
  128. with FileDeleterContext([keyfile, certfile]):
  129. self.assertFalse(os.path.exists(keyfile))
  130. self.assertFalse(os.path.exists(certfile))
  131. self.run_check(0, '', '', command)
  132. self.assertTrue(os.path.exists(keyfile))
  133. self.assertTrue(os.path.exists(certfile))
  134. # Validate the certificate that was just created
  135. self.validate_certificate(0, certfile)
  136. # When run with the same options, it should *not* create it again,
  137. # as the current certificate should still be valid
  138. certdata = read_file_data(certfile)
  139. keydata = read_file_data(keyfile)
  140. self.run_check(0, '', '', command)
  141. self.assertEqual(certdata, read_file_data(certfile))
  142. self.assertEqual(keydata, read_file_data(keyfile))
  143. # but if we add -f, it should force a new creation
  144. command.append('-f')
  145. self.run_check(0, '', '', command)
  146. self.assertNotEqual(certdata, read_file_data(certfile))
  147. self.assertNotEqual(keydata, read_file_data(keyfile))
  148. def test_check_bad_certificates(self):
  149. """
  150. Tests a few pre-created certificates with the -c option
  151. """
  152. path = os.environ['CMDCTL_SRC_PATH'] + '/tests/testdata/'
  153. self.validate_certificate(10, path + 'expired-certfile.pem')
  154. self.validate_certificate(100, path + 'mangled-certfile.pem')
  155. self.validate_certificate(17, path + 'noca-certfile.pem')
  156. def test_bad_options(self):
  157. """
  158. Tests some combinations of commands that should fail.
  159. """
  160. # specify -c but not -k
  161. self.run_check(101,
  162. 'Error: keyfile and certfile must both be specified '
  163. 'if one of them is when calling b10-certgen in write '
  164. 'mode.\n',
  165. '', [self.TOOL, '-w', '-c', 'foo'])
  166. self.run_check(101,
  167. 'Error: keyfile and certfile must both be specified '
  168. 'if one of them is when calling b10-certgen in write '
  169. 'mode.\n',
  170. '', [self.TOOL, '-w', '-k', 'foo'])
  171. self.run_check(101,
  172. 'Error: keyfile is not used when not in write mode\n',
  173. '', [self.TOOL, '-k', 'foo'])
  174. # Extraneous argument
  175. self.run_check(101, None, None, [self.TOOL, 'foo'])
  176. # No such file
  177. self.run_check(105, None, None, [self.TOOL, '-c', 'foo'])
  178. @unittest.skipIf(os.getuid() == 0,
  179. 'test cannot be run as root user')
  180. def test_permissions(self):
  181. """
  182. Test some combinations of correct and bad permissions.
  183. """
  184. keyfile = 'mod-keyfile.pem'
  185. certfile = 'mod-certfile.pem'
  186. command = [ self.TOOL, '-q', '-w', '-c', certfile, '-k', keyfile ]
  187. # Delete them at the end
  188. with FileDeleterContext([keyfile, certfile]):
  189. # Create the two files first
  190. self.run_check(0, '', '', command)
  191. self.validate_certificate(0, certfile)
  192. # Make the key file unwritable
  193. with FilePermissionContext(keyfile, unset_flags = [stat.S_IWUSR]):
  194. self.run_check(106, '', '', command)
  195. # Should have no effect on validation
  196. self.validate_certificate(0, certfile)
  197. # Make the cert file unwritable
  198. with FilePermissionContext(certfile, unset_flags = [stat.S_IWUSR]):
  199. self.run_check(106, '', '', command)
  200. # Should have no effect on validation
  201. self.validate_certificate(0, certfile)
  202. # Make the key file unreadable (this should not matter)
  203. with FilePermissionContext(keyfile, unset_flags = [stat.S_IRUSR]):
  204. self.run_check(0, '', '', command)
  205. # unreadable key file should also not have any effect on
  206. # validation
  207. self.validate_certificate(0, certfile)
  208. # Make the cert file unreadable (this should matter)
  209. with FilePermissionContext(certfile, unset_flags = [stat.S_IRUSR]):
  210. self.run_check(106, '', '', command)
  211. # Unreadable cert file should also fail validation
  212. self.validate_certificate(106, certfile)
  213. # Not directly a permission problem, but trying to check or create
  214. # in a nonexistent directory returns different error codes
  215. self.validate_certificate(105, 'fakedir/cert')
  216. self.run_check(103, '', '', [ self.TOOL, '-q', '-w', '-c',
  217. 'fakedir/cert', '-k', 'fakedir/key' ])
  218. if __name__== '__main__':
  219. unittest.main()