From 72881e9cd664815cea53f9d9bf7a03152bb79369 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Thu, 30 Nov 2023 09:03:17 +0200 Subject: [PATCH 01/16] added more unit tests for pseudo-random functions --- tests/test_pseudo_random.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_pseudo_random.py b/tests/test_pseudo_random.py index 23b452c..e3d6890 100644 --- a/tests/test_pseudo_random.py +++ b/tests/test_pseudo_random.py @@ -1,6 +1,6 @@ import unittest -from random import seed, randbytes -from kyber.utils.pseudo_random import prf, G, xof +from random import seed, randbytes, randint +from kyber.utils.pseudo_random import prf, G, xof, kdf, H class TestPseudoRandom(unittest.TestCase): def setUp(self): @@ -24,6 +24,22 @@ def test_prf_returns_different_results_with_different_arguments(self): self.assertNotEqual(result1, result2) + def test_kdf_sample_output(self): + payload = bytes.fromhex("9d79b1a37f31801cd11a6706fb40d6bd57526846903bb13ede562439e9c1b823a96089bca71f3d1a6d2d3cadb3669cbd50e165e434249d8b829f411669842a979911036cf3e822086ecaa0075a69fc178ba8f83718aa8f3bd1f65e8144e61d9ab30fcb06a6c1ad8f2906e732b10f4db789d35ea68c088ab3f648818b") + result = kdf(payload, 93) + expected_result = bytes.fromhex("8d85b491f075655ec2620be0ed8a061fe481c989ca609987ad1aeec1ddbc66c9affbacb27d7c163f4c709de77e470607d63315089e0d69c93351f650417e612f7b6a63885f0a1e91836d15bbb23d76b84e85da0c54090d493202abc85f") + self.assertEqual(result, expected_result) + + def test_kdf_random_inputs(self): + outputs = set() + for _ in range(100): + output_length = randint(1, 2000) + output = kdf(randbytes(randint(1, 2000)), output_length) + self.assertEqual(len(output), output_length) + self.assertFalse(output in outputs) + outputs.add(output) + + def test_g_result_length(self): # G should return exactly 64 bytes result = G(randbytes(9)) @@ -54,3 +70,17 @@ def test_xof_returns_different_bytes_with_different_arguments(self): output1 = [next(generator1) for _ in range(1000)] output2 = [next(generator2) for _ in range(1000)] self.assertNotEqual(output1, output2) + + + def test_h_sample_output(self): + payload = bytes.fromhex("9d79b1a37f31801cd11a6706fb40d6bd57526846903bb13ede562439e9c1b823a96089bca71f3d1a6d2d3cadb3669cbd50e165e434249d8b") + expected_output = bytes.fromhex("377d53fb7115593aa9e317b2aa2251d9edcad8388986152638bab0d4af1e4443") + self.assertEqual(H(payload), expected_output) + + def test_h_random_inputs(self): + outputs = set() + for _ in range(100): + output = H(randbytes(randint(1, 2000))) + self.assertEqual(len(output), 32) + self.assertFalse(output in outputs) + outputs.add(output) From 15e54d1aef498a1ff5b643ccca186a8c9fb01e59 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Thu, 30 Nov 2023 12:48:24 +0200 Subject: [PATCH 02/16] added ccakem performance test --- perf_tests/__main__.py | 2 ++ perf_tests/test_ccakem.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 perf_tests/test_ccakem.py diff --git a/perf_tests/__main__.py b/perf_tests/__main__.py index a6b752e..ee37d37 100644 --- a/perf_tests/__main__.py +++ b/perf_tests/__main__.py @@ -1,3 +1,5 @@ from perf_tests.test_encryption import runner as encryption_test_runner +from perf_tests.test_ccakem import runner as ccakem_test_runner encryption_test_runner() +ccakem_test_runner() diff --git a/perf_tests/test_ccakem.py b/perf_tests/test_ccakem.py new file mode 100644 index 0000000..8b2ff43 --- /dev/null +++ b/perf_tests/test_ccakem.py @@ -0,0 +1,39 @@ +from time import time +from kyber.ccakem import ccakem_generate_keys, ccakem_encrypt, ccakem_decrypt + +def run_test() -> tuple[float, float, float]: + t0 = time() + + private_key, public_key = ccakem_generate_keys() + + t1 = time() + + ciphertext, shared_secret1 = ccakem_encrypt(public_key) + + t2 = time() + + shared_secret2 = ccakem_decrypt(ciphertext, private_key) + + t3 = time() + + assert shared_secret1 == shared_secret2 + + return (t1-t0, t2-t1, t3-t2) + +def runner(): + print("Starting ccakem performance test (about 2 mins)") + + test_iters = 250 + averages = [0, 0, 0] + + for _ in range(test_iters): + durations = run_test() + averages = [averages[i]+durations[i] for i in range(3)] + + print("Results (averages):") + print(f"Keypair generation: {averages[0]/test_iters:.5f} sec") + print(f"Encryption: {averages[1]/test_iters:.5f} sec") + print(f"Decryption: {averages[2]/test_iters:.5f} sec") + +if __name__ == "__main__": + runner() From 6a08c5bad69a9eab85aa6e2e62a5d56c760e2743 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 09:37:03 +0200 Subject: [PATCH 03/16] fixed a typo --- docs/requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.md b/docs/requirements.md index 143c27e..bdde75d 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -2,7 +2,7 @@ ### Problem to be solved -When two people want to communicate securely with each other using insecure network, they need to use encryption. One way of doing this would be to use asymmetric encryption, in which both would encrypt the payload with recipient's public key. However, asymmetric encryption is relatively slow when the payload gets longer. Therefore it is common to use asymmetric encryption to securely share a key that will then be used in faster asymmetric encryption. This is called key encapsulation mechanism, KEM [6]. +When two people want to communicate securely with each other using insecure network, they need to use encryption. One way of doing this would be to use asymmetric encryption, in which both would encrypt the payload with recipient's public key. However, asymmetric encryption is relatively slow when the payload gets longer. Therefore it is common to use asymmetric encryption to securely share a key that will then be used in faster symmetric encryption. This is called key encapsulation mechanism, KEM [6]. Traditionally [Diffie–Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) method has been used for this [6], but because of Shor's algorithm it is thought not to be safe against powerful quantum computers. For this demand of quantum-resistant asymmetric encryption suitable for key-sharing was developed a new algorithm called CRYSTALS-Kyber. In 2022 National Institute of Standards and Technology (NIST) selected Kyber among three other algorithms to be the first post-quantum standards [4]. In August 2023 NIST released a candidate for the final standard [5] and this project is based on that. From 9c3d9009184dff52bad8d970ed71b3cd85936d86 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 10:28:06 +0200 Subject: [PATCH 04/16] added implementation docs --- docs/implementation.md | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/implementation.md diff --git a/docs/implementation.md b/docs/implementation.md new file mode 100644 index 0000000..ed9c0cd --- /dev/null +++ b/docs/implementation.md @@ -0,0 +1,47 @@ +# Implementation + +## Project structure + +This project is divided into packages and modules that handle a small portion of the functionality. Packages have strict hierarchy that allows importing modules only from the same or lower-ranked package. Below is a diagram showing the import structure between packages. + +```mermaid +classDiagram + encryption <|-- utilities + encryption <|-- constants + encryption <|-- entities + entities <|-- constants + utilities <|-- entities + utilities <|-- constants + ccakem <|-- utilities + ccakem <|-- entities + ccakem <|-- constants + ccakem <|-- encryption + + class encryption { + encrypt + decrypt + keygen + } + class utilities { + byte_conversion + compression + encoding + cbd + parse + pseudo_random + round + } + class entities { + polring + } + class constants + class ccakem +``` + +Here are short descriptions of what is the purpose of each package: + +* **utilities** package provides some basic functionalities, such as conversions, rounding and encoding, for higher-ranked modules to use. +* **entities** contains data structures. +* **constants** module has some fixed numerical values defined in the Kyber specification. +* **encryption** has capabilities for Kyber asymmetric encryption. +* **ccakem** has functions that utilize encryption and make Kyber a key-encapsulation mechanism. From af92490cb6503b50937b22176a5dd251323f7107 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 10:51:00 +0200 Subject: [PATCH 05/16] added link to implementation docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee8758a..dde247c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Implementation of [CRYSTALS-Kyber](https://pq-crystals.org/kyber/index.shtml) en ## Documentation * [Requirements specification](docs/requirements.md) -* [Implementation]() +* [Implementation](docs/implementation.md) * [Testing](docs/tests.md) * [Usage guide](docs/usage.md) From ed55d1384d242c848c2ca941f8bf3ed2b34fa537 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 11:14:31 +0200 Subject: [PATCH 06/16] added usage sequence diagram --- docs/usage.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index a827f18..8e8a74a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -10,7 +10,18 @@ poetry install ## Usage -Currently `kyber` provides three main functions that can be used directly from Python code. A sample usage is included in `main.py`. +Kyber has three main functions: key generation, encrypt and decrypt. Below is a diagram showing the workflow of key exchange. + +```mermaid +sequenceDiagram +Note over Alice: private_key, public_key = generate_keys() +Alice->>Bob: public_key +Note over Bob: ciphertext, shared_secret = encrypt(public_key) +Bob->>Alice: ciphertext +Note over Alice: shared_secret = decrypt(private_key, ciphertext) +``` + +`kyber` package can be used directly from Python code. A sample usage is included in `main.py`. Kyber can also be used via command-line interface that can be accessed with `poetry run python cli.py`. It has four subcommands: `keygen`, `pubkey`, `encrypt` and `decrypt`. Run any subcommand with `-h` flag to get help. Below is a usage example: From 922490d65312af16b3144971a904e5381d8f37f9 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 12:00:29 +0200 Subject: [PATCH 07/16] made cli usage example clearer --- docs/usage.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 8e8a74a..277590a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -25,24 +25,25 @@ Note over Alice: shared_secret = decrypt(private_key, ciphertext) Kyber can also be used via command-line interface that can be accessed with `poetry run python cli.py`. It has four subcommands: `keygen`, `pubkey`, `encrypt` and `decrypt`. Run any subcommand with `-h` flag to get help. Below is a usage example: +First, Alice generates a private key and extracts its public key to a separate file. + ``` -# Alice poetry run python cli.py keygen private.txt poetry run python cli.py pubkey --output public.txt private.txt +``` -# Alice sends her public.txt file to Bob +After Bob has received Alice's public key, Bob can generate a random shared secret and encrypt it to ciphertext. -# Bob +``` poetry run python cli.py encrypt --key alice_public.txt --secret secret.txt --cipher cipher.txt +``` -# Bob sends his cipher.txt file to Alice +When Alice receives Bob's ciphertext, Alice can decrypt it to obtain the same shared secret as Bob has. -# Alice +``` poetry run python cli.py decrypt --key private.txt --output secret.txt bob_cipher.txt ``` -In the first line Alice generates herself a private key. On the second line she generates a public key matching the freshly-generated private key, after which she sends this public key to Bob. On the third line Bob encrypts a random shared secret with Alice's public key, after which he sends the ciphertext to Alice. On the last line Alice decrypts the ciphertext with her private key. At the end, both Alice and Bob have a file called `secret.txt` that contain the same shared secret. - ### Tests Unit tests can be run with From 07f553fa5f67ee2b7ff16b00c7ae631cfc79ae51 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 12:37:36 +0200 Subject: [PATCH 08/16] added performance test that runs Kyber integrated with AES --- perf_tests/__main__.py | 2 ++ perf_tests/test_aes_integration.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 perf_tests/test_aes_integration.py diff --git a/perf_tests/__main__.py b/perf_tests/__main__.py index ee37d37..a326371 100644 --- a/perf_tests/__main__.py +++ b/perf_tests/__main__.py @@ -1,5 +1,7 @@ from perf_tests.test_encryption import runner as encryption_test_runner from perf_tests.test_ccakem import runner as ccakem_test_runner +from perf_tests.test_aes_integration import runner as aes_integration_test_runner encryption_test_runner() ccakem_test_runner() +aes_integration_test_runner() diff --git a/perf_tests/test_aes_integration.py b/perf_tests/test_aes_integration.py new file mode 100644 index 0000000..955f74b --- /dev/null +++ b/perf_tests/test_aes_integration.py @@ -0,0 +1,52 @@ +from random import seed, randbytes +from time import time +from Crypto.Cipher import AES +from kyber.ccakem import ccakem_generate_keys, ccakem_encrypt, ccakem_decrypt + +def run(payload: bytes) -> tuple[float, float, float]: + """:returns Durations of handshake and actual payload transfer in seconds as a tuple.""" + + t0 = time() + + # Alice + private_key, public_key = ccakem_generate_keys() + + # send public_key Alice->Bob + + # Bob + ss_ciphertext, shared_secret1 = ccakem_encrypt(public_key) + + # send ss_ciphertext Bob->Alice + + # Alice + shared_secret2 = ccakem_decrypt(ss_ciphertext, private_key) + + t1 = time() + + # Alice + aes_cipher = AES.new(shared_secret2, AES.MODE_GCM) + payload_nonce = aes_cipher.nonce + payload_ciphertext, payload_tag = aes_cipher.encrypt_and_digest(payload) + + # send payload_ciphertext, payload_tag and payload_nonce Alice->Bob + + # Bob + aes_cipher = AES.new(shared_secret1, AES.MODE_GCM, nonce=payload_nonce) + decrypted_payload = aes_cipher.decrypt_and_verify(payload_ciphertext, payload_tag) + + assert payload == decrypted_payload + + return (t1-t0, time()-t1) + +def runner(): + seed(42) + payload = randbytes(100_000_000) # 100 megabytes + print("Starting AES integration performance test (about 3 seconds)") + durations = run(payload) + print("Results:") + print(f"Handshake: {durations[0]:.2f} sec") + print(f"Payload transfer: {durations[1]:.2f} sec") + print(f"Total: {sum(durations):.2f} sec") + +if __name__ == "__main__": + runner() From 1e1deb31b15f6fd93452183387d58e113e6b18d2 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 12:48:17 +0200 Subject: [PATCH 09/16] added AES usage to sequence diagram --- docs/usage.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 277590a..5baa7fa 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -19,10 +19,24 @@ Alice->>Bob: public_key Note over Bob: ciphertext, shared_secret = encrypt(public_key) Bob->>Alice: ciphertext Note over Alice: shared_secret = decrypt(private_key, ciphertext) + +rect rgb(252, 132, 113) +loop Transfer the actual payload* + Note over Alice: payload_cipher = AESEncrypt(payload, key=shared_secret) + Alice->>Bob: payload_cipher + Note over Bob: payload = AESDecrypt(payload_cipher, key=shared_secret) +end +end ``` +*) Section called **transfer the actual payload** is out of Kyber's scope. It is here just to illustrate how the shared secret generated by Kyber can be used with symmetric encryption algorithm (such as AES) to securely transfer the payload. To see a working example of how `kyber` is used with AES, take a look at `perf_tests/test_aes_integration.py`. + +#### In Python + `kyber` package can be used directly from Python code. A sample usage is included in `main.py`. +#### CLI + Kyber can also be used via command-line interface that can be accessed with `poetry run python cli.py`. It has four subcommands: `keygen`, `pubkey`, `encrypt` and `decrypt`. Run any subcommand with `-h` flag to get help. Below is a usage example: First, Alice generates a private key and extracts its public key to a separate file. From e510203beef443c36c18594b89f5ab649bd8fedf Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 15:48:07 +0200 Subject: [PATCH 10/16] added utility module descriptions --- docs/implementation.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/implementation.md b/docs/implementation.md index ed9c0cd..2d79aa6 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -45,3 +45,35 @@ Here are short descriptions of what is the purpose of each package: * **constants** module has some fixed numerical values defined in the Kyber specification. * **encryption** has capabilities for Kyber asymmetric encryption. * **ccakem** has functions that utilize encryption and make Kyber a key-encapsulation mechanism. + +## Utilities + +Here are more in-depth descriptions of modules in utilities package. + +### Byte conversion + +Byte conversion module has some basic functions that integers and bit arrays to bytes, and vice versa. + +### Compression + +During the en/decryption the coefficients of polynomial ring are in modulo `q`, that is, between 0 and `q-1` (inclusive). When we transfer these polynomial rings, we can, however, reduce the size by downscaling these coefficients. That is done with compress and decompress functions. + +### Encoding + +Usually the polynomial rings are handled as `PolynomialRing` instances. We can not, however, send these instances over the Internet, so we have to encode them into byte arrays. At the other end, we need to recover polynomial ring from the byte array. This is done with encode and decode functions. + +### Parse + +Parse is a pseudo-random function that generates a specific type of polynomial ring instance from a random byte stream. This is different from decoding in that the input is byte stream instead of byte array, i.e., the number of bytes required to form the result is not known beforehand. + +### CBD + +This module provides a single function that deterministically produces a polynomial ring from byte array. Behavior of this function is quite similar to `parse` but in this case the length of the input byte array is fixed. + +### Pseudo-random + +This module includes multiple functions that use SHA-3 hash algorithm family to deterministically produce pseudo-random byte arrays from given seeds. + +### Round + +This small module provides a function that rounds floats in a "traditional" way, that is, ties rounded up instead of away from zero. For example, Python's built-in round function outputs `round(-3.5)=-4` whereas `normal_round(-3.5)=-3`. From ec5fbcec8c9581144c44bf95239db27336b60877 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 15:59:34 +0200 Subject: [PATCH 11/16] added case test for CBD --- tests/test_cbd.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_cbd.py b/tests/test_cbd.py index 7006af3..cc48bb0 100644 --- a/tests/test_cbd.py +++ b/tests/test_cbd.py @@ -1,11 +1,18 @@ import unittest -from random import seed, randbytes +from random import seed,randbytes +from base64 import b64decode from kyber.utils.cbd import cbd class TestCBD(unittest.TestCase): def setUp(self): seed(42) + def test_cbd_returns_expected_polynomial_ring(self): + argument = b64decode("nXmxo38xgBzRGmcG+0DWvVdSaEaQO7E+3lYkOenBuCOpYIm8px89Gm0tPK2zZpy9UOFl5DQknYuCn0EWaYQql5kRA2zz6CIIbsqgB1pp/BeLqPg3GKqPO9H2XoFE5h2asw/LBqbBrY8pBucysQ9Nt4nTXqaMCIqz9kiBi6SmZWs=") + pol = cbd(argument, 2) + expected_result = [0,1,3328,0,3328,3328,0,3327,3328,0,3327,3328,1,0,3328,2,1,3328,3328,0,0,3328,0,0,0,3328,1,0,1,0,3328,1,0,3328,0,3328,0,1,1,0,0,0,3327,3328,3328,3328,3327,1,1,1,0,0,3328,1,3327,0,1,0,2,3328,3328,1,3328,3327,0,0,0,0,1,0,3328,2,0,3328,3328,0,3327,1,3328,0,0,1,3328,1,3327,2,0,1,3328,3327,0,0,0,2,3328,1,0,0,1,3328,0,0,1,1,3327,1,3328,1,0,1,1,3328,1,3328,0,0,1,3328,3328,0,0,0,1,1,3328,0,0,3328,0,0,3328,3328,0,3327,0,2,0,3327,1,1,3328,3328,0,1,0,1,2,0,0,0,0,3328,0,0,0,0,0,2,3328,3328,1,3328,0,1,0,1,3327,3328,3328,1,0,0,1,0,3327,3328,1,3328,0,0,0,1,1,3328,1,1,1,0,3328,1,0,0,3328,3327,0,0,2,3328,0,0,0,0,2,3328,0,1,1,0,3328,0,0,0,1,3328,3327,3328,3328,3328,0,0,1,1,3328,3328,1,0,1,3327,0,1,0,0,1,2,0,1,1,0,3328,3327,0,0,1,1,1,3328,1,3328,0,1,0,0,0,0,0,3328] + self.assertEqual(pol.coefs, expected_result) + def test_cbd_returns_same_result_with_same_arguments(self): eta = 5 argument = randbytes(320) # 64*eta From 2182f223eedaa60c950e3cfd0afc78e00df9372f Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 16:08:07 +0200 Subject: [PATCH 12/16] changed compression unit test to random based to cover more possible cases --- tests/test_compression.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_compression.py b/tests/test_compression.py index 6c8d33f..9990b65 100644 --- a/tests/test_compression.py +++ b/tests/test_compression.py @@ -5,11 +5,13 @@ class TestCompression(unittest.TestCase): def test_compression_symmetry(self): + # test that polynomial ring does not change when it is compressed and decompressed seed(42) - polynomial = PolynomialRing([randint(0, 2047) for _ in range(256)]) - decompressed = decompress(polynomial, 11) - compressed = compress([decompressed], 11)[0] - self.assertListEqual(list(polynomial.coefs), list(compressed.coefs)) + for _ in range(100): + polynomial = PolynomialRing([randint(0, 2047) for _ in range(256)]) + decompressed = decompress(polynomial, 11) + compressed = compress([decompressed], 11)[0] + self.assertListEqual(list(polynomial.coefs), list(compressed.coefs)) def test_compression(self): polynomial = PolynomialRing([416, 2913, 0, 1248]) @@ -40,4 +42,4 @@ def test_decompression_raises_with_too_large_coefficient(self): # coefficient should not be greather than 2**d-1 = 7 polynomial = PolynomialRing([2, 8, 3]) with self.assertRaises(ValueError): - decompress(polynomial, 3) + decompress(polynomial, 3)# From 67c990e9da8e1d9e66de495eba6bb9a8d4814cd7 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 18:40:24 +0200 Subject: [PATCH 13/16] added case test for pseudo-random functions --- tests/test_pseudo_random.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_pseudo_random.py b/tests/test_pseudo_random.py index e3d6890..ba7b6e9 100644 --- a/tests/test_pseudo_random.py +++ b/tests/test_pseudo_random.py @@ -6,6 +6,11 @@ class TestPseudoRandom(unittest.TestCase): def setUp(self): seed(42) + def test_prf_case_output(self): + # test case calculated with another tool found online + expected_output = "e19287039a95cebe6fb2994fef8b2773988c73781729ad420bbaa9d0988d58e19fe82b49e6c68ea589a92c81463c8cf2513710ac80beba2eeac4c5008742d60d3b0ee8e7fd5b404fa126791f8cd1a6c6822fcb14523db0e591eded03b259182ab4330f0712e776aab5da9168e39cae9743b418cf27f5c817329f5a0f2b093624" + self.assertEqual(prf(bytes.fromhex("c0ffee"), bytes.fromhex("c0de")), bytes.fromhex(expected_output)) + def test_prf_result_length(self): # prf should return exactly 128 bytes result = prf(randbytes(17), randbytes(2)) @@ -39,6 +44,10 @@ def test_kdf_random_inputs(self): self.assertFalse(output in outputs) outputs.add(output) + def test_kdf_case_output(self): + expected_result = bytes.fromhex("f17e4c7f0ac30b7cb7b26791e2d3151d59bbcbb4c83357b2bb07f0043bf2d96a00b37b79b5f3153aba5d") + self.assertEqual(kdf(bytes.fromhex("c0ffee"), 42), expected_result) + def test_g_result_length(self): # G should return exactly 64 bytes @@ -56,6 +65,10 @@ def test_g_returns_different_results_with_different_arguments(self): result1, result2 = G(argument1), G(argument2) self.assertNotEqual(result1, result2) + def test_g_case_output(self): + expected_result = "16e16e7ebc463111d9f0dddccfa9d33152706bcdf41bf2c89d928f4b4463e4576237b569a71fdb7f168f4932c43430624a80e135eb8b6a1aefc7dbd1210d97e5" + self.assertEqual(G(bytes.fromhex("c0ffee")), bytes.fromhex(expected_result)) + def test_xof_returns_same_bytes_with_same_arguments(self): arguments = (randbytes(32), randbytes(1), randbytes(1)) @@ -71,6 +84,14 @@ def test_xof_returns_different_bytes_with_different_arguments(self): output2 = [next(generator2) for _ in range(1000)] self.assertNotEqual(output1, output2) + def test_xof_case_output(self): + generator = xof(bytes.fromhex("c0ffee"), bytes.fromhex("c0de"), bytes.fromhex("0ff1ce")) + output = bytearray() + for _ in range(30): + output += next(generator) + expected_output = bytes.fromhex("74349c255f965bc5c45c0a570a35cdbd9d32899abcf62fd8f68a35df8446") + self.assertEqual(output, expected_output) + def test_h_sample_output(self): payload = bytes.fromhex("9d79b1a37f31801cd11a6706fb40d6bd57526846903bb13ede562439e9c1b823a96089bca71f3d1a6d2d3cadb3669cbd50e165e434249d8b") @@ -84,3 +105,7 @@ def test_h_random_inputs(self): self.assertEqual(len(output), 32) self.assertFalse(output in outputs) outputs.add(output) + + def test_h_case_output(self): + expected_output = bytes.fromhex("47b147ef11cd3eb6c9bf988470a83a21e2a5c39782dd25d1483b4a8d129ad291") + self.assertEqual(H(bytes.fromhex("c0ffee")), expected_output) From ceda91c136f2588b6b239cb44fd7942f9e42b68c Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 18:49:44 +0200 Subject: [PATCH 14/16] added missing tests for polynomial ring --- tests/test_polring.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_polring.py b/tests/test_polring.py index 06052d2..64e04fb 100644 --- a/tests/test_polring.py +++ b/tests/test_polring.py @@ -1,6 +1,7 @@ import unittest from random import seed, randint from numpy.polynomial.polynomial import Polynomial +import numpy as np from kyber.entities.polring import PolynomialRing from kyber.constants import q, n @@ -11,6 +12,16 @@ def test_initialization(self): pol = PolynomialRing([71, -5, 0, 1, 3329, 3330, 3328, 15]) self.assertListEqual(pol.coefs, [71, 3324, 0, 1, 0, 1, 3328, 15]) + def test_initialization_with_no_limit_checks(self): + pol = PolynomialRing([15, -3, 4012, -10514, 3329], check_limits=False) + self.assertEqual(pol.coefs, [15, -3, 4012, -10514, 3329]) + + def test_init_with_numpy_array(self): + pol = PolynomialRing(np.array([-5, 123, 3330])) + self.assertEqual(pol.coefs, [3324, 123, 1]) + self.assertEqual(type(pol.coefs), list) + self.assertEqual(type(pol.coefs[0]), int) + def test_init_with_random_inputs(self): # test with 1000 samples that randomily initialized polring matches expected seed(42) @@ -49,3 +60,7 @@ def test_multiplication_with_random_inputs(self): self.assertEqual(len(result.coefs), len(expected.coef)) for i in range(len(result.coefs)): self.assertEqual(result.coefs[i], expected.coef[i] % q) + + def test_representation(self): + pol = PolynomialRing([71, 3324, 0, 1, 0, 1, 3328, 15]) + self.assertEqual(pol.__repr__(), "PolRing(71, 3324, 0, 1, 0, 1, 3328, 15)") From 52198e26ebfff888088c0fb9c2894f70a469c052 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 19:44:33 +0200 Subject: [PATCH 15/16] improved test docs --- docs/tests.md | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index da178cc..4d7cbdd 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -1,27 +1,27 @@ # Tests -All functions and methods are tested individually with sample inputs. In addition to testing with correct inputs, code is tested to fail with invalid inputs. Outputs are tested to be in correct type and to match all criteria. +All functions and methods are tested individually with both sample and random inputs. In addition to testing with correct inputs, code is tested to fail with invalid inputs. Outputs are tested to be in correct type and to match all criteria. In addition to unit tests, Kyber is also tested with some intergration tests. That is, a function and its inverse function are called consecutively and the output is checked to equal the original input. Current test report: ``` -tests/test_byte_conversion.py ........ [ 15%] -tests/test_cbd.py .... [ 22%] -tests/test_ccakem.py .... [ 30%] -tests/test_compression.py ...... [ 41%] -tests/test_decrypt.py ... [ 47%] -tests/test_encoding.py ........ [ 62%] -tests/test_encrypt.py ... [ 67%] -tests/test_encryption.py . [ 69%] -tests/test_key_generation.py .. [ 73%] -tests/test_modulo.py .. [ 77%] -tests/test_parse.py ... [ 83%] -tests/test_pseudo_random.py ........ [ 98%] -tests/test_round.py . [100%] - -=============== 53 passed in 0.33s ================ +tests/test_byte_conversion.py ........ [ 11%] +tests/test_cbd.py ..... [ 18%] +tests/test_ccakem.py ..... [ 26%] +tests/test_compression.py ...... [ 34%] +tests/test_decrypt.py ... [ 39%] +tests/test_encoding.py ....... [ 49%] +tests/test_encrypt.py ... [ 53%] +tests/test_encryption.py . [ 55%] +tests/test_key_generation.py .. [ 57%] +tests/test_parse.py ... [ 62%] +tests/test_polring.py ........ [ 73%] +tests/test_pseudo_random.py ................. [ 98%] +tests/test_round.py . [100%] + +=================== 69 passed in 5.08s =================== ``` Tests can be run with `poetry run invoke test`. @@ -37,25 +37,29 @@ Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------ kyber/ccakem.py 34 0 2 0 100% kyber/constants.py 14 0 0 0 100% -kyber/encryption/decrypt.py 26 0 6 0 100% -kyber/encryption/encrypt.py 57 0 12 0 100% -kyber/encryption/keygen.py 35 0 8 0 100% +kyber/encryption/decrypt.py 24 0 6 0 100% +kyber/encryption/encrypt.py 51 0 12 0 100% +kyber/encryption/keygen.py 31 0 8 0 100% +kyber/entities/polring.py 50 0 26 0 100% kyber/utils/byte_conversion.py 23 0 12 0 100% kyber/utils/cbd.py 16 0 6 0 100% kyber/utils/compression.py 23 0 8 0 100% -kyber/utils/encoding.py 36 0 22 0 100% -kyber/utils/modulo.py 17 0 8 0 100% +kyber/utils/encoding.py 34 0 20 0 100% kyber/utils/parse.py 22 0 6 0 100% kyber/utils/pseudo_random.py 23 0 0 0 100% kyber/utils/round.py 5 0 2 0 100% ------------------------------------------------------------------ -TOTAL 331 0 92 0 100% +TOTAL 350 0 108 0 100% ``` For more detailed report, run `poetry run invoke coverage-report` and then open `htmlcov/index.html`. ## Performance tests -The asymmetric encryption part of Kyber (CPAPKE in the specification document) only works with fixed-lengthed input, but we can split larger payload into 32-byte chunks and encrypt them separately. Ciphertexts can be concatenated and the whole process can be reversed during decryption. Using this method, encryption is tested with about 10 kibibytes of random payload. +The asymmetric encryption part of Kyber (called CPAPKE in the specification document) only works with fixed-lengthed input, but we can split larger payload into 32-byte chunks and encrypt them separately. Ciphertexts can be concatenated and the whole process can be reversed during decryption. Using this method, encryption is tested in `perf_tests/test_encryption.py` with about 10 kibibytes of random payload. + +End-to-end process of Kyber handshake is iterated a couple of hundred times in `perf_tests/test_ccakem.py`. + +In addition, there is an illustrative and comparable test in `perf_tests/test_aes_integration.py` that integrates Kyber with AES encryption to point out how much faster it is to use key encapsulation mechanism instead of asymmetric encryption. Performance tests can be run with `poetry run invoke performance`. From fbb6f79448b636f441ad3424072b5c4704d49a08 Mon Sep 17 00:00:00 2001 From: Pyry Lahtinen Date: Sat, 2 Dec 2023 19:49:56 +0200 Subject: [PATCH 16/16] added week 5 report --- docs/week-5.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/week-5.md diff --git a/docs/week-5.md b/docs/week-5.md new file mode 100644 index 0000000..935348b --- /dev/null +++ b/docs/week-5.md @@ -0,0 +1,7 @@ +# Week 5 + +_27.11. – 3.12.2023_ + +This week I improved and added unit tests to lift branch coverage back to 100% (from 99% last week). I also added new performance tests. Documentation went through a process where I improved explanation and added the last missing one, implementation document. Finally, I had some time to response some of the issues that the first peer review pointed out. + +Total working time: 5 hours