blob: 1cd92c284de98cbb28f4f7fc3578a13cf00f528d [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 functools
Sybren A. Stüvel6760eb72019-08-04 15:47:11 +02008import io
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +01009import os
Sybren A. Stüvel6760eb72019-08-04 15:47:11 +020010import sys
11import typing
12import unittest
13from contextlib import contextmanager, redirect_stdout, redirect_stderr
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010014
15import rsa
16import rsa.cli
Sybren A. Stüvelff7f0c72016-03-17 16:12:55 +010017import rsa.util
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010018
Michael Manganiello81f0e952017-01-15 12:30:37 -030019
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010020@contextmanager
Sybren A. Stüvel6760eb72019-08-04 15:47:11 +020021def captured_output() -> typing.Generator:
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010022 """Captures output to stdout and stderr"""
23
Sybren A. Stüvel6760eb72019-08-04 15:47:11 +020024 # According to mypy, we're not supposed to change buf_out.buffer.
25 # However, this is just a test, and it works, hence the 'type: ignore'.
26 buf_out = io.StringIO()
27 buf_out.buffer = io.BytesIO() # type: ignore
28
29 buf_err = io.StringIO()
30 buf_err.buffer = io.BytesIO() # type: ignore
31
32 with redirect_stdout(buf_out), redirect_stderr(buf_err):
33 yield buf_out, buf_err
34
35
36def get_bytes_out(buf) -> bytes:
37 return buf.buffer.getvalue()
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010038
39
40@contextmanager
41def cli_args(*new_argv):
42 """Updates sys.argv[1:] for a single test."""
43
44 old_args = sys.argv[:]
45 sys.argv[1:] = [str(arg) for arg in new_argv]
46
47 try:
48 yield
49 finally:
50 sys.argv[1:] = old_args
51
52
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010053def remove_if_exists(fname):
54 """Removes a file if it exists."""
55
56 if os.path.exists(fname):
57 os.unlink(fname)
58
59
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010060def cleanup_files(*filenames):
61 """Makes sure the files don't exist when the test runs, and deletes them afterward."""
62
63 def remove():
64 for fname in filenames:
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010065 remove_if_exists(fname)
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +010066
67 def decorator(func):
68 @functools.wraps(func)
69 def wrapper(*args, **kwargs):
70 remove()
71 try:
72 return func(*args, **kwargs)
73 finally:
74 remove()
75
76 return wrapper
77
78 return decorator
79
80
81class AbstractCliTest(unittest.TestCase):
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +010082 @classmethod
83 def setUpClass(cls):
84 # Ensure there is a key to use
85 cls.pub_key, cls.priv_key = rsa.newkeys(512)
86 cls.pub_fname = '%s.pub' % cls.__name__
87 cls.priv_fname = '%s.key' % cls.__name__
88
89 with open(cls.pub_fname, 'wb') as outfile:
90 outfile.write(cls.pub_key.save_pkcs1())
91
92 with open(cls.priv_fname, 'wb') as outfile:
93 outfile.write(cls.priv_key.save_pkcs1())
94
95 @classmethod
96 def tearDownClass(cls):
97 if hasattr(cls, 'pub_fname'):
98 remove_if_exists(cls.pub_fname)
99 if hasattr(cls, 'priv_fname'):
100 remove_if_exists(cls.priv_fname)
101
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +0100102 def assertExits(self, status_code, func, *args, **kwargs):
103 try:
104 func(*args, **kwargs)
105 except SystemExit as ex:
106 if status_code == ex.code:
107 return
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100108 self.fail('SystemExit() raised by %r, but exited with code %r, expected %r' % (
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +0100109 func, ex.code, status_code))
110 else:
111 self.fail('SystemExit() not raised by %r' % func)
112
113
114class KeygenTest(AbstractCliTest):
115 def test_keygen_no_args(self):
116 with cli_args():
117 self.assertExits(1, rsa.cli.keygen)
118
119 def test_keygen_priv_stdout(self):
120 with captured_output() as (out, err):
121 with cli_args(128):
122 rsa.cli.keygen()
123
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100124 lines = get_bytes_out(out).splitlines()
adamantike9f577402016-05-08 15:36:57 -0300125 self.assertEqual(b'-----BEGIN RSA PRIVATE KEY-----', lines[0])
126 self.assertEqual(b'-----END RSA PRIVATE KEY-----', lines[-1])
Sybren A. Stüvel3d5c0982016-03-17 13:27:41 +0100127
128 # The key size should be shown on stderr
129 self.assertTrue('128-bit key' in err.getvalue())
130
131 @cleanup_files('test_cli_privkey_out.pem')
132 def test_keygen_priv_out_pem(self):
133 with captured_output() as (out, err):
134 with cli_args('--out=test_cli_privkey_out.pem', '--form=PEM', 128):
135 rsa.cli.keygen()
136
137 # The key size should be shown on stderr
138 self.assertTrue('128-bit key' in err.getvalue())
139
140 # The output file should be shown on stderr
141 self.assertTrue('test_cli_privkey_out.pem' in err.getvalue())
142
143 # If we can load the file as PEM, it's good enough.
144 with open('test_cli_privkey_out.pem', 'rb') as pemfile:
145 rsa.PrivateKey.load_pkcs1(pemfile.read())
146
147 @cleanup_files('test_cli_privkey_out.der')
148 def test_keygen_priv_out_der(self):
149 with captured_output() as (out, err):
150 with cli_args('--out=test_cli_privkey_out.der', '--form=DER', 128):
151 rsa.cli.keygen()
152
153 # The key size should be shown on stderr
154 self.assertTrue('128-bit key' in err.getvalue())
155
156 # The output file should be shown on stderr
157 self.assertTrue('test_cli_privkey_out.der' in err.getvalue())
158
159 # If we can load the file as der, it's good enough.
160 with open('test_cli_privkey_out.der', 'rb') as derfile:
161 rsa.PrivateKey.load_pkcs1(derfile.read(), format='DER')
162
163 @cleanup_files('test_cli_privkey_out.pem', 'test_cli_pubkey_out.pem')
164 def test_keygen_pub_out_pem(self):
165 with captured_output() as (out, err):
166 with cli_args('--out=test_cli_privkey_out.pem',
167 '--pubout=test_cli_pubkey_out.pem',
168 '--form=PEM', 256):
169 rsa.cli.keygen()
170
171 # The key size should be shown on stderr
172 self.assertTrue('256-bit key' in err.getvalue())
173
174 # The output files should be shown on stderr
175 self.assertTrue('test_cli_privkey_out.pem' in err.getvalue())
176 self.assertTrue('test_cli_pubkey_out.pem' in err.getvalue())
177
178 # If we can load the file as PEM, it's good enough.
179 with open('test_cli_pubkey_out.pem', 'rb') as pemfile:
180 rsa.PublicKey.load_pkcs1(pemfile.read())
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100181
182
183class EncryptDecryptTest(AbstractCliTest):
184 def test_empty_decrypt(self):
185 with cli_args():
186 self.assertExits(1, rsa.cli.decrypt)
187
188 def test_empty_encrypt(self):
189 with cli_args():
190 self.assertExits(1, rsa.cli.encrypt)
191
192 @cleanup_files('encrypted.txt', 'cleartext.txt')
193 def test_encrypt_decrypt(self):
194 with open('cleartext.txt', 'wb') as outfile:
195 outfile.write(b'Hello cleartext RSA users!')
196
197 with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname):
198 with captured_output():
199 rsa.cli.encrypt()
200
201 with cli_args('-i', 'encrypted.txt', self.priv_fname):
202 with captured_output() as (out, err):
203 rsa.cli.decrypt()
204
205 # We should have the original cleartext on stdout now.
206 output = get_bytes_out(out)
adamantike9f577402016-05-08 15:36:57 -0300207 self.assertEqual(b'Hello cleartext RSA users!', output)
Sybren A. Stüvelf0627be2016-03-17 15:52:23 +0100208
209 @cleanup_files('encrypted.txt', 'cleartext.txt')
210 def test_encrypt_decrypt_unhappy(self):
211 with open('cleartext.txt', 'wb') as outfile:
212 outfile.write(b'Hello cleartext RSA users!')
213
214 with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname):
215 with captured_output():
216 rsa.cli.encrypt()
217
218 # Change a few bytes in the encrypted stream.
219 with open('encrypted.txt', 'r+b') as encfile:
220 encfile.seek(40)
221 encfile.write(b'hahaha')
222
223 with cli_args('-i', 'encrypted.txt', self.priv_fname):
224 with captured_output() as (out, err):
225 self.assertRaises(rsa.DecryptionError, rsa.cli.decrypt)
226
227
228class SignVerifyTest(AbstractCliTest):
229 def test_empty_verify(self):
230 with cli_args():
231 self.assertExits(1, rsa.cli.verify)
232
233 def test_empty_sign(self):
234 with cli_args():
235 self.assertExits(1, rsa.cli.sign)
236
237 @cleanup_files('signature.txt', 'cleartext.txt')
238 def test_sign_verify(self):
239 with open('cleartext.txt', 'wb') as outfile:
240 outfile.write(b'Hello RSA users!')
241
242 with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'):
243 with captured_output():
244 rsa.cli.sign()
245
246 with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'):
247 with captured_output() as (out, err):
248 rsa.cli.verify()
249
250 self.assertFalse(b'Verification OK' in get_bytes_out(out))
251
252 @cleanup_files('signature.txt', 'cleartext.txt')
253 def test_sign_verify_unhappy(self):
254 with open('cleartext.txt', 'wb') as outfile:
255 outfile.write(b'Hello RSA users!')
256
257 with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'):
258 with captured_output():
259 rsa.cli.sign()
260
261 # Change a few bytes in the cleartext file.
262 with open('cleartext.txt', 'r+b') as encfile:
263 encfile.seek(6)
264 encfile.write(b'DSA')
265
266 with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'):
267 with captured_output() as (out, err):
268 self.assertExits('Verification failed.', rsa.cli.verify)
Sybren A. Stüvelff7f0c72016-03-17 16:12:55 +0100269
270
271class PrivatePublicTest(AbstractCliTest):
272 """Test CLI command to convert a private to a public key."""
273
274 @cleanup_files('test_private_to_public.pem')
275 def test_private_to_public(self):
276
277 with cli_args('-i', self.priv_fname, '-o', 'test_private_to_public.pem'):
278 with captured_output():
279 rsa.util.private_to_public()
280
281 # Check that the key is indeed valid.
282 with open('test_private_to_public.pem', 'rb') as pemfile:
283 key = rsa.PublicKey.load_pkcs1(pemfile.read())
284
285 self.assertEqual(self.priv_key.n, key.n)
286 self.assertEqual(self.priv_key.e, key.e)