source: trunk/src/allmydata/test/test_crypto.py

Last change on this file was 53084f7, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-27T23:49:07Z

remove more Python2 compatibility

  • Property mode set to 100644
File size: 17.8 KB
Line 
1import unittest
2
3from base64 import b64decode
4from binascii import a2b_hex, b2a_hex
5
6from twisted.python.filepath import FilePath
7
8from allmydata.crypto import (
9    aes,
10    ed25519,
11    rsa,
12)
13from allmydata.crypto.util import remove_prefix
14from allmydata.crypto.error import BadPrefixError
15
16
17
18RESOURCE_DIR = FilePath(__file__).parent().child('data')
19
20
21class TestRegression(unittest.TestCase):
22    '''
23    These tests are regression tests to ensure that the upgrade from `pycryptopp` to `cryptography`
24    doesn't break anything. They check that data encrypted with old keys can be decrypted with new
25    keys.
26    '''
27
28    AES_KEY = b'My\x9c\xc0f\xd3\x03\x9a1\x8f\xbd\x17W_\x1f2'
29    IV = b'\x96\x1c\xa0\xbcUj\x89\xc1\x85J\x1f\xeb=\x17\x04\xca'
30
31    with RESOURCE_DIR.child('pycryptopp-rsa-2048-priv.txt').open('r') as f:
32        # Created using `pycryptopp`:
33        #
34        #     from base64 import b64encode
35        #     from pycryptopp.publickey import rsa
36        #     priv = rsa.generate(2048)
37        #     priv_str = b64encode(priv.serialize())
38        #     pub_str = b64encode(priv.get_verifying_key().serialize())
39        RSA_2048_PRIV_KEY = b64decode(f.read().strip())
40        assert isinstance(RSA_2048_PRIV_KEY, bytes)
41
42    with RESOURCE_DIR.child('pycryptopp-rsa-2048-sig.txt').open('r') as f:
43        # Signature created using `RSA_2048_PRIV_KEY` via:
44        #
45        #     sig = priv.sign(b'test')
46        RSA_2048_SIG = b64decode(f.read().strip())
47
48    with RESOURCE_DIR.child('pycryptopp-rsa-2048-pub.txt').open('r') as f:
49        # The public key corresponding to `RSA_2048_PRIV_KEY`.
50        RSA_2048_PUB_KEY = b64decode(f.read().strip())
51
52    with RESOURCE_DIR.child('pycryptopp-rsa-1024-priv.txt').open('r') as f:
53        # Created using `pycryptopp`:
54        #
55        #     from base64 import b64encode
56        #     from pycryptopp.publickey import rsa
57        #     priv = rsa.generate(1024)
58        #     priv_str = b64encode(priv.serialize())
59        #     pub_str = b64encode(priv.get_verifying_key().serialize())
60        RSA_TINY_PRIV_KEY = b64decode(f.read().strip())
61        assert isinstance(RSA_TINY_PRIV_KEY, bytes)
62
63    with RESOURCE_DIR.child('pycryptopp-rsa-32768-priv.txt').open('r') as f:
64        # Created using `pycryptopp`:
65        #
66        #     from base64 import b64encode
67        #     from pycryptopp.publickey import rsa
68        #     priv = rsa.generate(32768)
69        #     priv_str = b64encode(priv.serialize())
70        #     pub_str = b64encode(priv.get_verifying_key().serialize())
71        RSA_HUGE_PRIV_KEY = b64decode(f.read().strip())
72        assert isinstance(RSA_HUGE_PRIV_KEY, bytes)
73
74    def test_old_start_up_test(self):
75        """
76        This was the old startup test run at import time in `pycryptopp.cipher.aes`.
77        """
78        enc0 = b"dc95c078a2408989ad48a21492842087530f8afbc74536b9a963b4f1c4cb738b"
79        cryptor = aes.create_decryptor(key=b"\x00" * 32)
80        ct = aes.decrypt_data(cryptor, b"\x00" * 32)
81        self.assertEqual(enc0, b2a_hex(ct))
82
83        cryptor = aes.create_decryptor(key=b"\x00" * 32)
84        ct1 = aes.decrypt_data(cryptor, b"\x00" * 15)
85        ct2 = aes.decrypt_data(cryptor, b"\x00" * 17)
86        self.assertEqual(enc0, b2a_hex(ct1+ct2))
87
88        enc0 = b"66e94bd4ef8a2c3b884cfa59ca342b2e"
89        cryptor = aes.create_decryptor(key=b"\x00" * 16)
90        ct = aes.decrypt_data(cryptor, b"\x00" * 16)
91        self.assertEqual(enc0, b2a_hex(ct))
92
93        cryptor = aes.create_decryptor(key=b"\x00" * 16)
94        ct1 = aes.decrypt_data(cryptor, b"\x00" * 8)
95        ct2 = aes.decrypt_data(cryptor, b"\x00" * 8)
96        self.assertEqual(enc0, b2a_hex(ct1+ct2))
97
98        def _test_from_Niels_AES(keysize, result):
99            def fake_ecb_using_ctr(k, p):
100                encryptor = aes.create_encryptor(key=k, iv=p)
101                return aes.encrypt_data(encryptor, b'\x00' * 16)
102
103            E = fake_ecb_using_ctr
104            b = 16
105            k = keysize
106            S = b'\x00' * (k + b)
107
108            for i in range(1000):
109                K = S[-k:]
110                P = S[-k-b:-k]
111                S += E(K, E(K, P))
112
113            self.assertEqual(S[-b:], a2b_hex(result))
114
115        _test_from_Niels_AES(16, b'bd883f01035e58f42f9d812f2dacbcd8')
116        _test_from_Niels_AES(32, b'c84b0f3a2c76dd9871900b07f09bdd3e')
117
118    def test_aes_no_iv_process_short_input(self):
119        '''
120        The old code used the following patterns with AES ciphers.
121
122            import os
123            from pycryptopp.cipher.aes import AES
124            key = = os.urandom(16)
125            ciphertext = AES(key).process(plaintext)
126
127        This test verifies that using the new AES wrapper generates the same output.
128        '''
129        plaintext = b'test'
130        expected_ciphertext = b'\x7fEK\\'
131
132        k = aes.create_decryptor(self.AES_KEY)
133        ciphertext = aes.decrypt_data(k, plaintext)
134
135        self.assertEqual(ciphertext, expected_ciphertext)
136
137    def test_aes_no_iv_process_long_input(self):
138        '''
139        The old code used the following patterns with AES ciphers.
140
141            import os
142            from pycryptopp.cipher.aes import AES
143            key = = os.urandom(16)
144            ciphertext = AES(key).process(plaintext)
145
146        This test verifies that using the new AES wrapper generates the same output.
147        '''
148        plaintext = b'hi' * 32
149        expected_ciphertext = (
150            b'cIPAY%o:\xce\xfex\x8e@^.\x90\xb1\x80a\xff\xd8^\xac\x8d\xa7/\x1d\xe6\x92\xa1\x04\x92'
151            b'\x1f\xa1|\xd2$E\xb5\xe7\x9d\xae\xd1\x1f)\xe4\xc7\x83\xb8\xd5|dHhU\xc8\x9a\xb1\x10\xed'
152            b'\xd1\xe7|\xd1')
153
154        k = aes.create_decryptor(self.AES_KEY)
155        ciphertext = aes.decrypt_data(k, plaintext)
156
157        self.assertEqual(ciphertext, expected_ciphertext)
158
159    def test_aes_with_iv_process_short_input(self):
160        '''
161        The old code used the following patterns with AES ciphers.
162
163            import os
164            from pycryptopp.cipher.aes import AES
165            key = = os.urandom(16)
166            ciphertext = AES(key).process(plaintext)
167
168        This test verifies that using the new AES wrapper generates the same output.
169        '''
170        plaintext = b'test'
171        expected_ciphertext = b'\x82\x0e\rt'
172
173        k = aes.create_decryptor(self.AES_KEY, iv=self.IV)
174        ciphertext = aes.decrypt_data(k, plaintext)
175
176        self.assertEqual(ciphertext, expected_ciphertext)
177
178    def test_aes_with_iv_process_long_input(self):
179        '''
180        The old code used the following patterns with AES ciphers.
181
182            import os
183            from pycryptopp.cipher.aes import AES
184            key = = os.urandom(16)
185            ciphertext = AES(key).process(plaintext)
186
187        This test verifies that using the new AES wrapper generates the same output.
188        '''
189        plaintext = b'hi' * 32
190        expected_ciphertext = (
191            b'\x9e\x02\x16i}WL\xbf\x83\xac\xb4K\xf7\xa0\xdf\xa3\xba!3\x15\xd3(L\xb7\xb3\x91\xbcb'
192            b'\x97a\xdc\x100?\xf5L\x9f\xd9\xeeO\x98\xda\xf5g\x93\xa7q\xe1\xb1~\xf8\x1b\xe8[\\s'
193            b'\x144$\x86\xeaC^f')
194
195        k = aes.create_decryptor(self.AES_KEY, iv=self.IV)
196        ciphertext = aes.decrypt_data(k, plaintext)
197
198        self.assertEqual(ciphertext, expected_ciphertext)
199
200    def test_decode_ed15519_keypair(self):
201        '''
202        Created using the old code:
203
204            from allmydata.util.keyutil import make_keypair, parse_privkey, parse_pubkey
205            test_data = b'test'
206            priv_str, pub_str = make_keypair()
207            priv, _ = parse_privkey(priv_str)
208            pub = parse_pubkey(pub_str)
209            sig = priv.sign(test_data)
210            pub.verify(sig, test_data)
211
212        This simply checks that keys and signatures generated using the old code are still valid
213        using the new code.
214        '''
215        priv_str = b'priv-v0-lqcj746bqa4npkb6zpyc6esd74x3bl6mbcjgqend7cvtgmcpawhq'
216        pub_str = b'pub-v0-yzpqin3of3ep363lwzxwpvgai3ps43dao46k2jds5kw5ohhpcwhq'
217        test_data = b'test'
218        sig = (b'\xde\x0e\xd6\xe2\xf5\x03]8\xfe\xa71\xad\xb4g\x03\x11\x81\x8b\x08\xffz\xf4K\xa0'
219               b'\x86 ier!\xe8\xe5#*\x9d\x8c\x0bI\x02\xd90\x0e7\xbeW\xbf\xa3\xfe\xc1\x1c\xf5+\xe9)'
220               b'\xa3\xde\xc9\xc6s\xc9\x90\xf7x\x08')
221
222        private_key, derived_public_key = ed25519.signing_keypair_from_string(priv_str)
223        public_key = ed25519.verifying_key_from_string(pub_str)
224
225        self.assertEqual(
226            ed25519.string_from_verifying_key(public_key),
227            ed25519.string_from_verifying_key(derived_public_key),
228        )
229
230        new_sig = ed25519.sign_data(private_key, test_data)
231        self.assertEqual(new_sig, sig)
232
233        ed25519.verify_signature(public_key, new_sig, test_data)
234        ed25519.verify_signature(derived_public_key, new_sig, test_data)
235        ed25519.verify_signature(public_key, sig, test_data)
236        ed25519.verify_signature(derived_public_key, sig, test_data)
237
238    def test_decode_rsa_keypair(self):
239        '''
240        This simply checks that keys and signatures generated using the old code are still valid
241        using the new code.
242        '''
243        priv_key, pub_key = rsa.create_signing_keypair_from_string(self.RSA_2048_PRIV_KEY)
244        rsa.verify_signature(pub_key, self.RSA_2048_SIG, b'test')
245
246    def test_decode_tiny_rsa_keypair(self):
247        '''
248        An unreasonably small RSA key is rejected ("unreasonably small"
249        means less that 2048 bits)
250        '''
251        with self.assertRaises(ValueError):
252            rsa.create_signing_keypair_from_string(self.RSA_TINY_PRIV_KEY)
253
254    def test_decode_huge_rsa_keypair(self):
255        '''
256        An unreasonably _large_ RSA key is rejected ("unreasonably large"
257        means 32768 or more bits)
258        '''
259        with self.assertRaises(ValueError):
260            rsa.create_signing_keypair_from_string(self.RSA_HUGE_PRIV_KEY)
261
262    def test_encrypt_data_not_bytes(self):
263        '''
264        only bytes can be encrypted
265        '''
266        key = b'\x00' * 16
267        encryptor = aes.create_encryptor(key)
268        with self.assertRaises(ValueError) as ctx:
269            aes.encrypt_data(encryptor, u"not bytes")
270        self.assertIn(
271            "must be bytes",
272            str(ctx.exception)
273        )
274
275    def test_key_incorrect_size(self):
276        '''
277        keys that aren't 16 or 32 bytes are rejected
278        '''
279        key = b'\x00' * 12
280        with self.assertRaises(ValueError) as ctx:
281            aes.create_encryptor(key)
282        self.assertIn(
283            "16 or 32 bytes long",
284            str(ctx.exception)
285        )
286
287    def test_iv_not_bytes(self):
288        '''
289        iv must be bytes
290        '''
291        key = b'\x00' * 16
292        with self.assertRaises(TypeError) as ctx:
293            aes.create_encryptor(key, iv=u"1234567890abcdef")
294        self.assertIn(
295            "must be bytes",
296            str(ctx.exception)
297        )
298
299    def test_incorrect_iv_size(self):
300        '''
301        iv must be 16 bytes
302        '''
303        key = b'\x00' * 16
304        with self.assertRaises(ValueError) as ctx:
305            aes.create_encryptor(key, iv=b'\x00' * 3)
306        self.assertIn(
307            "16 bytes long",
308            str(ctx.exception)
309        )
310
311
312class TestEd25519(unittest.TestCase):
313    """
314    Test allmydata.crypto.ed25519
315    """
316
317    def test_key_serialization(self):
318        """
319        a serialized+deserialized keypair is the same as the original
320        """
321        private_key, public_key = ed25519.create_signing_keypair()
322        private_key_str = ed25519.string_from_signing_key(private_key)
323
324        self.assertIsInstance(private_key_str, bytes)
325
326        private_key2, public_key2 = ed25519.signing_keypair_from_string(private_key_str)
327
328        # the deserialized signing keys are the same as the original
329        self.assertEqual(
330            ed25519.string_from_signing_key(private_key),
331            ed25519.string_from_signing_key(private_key2),
332        )
333        self.assertEqual(
334            ed25519.string_from_verifying_key(public_key),
335            ed25519.string_from_verifying_key(public_key2),
336        )
337
338        # ditto, but for the verifying keys
339        public_key_str = ed25519.string_from_verifying_key(public_key)
340        self.assertIsInstance(public_key_str, bytes)
341
342        public_key2 = ed25519.verifying_key_from_string(public_key_str)
343        self.assertEqual(
344            ed25519.string_from_verifying_key(public_key),
345            ed25519.string_from_verifying_key(public_key2),
346        )
347
348    def test_deserialize_private_not_bytes(self):
349        '''
350        serialized key must be bytes
351        '''
352        with self.assertRaises(ValueError) as ctx:
353            ed25519.signing_keypair_from_string(u"not bytes")
354        self.assertIn(
355            "must be bytes",
356            str(ctx.exception)
357        )
358
359    def test_deserialize_public_not_bytes(self):
360        '''
361        serialized key must be bytes
362        '''
363        with self.assertRaises(ValueError) as ctx:
364            ed25519.verifying_key_from_string(u"not bytes")
365        self.assertIn(
366            "must be bytes",
367            str(ctx.exception)
368        )
369
370    def test_signed_data_not_bytes(self):
371        '''
372        data to sign must be bytes
373        '''
374        priv, pub = ed25519.create_signing_keypair()
375        with self.assertRaises(ValueError) as ctx:
376            ed25519.sign_data(priv, u"not bytes")
377        self.assertIn(
378            "must be bytes",
379            str(ctx.exception)
380        )
381
382    def test_signature_not_bytes(self):
383        '''
384        signature must be bytes
385        '''
386        priv, pub = ed25519.create_signing_keypair()
387        with self.assertRaises(ValueError) as ctx:
388            ed25519.verify_signature(pub, u"not bytes", b"data")
389        self.assertIn(
390            "must be bytes",
391            str(ctx.exception)
392        )
393
394    def test_signature_data_not_bytes(self):
395        '''
396        signed data must be bytes
397        '''
398        priv, pub = ed25519.create_signing_keypair()
399        with self.assertRaises(ValueError) as ctx:
400            ed25519.verify_signature(pub, b"signature", u"not bytes")
401        self.assertIn(
402            "must be bytes",
403            str(ctx.exception)
404        )
405
406    def test_sign_invalid_pubkey(self):
407        '''
408        pubkey must be correct kind of object
409        '''
410        priv, pub = ed25519.create_signing_keypair()
411        with self.assertRaises(ValueError) as ctx:
412            ed25519.sign_data(object(), b"data")
413        self.assertIn(
414            "must be an Ed25519PrivateKey",
415            str(ctx.exception)
416        )
417
418    def test_verify_invalid_pubkey(self):
419        '''
420        pubkey must be correct kind of object
421        '''
422        priv, pub = ed25519.create_signing_keypair()
423        with self.assertRaises(ValueError) as ctx:
424            ed25519.verify_signature(object(), b"signature", b"data")
425        self.assertIn(
426            "must be an Ed25519PublicKey",
427            str(ctx.exception)
428        )
429
430
431class TestRsa(unittest.TestCase):
432    """
433    Tests related to allmydata.crypto.rsa module
434    """
435
436    def test_keys(self):
437        """
438        test that two instances of 'the same' key sign and verify data
439        in the same way
440        """
441        priv_key, pub_key = rsa.create_signing_keypair(2048)
442        priv_key_str = rsa.der_string_from_signing_key(priv_key)
443
444        self.assertIsInstance(priv_key_str, bytes)
445
446        priv_key2, pub_key2 = rsa.create_signing_keypair_from_string(priv_key_str)
447
448        # instead of asking "are these two keys equal", we can instead
449        # test their function: can the second key verify a signature
450        # produced by the first (and FAIL a signature with different
451        # data)
452
453        data_to_sign = b"test data"
454        sig0 = rsa.sign_data(priv_key, data_to_sign)
455        rsa.verify_signature(pub_key2, sig0, data_to_sign)
456
457        # ..and the other way
458        sig1 = rsa.sign_data(priv_key2, data_to_sign)
459        rsa.verify_signature(pub_key, sig1, data_to_sign)
460
461        # ..and a failed way
462        with self.assertRaises(rsa.BadSignature):
463            rsa.verify_signature(pub_key, sig1, data_to_sign + b"more")
464
465    def test_sign_invalid_pubkey(self):
466        '''
467        signing data using an invalid key-object fails
468        '''
469        priv, pub = rsa.create_signing_keypair(1024)
470        with self.assertRaises(ValueError) as ctx:
471            rsa.sign_data(object(), b"data")
472        self.assertIn(
473            "must be an RSAPrivateKey",
474            str(ctx.exception)
475        )
476
477    def test_verify_invalid_pubkey(self):
478        '''
479        verifying a signature using an invalid key-object fails
480        '''
481        priv, pub = rsa.create_signing_keypair(1024)
482        with self.assertRaises(ValueError) as ctx:
483            rsa.verify_signature(object(), b"signature", b"data")
484        self.assertIn(
485            "must be an RSAPublicKey",
486            str(ctx.exception)
487        )
488
489
490class TestUtil(unittest.TestCase):
491    """
492    tests related to allmydata.crypto utils
493    """
494
495    def test_remove_prefix_good(self):
496        """
497        remove a simple prefix properly
498        """
499        self.assertEqual(
500            remove_prefix(b"foobar", b"foo"),
501            b"bar"
502        )
503
504    def test_remove_prefix_bad(self):
505        """
506        attempt to remove a prefix that doesn't exist fails with exception
507        """
508        with self.assertRaises(BadPrefixError):
509            remove_prefix(b"foobar", b"bar")
510
511    def test_remove_prefix_zero(self):
512        """
513        removing a zero-length prefix does nothing
514        """
515        self.assertEqual(
516            remove_prefix(b"foobar", b""),
517            b"foobar",
518        )
519
520    def test_remove_prefix_entire_string(self):
521        """
522        removing a prefix which is the whole string is empty
523        """
524        self.assertEqual(
525            remove_prefix(b"foobar", b"foobar"),
526            b"",
527        )
528
529    def test_remove_prefix_partial(self):
530        """
531        removing a prefix with only partial match fails with exception
532        """
533        with self.assertRaises(BadPrefixError):
534            remove_prefix(b"foobar", b"fooz"),
Note: See TracBrowser for help on using the repository browser.