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