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