blob: 511d25d8cef415f9c54e7b667ca9fe54d429d122 [file] [log] [blame]
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +01001"""
2Unit tests for CLI entry points.
3"""
4
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +01005from __future__ import print_function
6
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +01007import unittest
8import sys
9import functools
10from contextlib import contextmanager
11
12import os
13from io import StringIO, BytesIO
14
15import rsa
16import rsa.cli
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010017from rsa._compat import b
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010018
19if sys.version_info[0] < 3:
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010020 def make_buffer():
21 return BytesIO()
22
23
24 def get_bytes_out(out):
25 # Python 2.x writes 'str' to stdout:
26 return out.getvalue()
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010027else:
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010028 def make_buffer():
29 buf = StringIO()
30 buf.buffer = BytesIO()
31 return buf
32
33
34 def get_bytes_out(out):
35 # Python 3.x writes 'bytes' to stdout.buffer:
36 return out.buffer.getvalue()
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010037
38
39@contextmanager
40def captured_output():
41 """Captures output to stdout and stderr"""
42
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010043 new_out, new_err = make_buffer(), make_buffer()
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010044 old_out, old_err = sys.stdout, sys.stderr
45 try:
46 sys.stdout, sys.stderr = new_out, new_err
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010047 yield new_out, new_err
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010048 finally:
49 sys.stdout, sys.stderr = old_out, old_err
50
51
52@contextmanager
53def cli_args(*new_argv):
54 """Updates sys.argv[1:] for a single test."""
55
56 old_args = sys.argv[:]
57 sys.argv[1:] = [str(arg) for arg in new_argv]
58
59 try:
60 yield
61 finally:
62 sys.argv[1:] = old_args
63
64
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010065def remove_if_exists(fname):
66 """Removes a file if it exists."""
67
68 if os.path.exists(fname):
69 os.unlink(fname)
70
71
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010072def cleanup_files(*filenames):
73 """Makes sure the files don't exist when the test runs, and deletes them afterward."""
74
75 def remove():
76 for fname in filenames:
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010077 remove_if_exists(fname)
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010078
79 def decorator(func):
80 @functools.wraps(func)
81 def wrapper(*args, **kwargs):
82 remove()
83 try:
84 return func(*args, **kwargs)
85 finally:
86 remove()
87
88 return wrapper
89
90 return decorator
91
92
93class AbstractCliTest(unittest.TestCase):
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010094 @classmethod
95 def setUpClass(cls):
96 # Ensure there is a key to use
97 cls.pub_key, cls.priv_key = rsa.newkeys(512)
98 cls.pub_fname = '%s.pub' % cls.__name__
99 cls.priv_fname = '%s.key' % cls.__name__
100
101 with open(cls.pub_fname, 'wb') as outfile:
102 outfile.write(cls.pub_key.save_pkcs1())
103
104 with open(cls.priv_fname, 'wb') as outfile:
105 outfile.write(cls.priv_key.save_pkcs1())
106
107 @classmethod
108 def tearDownClass(cls):
109 if hasattr(cls, 'pub_fname'):
110 remove_if_exists(cls.pub_fname)
111 if hasattr(cls, 'priv_fname'):
112 remove_if_exists(cls.priv_fname)
113
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +0100114 def assertExits(self, status_code, func, *args, **kwargs):
115 try:
116 func(*args, **kwargs)
117 except SystemExit as ex:
118 if status_code == ex.code:
119 return
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100120 self.fail('SystemExit() raised by %r, but exited with code %r, expected %r' % (
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +0100121 func, ex.code, status_code))
122 else:
123 self.fail('SystemExit() not raised by %r' % func)
124
125
126class KeygenTest(AbstractCliTest):
127 def test_keygen_no_args(self):
128 with cli_args():
129 self.assertExits(1, rsa.cli.keygen)
130
131 def test_keygen_priv_stdout(self):
132 with captured_output() as (out, err):
133 with cli_args(128):
134 rsa.cli.keygen()
135
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100136 lines = get_bytes_out(out).splitlines()
137 self.assertEqual(b('-----BEGIN RSA PRIVATE KEY-----'), lines[0])
138 self.assertEqual(b('-----END RSA PRIVATE KEY-----'), lines[-1])
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +0100139
140 # The key size should be shown on stderr
141 self.assertTrue('128-bit key' in err.getvalue())
142
143 @cleanup_files('test_cli_privkey_out.pem')
144 def test_keygen_priv_out_pem(self):
145 with captured_output() as (out, err):
146 with cli_args('--out=test_cli_privkey_out.pem', '--form=PEM', 128):
147 rsa.cli.keygen()
148
149 # The key size should be shown on stderr
150 self.assertTrue('128-bit key' in err.getvalue())
151
152 # The output file should be shown on stderr
153 self.assertTrue('test_cli_privkey_out.pem' in err.getvalue())
154
155 # If we can load the file as PEM, it's good enough.
156 with open('test_cli_privkey_out.pem', 'rb') as pemfile:
157 rsa.PrivateKey.load_pkcs1(pemfile.read())
158
159 @cleanup_files('test_cli_privkey_out.der')
160 def test_keygen_priv_out_der(self):
161 with captured_output() as (out, err):
162 with cli_args('--out=test_cli_privkey_out.der', '--form=DER', 128):
163 rsa.cli.keygen()
164
165 # The key size should be shown on stderr
166 self.assertTrue('128-bit key' in err.getvalue())
167
168 # The output file should be shown on stderr
169 self.assertTrue('test_cli_privkey_out.der' in err.getvalue())
170
171 # If we can load the file as der, it's good enough.
172 with open('test_cli_privkey_out.der', 'rb') as derfile:
173 rsa.PrivateKey.load_pkcs1(derfile.read(), format='DER')
174
175 @cleanup_files('test_cli_privkey_out.pem', 'test_cli_pubkey_out.pem')
176 def test_keygen_pub_out_pem(self):
177 with captured_output() as (out, err):
178 with cli_args('--out=test_cli_privkey_out.pem',
179 '--pubout=test_cli_pubkey_out.pem',
180 '--form=PEM', 256):
181 rsa.cli.keygen()
182
183 # The key size should be shown on stderr
184 self.assertTrue('256-bit key' in err.getvalue())
185
186 # The output files should be shown on stderr
187 self.assertTrue('test_cli_privkey_out.pem' in err.getvalue())
188 self.assertTrue('test_cli_pubkey_out.pem' in err.getvalue())
189
190 # If we can load the file as PEM, it's good enough.
191 with open('test_cli_pubkey_out.pem', 'rb') as pemfile:
192 rsa.PublicKey.load_pkcs1(pemfile.read())
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100193
194
195class EncryptDecryptTest(AbstractCliTest):
196 def test_empty_decrypt(self):
197 with cli_args():
198 self.assertExits(1, rsa.cli.decrypt)
199
200 def test_empty_encrypt(self):
201 with cli_args():
202 self.assertExits(1, rsa.cli.encrypt)
203
204 @cleanup_files('encrypted.txt', 'cleartext.txt')
205 def test_encrypt_decrypt(self):
206 with open('cleartext.txt', 'wb') as outfile:
207 outfile.write(b'Hello cleartext RSA users!')
208
209 with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname):
210 with captured_output():
211 rsa.cli.encrypt()
212
213 with cli_args('-i', 'encrypted.txt', self.priv_fname):
214 with captured_output() as (out, err):
215 rsa.cli.decrypt()
216
217 # We should have the original cleartext on stdout now.
218 output = get_bytes_out(out)
219 self.assertEqual(b('Hello cleartext RSA users!'), output)
220
221 @cleanup_files('encrypted.txt', 'cleartext.txt')
222 def test_encrypt_decrypt_unhappy(self):
223 with open('cleartext.txt', 'wb') as outfile:
224 outfile.write(b'Hello cleartext RSA users!')
225
226 with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname):
227 with captured_output():
228 rsa.cli.encrypt()
229
230 # Change a few bytes in the encrypted stream.
231 with open('encrypted.txt', 'r+b') as encfile:
232 encfile.seek(40)
233 encfile.write(b'hahaha')
234
235 with cli_args('-i', 'encrypted.txt', self.priv_fname):
236 with captured_output() as (out, err):
237 self.assertRaises(rsa.DecryptionError, rsa.cli.decrypt)
238
239
240class SignVerifyTest(AbstractCliTest):
241 def test_empty_verify(self):
242 with cli_args():
243 self.assertExits(1, rsa.cli.verify)
244
245 def test_empty_sign(self):
246 with cli_args():
247 self.assertExits(1, rsa.cli.sign)
248
249 @cleanup_files('signature.txt', 'cleartext.txt')
250 def test_sign_verify(self):
251 with open('cleartext.txt', 'wb') as outfile:
252 outfile.write(b'Hello RSA users!')
253
254 with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'):
255 with captured_output():
256 rsa.cli.sign()
257
258 with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'):
259 with captured_output() as (out, err):
260 rsa.cli.verify()
261
262 self.assertFalse(b'Verification OK' in get_bytes_out(out))
263
264 @cleanup_files('signature.txt', 'cleartext.txt')
265 def test_sign_verify_unhappy(self):
266 with open('cleartext.txt', 'wb') as outfile:
267 outfile.write(b'Hello RSA users!')
268
269 with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'):
270 with captured_output():
271 rsa.cli.sign()
272
273 # Change a few bytes in the cleartext file.
274 with open('cleartext.txt', 'r+b') as encfile:
275 encfile.seek(6)
276 encfile.write(b'DSA')
277
278 with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'):
279 with captured_output() as (out, err):
280 self.assertExits('Verification failed.', rsa.cli.verify)