test_cli.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. """
  2. Unit tests for CLI entry points.
  3. """
  4. from __future__ import print_function
  5. import unittest
  6. import sys
  7. import functools
  8. from contextlib import contextmanager
  9. import os
  10. from io import StringIO, BytesIO
  11. import rsa
  12. import rsa.cli
  13. import rsa.util
  14. from rsa._compat import PY2
  15. def make_buffer():
  16. if PY2:
  17. return BytesIO()
  18. buf = StringIO()
  19. buf.buffer = BytesIO()
  20. return buf
  21. def get_bytes_out(out):
  22. if PY2:
  23. # Python 2.x writes 'str' to stdout
  24. return out.getvalue()
  25. # Python 3.x writes 'bytes' to stdout.buffer
  26. return out.buffer.getvalue()
  27. @contextmanager
  28. def captured_output():
  29. """Captures output to stdout and stderr"""
  30. new_out, new_err = make_buffer(), make_buffer()
  31. old_out, old_err = sys.stdout, sys.stderr
  32. try:
  33. sys.stdout, sys.stderr = new_out, new_err
  34. yield new_out, new_err
  35. finally:
  36. sys.stdout, sys.stderr = old_out, old_err
  37. @contextmanager
  38. def cli_args(*new_argv):
  39. """Updates sys.argv[1:] for a single test."""
  40. old_args = sys.argv[:]
  41. sys.argv[1:] = [str(arg) for arg in new_argv]
  42. try:
  43. yield
  44. finally:
  45. sys.argv[1:] = old_args
  46. def remove_if_exists(fname):
  47. """Removes a file if it exists."""
  48. if os.path.exists(fname):
  49. os.unlink(fname)
  50. def cleanup_files(*filenames):
  51. """Makes sure the files don't exist when the test runs, and deletes them afterward."""
  52. def remove():
  53. for fname in filenames:
  54. remove_if_exists(fname)
  55. def decorator(func):
  56. @functools.wraps(func)
  57. def wrapper(*args, **kwargs):
  58. remove()
  59. try:
  60. return func(*args, **kwargs)
  61. finally:
  62. remove()
  63. return wrapper
  64. return decorator
  65. class AbstractCliTest(unittest.TestCase):
  66. @classmethod
  67. def setUpClass(cls):
  68. # Ensure there is a key to use
  69. cls.pub_key, cls.priv_key = rsa.newkeys(512)
  70. cls.pub_fname = '%s.pub' % cls.__name__
  71. cls.priv_fname = '%s.key' % cls.__name__
  72. with open(cls.pub_fname, 'wb') as outfile:
  73. outfile.write(cls.pub_key.save_pkcs1())
  74. with open(cls.priv_fname, 'wb') as outfile:
  75. outfile.write(cls.priv_key.save_pkcs1())
  76. @classmethod
  77. def tearDownClass(cls):
  78. if hasattr(cls, 'pub_fname'):
  79. remove_if_exists(cls.pub_fname)
  80. if hasattr(cls, 'priv_fname'):
  81. remove_if_exists(cls.priv_fname)
  82. def assertExits(self, status_code, func, *args, **kwargs):
  83. try:
  84. func(*args, **kwargs)
  85. except SystemExit as ex:
  86. if status_code == ex.code:
  87. return
  88. self.fail('SystemExit() raised by %r, but exited with code %r, expected %r' % (
  89. func, ex.code, status_code))
  90. else:
  91. self.fail('SystemExit() not raised by %r' % func)
  92. class KeygenTest(AbstractCliTest):
  93. def test_keygen_no_args(self):
  94. with cli_args():
  95. self.assertExits(1, rsa.cli.keygen)
  96. def test_keygen_priv_stdout(self):
  97. with captured_output() as (out, err):
  98. with cli_args(128):
  99. rsa.cli.keygen()
  100. lines = get_bytes_out(out).splitlines()
  101. self.assertEqual(b'-----BEGIN RSA PRIVATE KEY-----', lines[0])
  102. self.assertEqual(b'-----END RSA PRIVATE KEY-----', lines[-1])
  103. # The key size should be shown on stderr
  104. self.assertTrue('128-bit key' in err.getvalue())
  105. @cleanup_files('test_cli_privkey_out.pem')
  106. def test_keygen_priv_out_pem(self):
  107. with captured_output() as (out, err):
  108. with cli_args('--out=test_cli_privkey_out.pem', '--form=PEM', 128):
  109. rsa.cli.keygen()
  110. # The key size should be shown on stderr
  111. self.assertTrue('128-bit key' in err.getvalue())
  112. # The output file should be shown on stderr
  113. self.assertTrue('test_cli_privkey_out.pem' in err.getvalue())
  114. # If we can load the file as PEM, it's good enough.
  115. with open('test_cli_privkey_out.pem', 'rb') as pemfile:
  116. rsa.PrivateKey.load_pkcs1(pemfile.read())
  117. @cleanup_files('test_cli_privkey_out.der')
  118. def test_keygen_priv_out_der(self):
  119. with captured_output() as (out, err):
  120. with cli_args('--out=test_cli_privkey_out.der', '--form=DER', 128):
  121. rsa.cli.keygen()
  122. # The key size should be shown on stderr
  123. self.assertTrue('128-bit key' in err.getvalue())
  124. # The output file should be shown on stderr
  125. self.assertTrue('test_cli_privkey_out.der' in err.getvalue())
  126. # If we can load the file as der, it's good enough.
  127. with open('test_cli_privkey_out.der', 'rb') as derfile:
  128. rsa.PrivateKey.load_pkcs1(derfile.read(), format='DER')
  129. @cleanup_files('test_cli_privkey_out.pem', 'test_cli_pubkey_out.pem')
  130. def test_keygen_pub_out_pem(self):
  131. with captured_output() as (out, err):
  132. with cli_args('--out=test_cli_privkey_out.pem',
  133. '--pubout=test_cli_pubkey_out.pem',
  134. '--form=PEM', 256):
  135. rsa.cli.keygen()
  136. # The key size should be shown on stderr
  137. self.assertTrue('256-bit key' in err.getvalue())
  138. # The output files should be shown on stderr
  139. self.assertTrue('test_cli_privkey_out.pem' in err.getvalue())
  140. self.assertTrue('test_cli_pubkey_out.pem' in err.getvalue())
  141. # If we can load the file as PEM, it's good enough.
  142. with open('test_cli_pubkey_out.pem', 'rb') as pemfile:
  143. rsa.PublicKey.load_pkcs1(pemfile.read())
  144. class EncryptDecryptTest(AbstractCliTest):
  145. def test_empty_decrypt(self):
  146. with cli_args():
  147. self.assertExits(1, rsa.cli.decrypt)
  148. def test_empty_encrypt(self):
  149. with cli_args():
  150. self.assertExits(1, rsa.cli.encrypt)
  151. @cleanup_files('encrypted.txt', 'cleartext.txt')
  152. def test_encrypt_decrypt(self):
  153. with open('cleartext.txt', 'wb') as outfile:
  154. outfile.write(b'Hello cleartext RSA users!')
  155. with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname):
  156. with captured_output():
  157. rsa.cli.encrypt()
  158. with cli_args('-i', 'encrypted.txt', self.priv_fname):
  159. with captured_output() as (out, err):
  160. rsa.cli.decrypt()
  161. # We should have the original cleartext on stdout now.
  162. output = get_bytes_out(out)
  163. self.assertEqual(b'Hello cleartext RSA users!', output)
  164. @cleanup_files('encrypted.txt', 'cleartext.txt')
  165. def test_encrypt_decrypt_unhappy(self):
  166. with open('cleartext.txt', 'wb') as outfile:
  167. outfile.write(b'Hello cleartext RSA users!')
  168. with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname):
  169. with captured_output():
  170. rsa.cli.encrypt()
  171. # Change a few bytes in the encrypted stream.
  172. with open('encrypted.txt', 'r+b') as encfile:
  173. encfile.seek(40)
  174. encfile.write(b'hahaha')
  175. with cli_args('-i', 'encrypted.txt', self.priv_fname):
  176. with captured_output() as (out, err):
  177. self.assertRaises(rsa.DecryptionError, rsa.cli.decrypt)
  178. class SignVerifyTest(AbstractCliTest):
  179. def test_empty_verify(self):
  180. with cli_args():
  181. self.assertExits(1, rsa.cli.verify)
  182. def test_empty_sign(self):
  183. with cli_args():
  184. self.assertExits(1, rsa.cli.sign)
  185. @cleanup_files('signature.txt', 'cleartext.txt')
  186. def test_sign_verify(self):
  187. with open('cleartext.txt', 'wb') as outfile:
  188. outfile.write(b'Hello RSA users!')
  189. with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'):
  190. with captured_output():
  191. rsa.cli.sign()
  192. with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'):
  193. with captured_output() as (out, err):
  194. rsa.cli.verify()
  195. self.assertFalse(b'Verification OK' in get_bytes_out(out))
  196. @cleanup_files('signature.txt', 'cleartext.txt')
  197. def test_sign_verify_unhappy(self):
  198. with open('cleartext.txt', 'wb') as outfile:
  199. outfile.write(b'Hello RSA users!')
  200. with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'):
  201. with captured_output():
  202. rsa.cli.sign()
  203. # Change a few bytes in the cleartext file.
  204. with open('cleartext.txt', 'r+b') as encfile:
  205. encfile.seek(6)
  206. encfile.write(b'DSA')
  207. with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'):
  208. with captured_output() as (out, err):
  209. self.assertExits('Verification failed.', rsa.cli.verify)
  210. class PrivatePublicTest(AbstractCliTest):
  211. """Test CLI command to convert a private to a public key."""
  212. @cleanup_files('test_private_to_public.pem')
  213. def test_private_to_public(self):
  214. with cli_args('-i', self.priv_fname, '-o', 'test_private_to_public.pem'):
  215. with captured_output():
  216. rsa.util.private_to_public()
  217. # Check that the key is indeed valid.
  218. with open('test_private_to_public.pem', 'rb') as pemfile:
  219. key = rsa.PublicKey.load_pkcs1(pemfile.read())
  220. self.assertEqual(self.priv_key.n, key.n)
  221. self.assertEqual(self.priv_key.e, key.e)