From bbf168e08b3762c31fff3363e1c979204920c6d0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 15 Mar 2021 19:44:24 -0700 Subject: [PATCH 01/25] Clean slate --- tests/__init__.py | 16 - tests/conftest.py | 139 ----- tests/functional/__init__.py | 16 - tests/functional/test_correctness.py | 156 ------ tests/functional/test_correctness_keys.py | 81 --- tests/functional/test_pre_api.py | 56 -- tests/functional/test_vectors.py | 190 ------- tests/metrics/reencryption_benchmark.py | 140 ----- tests/metrics/reencryption_firehose.py | 81 --- tests/scenario/__init__.py | 16 - tests/scenario/test_lifecycle_multidomain.py | 167 ------ tests/scenario/test_simple_api.py | 98 ---- tests/unit/test_capsule_correctness_checks.py | 104 ---- tests/unit/test_capsule_operations.py | 135 ----- tests/unit/test_capsule_serializers.py | 63 --- tests/unit/test_cfrags.py | 126 ----- tests/unit/test_config.py | 86 --- tests/unit/test_curve.py | 82 --- tests/unit/test_dem.py | 77 --- tests/unit/test_kfrags.py | 67 --- tests/unit/test_primitives/conftest.py | 167 ------ .../test_curvebn_arithmetic.py | 64 --- .../test_primitives/test_curvebn_methods.py | 39 -- .../test_curvebn_serializers.py | 62 --- .../test_primitives/test_point_arithmetic.py | 59 -- .../test_primitives/test_point_serializers.py | 165 ------ .../unit/test_serialization_property_based.py | 115 ---- tests/unit/test_signing.py | 62 --- tests/unit/test_umbral_keys.py | 223 -------- umbral/__about__.py | 17 - umbral/__init__.py | 17 - umbral/cfrags.py | 287 ---------- umbral/config.py | 77 --- umbral/curve.py | 135 ----- umbral/curvebn.py | 272 --------- umbral/dem.py | 58 -- umbral/keys.py | 472 ---------------- umbral/kfrags.py | 199 ------- umbral/openssl.py | 218 -------- umbral/params.py | 41 -- umbral/point.py | 211 ------- umbral/pre.py | 518 ------------------ umbral/random_oracles.py | 214 -------- umbral/signing.py | 151 ----- umbral/utils.py | 41 -- 45 files changed, 5780 deletions(-) delete mode 100644 tests/conftest.py delete mode 100644 tests/functional/__init__.py delete mode 100644 tests/functional/test_correctness.py delete mode 100644 tests/functional/test_correctness_keys.py delete mode 100644 tests/functional/test_pre_api.py delete mode 100644 tests/functional/test_vectors.py delete mode 100644 tests/metrics/reencryption_benchmark.py delete mode 100644 tests/metrics/reencryption_firehose.py delete mode 100644 tests/scenario/__init__.py delete mode 100644 tests/scenario/test_lifecycle_multidomain.py delete mode 100644 tests/scenario/test_simple_api.py delete mode 100644 tests/unit/test_capsule_correctness_checks.py delete mode 100644 tests/unit/test_capsule_operations.py delete mode 100644 tests/unit/test_capsule_serializers.py delete mode 100644 tests/unit/test_cfrags.py delete mode 100644 tests/unit/test_config.py delete mode 100644 tests/unit/test_curve.py delete mode 100644 tests/unit/test_dem.py delete mode 100644 tests/unit/test_kfrags.py delete mode 100644 tests/unit/test_primitives/conftest.py delete mode 100644 tests/unit/test_primitives/test_curvebn_arithmetic.py delete mode 100644 tests/unit/test_primitives/test_curvebn_methods.py delete mode 100644 tests/unit/test_primitives/test_curvebn_serializers.py delete mode 100644 tests/unit/test_primitives/test_point_arithmetic.py delete mode 100644 tests/unit/test_primitives/test_point_serializers.py delete mode 100644 tests/unit/test_serialization_property_based.py delete mode 100644 tests/unit/test_signing.py delete mode 100644 tests/unit/test_umbral_keys.py delete mode 100644 umbral/cfrags.py delete mode 100644 umbral/config.py delete mode 100644 umbral/curve.py delete mode 100644 umbral/curvebn.py delete mode 100644 umbral/dem.py delete mode 100644 umbral/keys.py delete mode 100644 umbral/kfrags.py delete mode 100644 umbral/openssl.py delete mode 100644 umbral/params.py delete mode 100644 umbral/point.py delete mode 100644 umbral/pre.py delete mode 100644 umbral/random_oracles.py delete mode 100644 umbral/signing.py delete mode 100644 umbral/utils.py diff --git a/tests/__init__.py b/tests/__init__.py index b1419391..e69de29b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,16 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index ed4fca93..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest -from collections import namedtuple - -from umbral import keys -from umbral.curve import SECP256K1, SECP384R1, SECP256R1 -from umbral.curvebn import CurveBN -from umbral.config import set_default_curve -from umbral.point import Point -from umbral.signing import Signer -from umbral import pre - -set_default_curve(SECP256K1) - -parameters = ( - # (N, M) - (1, 1), - (6, 1), - (6, 4), - (6, 6), - (50, 30) -) - -wrong_parameters = ( - # (N, M) - (-1, -1), (-1, 0), (-1, 5), - (0, -1), (0, 0), (0, 5), - (1, -1), (1, 0), (1, 5), - (5, -1), (5, 0), (5, 10) -) - -other_supported_curves = ( - SECP384R1, - SECP256R1 -) - -kfrag_signing_modes = ( - (True, True), (True, False), (False, True), (False, False) -) - - -@pytest.fixture -def alices_keys(): - delegating_priv = keys.UmbralPrivateKey.gen_key() - signing_priv = keys.UmbralPrivateKey.gen_key() - return delegating_priv, signing_priv - - -@pytest.fixture -def bobs_keys(): - priv = keys.UmbralPrivateKey.gen_key() - pub = priv.get_pubkey() - return priv, pub - - -@pytest.fixture() -def random_ec_point1(): - yield Point.gen_rand() - - -@pytest.fixture() -def random_ec_point2(): - yield Point.gen_rand() - - -@pytest.fixture() -def random_ec_curvebn1(): - yield CurveBN.gen_rand() - - -@pytest.fixture() -def random_ec_curvebn2(): - yield CurveBN.gen_rand() - - - - -@pytest.fixture(scope='session') -def message(): - message = b"dnunez [9:30 AM]" \ - b"@Tux we had this super fruitful discussion last night with @jMyles @michwill @KPrasch" \ - b"to sum up: the symmetric ciphertext is now called the 'Chimney'." \ - b"the chimney of the capsule, of course" \ - b"tux [9:32 AM]" \ - b"wat" - return message - - -@pytest.fixture -def ciphertext_and_capsule(alices_keys, message): - delegating_privkey, _signing_privkey = alices_keys - # See nucypher's issue #183 - chimney, capsule = pre.encrypt(delegating_privkey.get_pubkey(), message) - return chimney, capsule - - -@pytest.fixture -def capsule(ciphertext_and_capsule): - ciphertext, capsule = ciphertext_and_capsule - return capsule - - -@pytest.fixture -def prepared_capsule(alices_keys, bobs_keys, capsule): - delegating_privkey, signing_privkey = alices_keys - _receiving_privkey, receiving_pubkey = bobs_keys - capsule.set_correctness_keys(delegating=delegating_privkey.get_pubkey(), - receiving=receiving_pubkey, - verifying=signing_privkey.get_pubkey()) - return capsule - - -@pytest.fixture -def kfrags(alices_keys, bobs_keys): - delegating_privkey, signing_privkey = alices_keys - signer_alice = Signer(signing_privkey) - - receiving_privkey, receiving_pubkey = bobs_keys - - yield pre.generate_kfrags(delegating_privkey=delegating_privkey, - signer=signer_alice, - receiving_pubkey=receiving_pubkey, - threshold=6, N=10) diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py deleted file mode 100644 index b1419391..00000000 --- a/tests/functional/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" diff --git a/tests/functional/test_correctness.py b/tests/functional/test_correctness.py deleted file mode 100644 index eea0a063..00000000 --- a/tests/functional/test_correctness.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral import pre -from umbral.point import Point -from umbral.signing import Signer -from umbral.cfrags import CapsuleFrag - - -def test_cheating_ursula_replays_old_reencryption(alices_keys, bobs_keys, - kfrags, prepared_capsule): - delegating_privkey, signing_privkey = alices_keys - delegating_pubkey = delegating_privkey.get_pubkey() - - receiving_privkey, receiving_pubkey = bobs_keys - - capsule_alice1 = prepared_capsule - - _unused_key2, capsule_alice2 = pre._encapsulate(delegating_pubkey) - - capsule_alice2.set_correctness_keys(delegating=delegating_pubkey, - receiving=receiving_pubkey, - verifying=signing_privkey.get_pubkey()) - - cfrags = [] - for i, kfrag in enumerate(kfrags): - - # Example of potential metadata to describe the re-encryption request - metadata_i = "This is an example of metadata for re-encryption request #{}" - metadata_i = metadata_i.format(i).encode() - - if i == 0: - # Let's put the re-encryption of a different Alice ciphertext - cfrag = pre.reencrypt(kfrag, capsule_alice2, metadata=metadata_i) - else: - cfrag = pre.reencrypt(kfrag, capsule_alice1, metadata=metadata_i) - - cfrags.append(cfrag) - - #  CFrag 0 is not valid ... - assert not cfrags[0].verify_correctness(capsule_alice1) - - # ... and trying to attach it raises an error. - with pytest.raises(pre.UmbralCorrectnessError) as exception_info: - capsule_alice1.attach_cfrag(cfrags[0]) - - correctness_error = exception_info.value - assert cfrags[0] in correctness_error.offending_cfrags - assert len(correctness_error.offending_cfrags) == 1 - - # The rest of CFrags should be correct: - correct_cases = 0 - for cfrag_i in cfrags[1:]: - assert cfrag_i.verify_correctness(capsule_alice1) - capsule_alice1.attach_cfrag(cfrag_i) - correct_cases += 1 - - assert correct_cases == len(cfrags[1:]) - - -def test_cheating_ursula_sends_garbage(kfrags, prepared_capsule): - capsule_alice = prepared_capsule - - cfrags = [] - for i, kfrag in enumerate(kfrags): - # Example of potential metadata to describe the re-encryption request - metadata_i = "This is an example of metadata for re-encryption request #{}" - metadata_i = metadata_i.format(i).encode() - - cfrag = pre.reencrypt(kfrag, capsule_alice, metadata=metadata_i) - cfrags.append(cfrag) - - # Let's put random garbage in one of the cfrags - cfrags[0].point_e1 = Point.gen_rand() - cfrags[0].point_v1 = Point.gen_rand() - - #  Of course, this CFrag is not valid ... - assert not cfrags[0].verify_correctness(capsule_alice) - - # ... and trying to attach it raises an error. - with pytest.raises(pre.UmbralCorrectnessError) as exception_info: - capsule_alice.attach_cfrag(cfrags[0]) - - correctness_error = exception_info.value - assert cfrags[0] in correctness_error.offending_cfrags - assert len(correctness_error.offending_cfrags) == 1 - - # The response of cheating Ursula is in cfrags[0], - # so the rest of CFrags should be correct: - for cfrag_i in cfrags[1:]: - assert cfrag_i.verify_correctness(capsule_alice) - capsule_alice.attach_cfrag(cfrag_i) - - -def test_cfrag_with_missing_proof_cannot_be_attached(kfrags, prepared_capsule): - capsule = prepared_capsule - - cfrags = [] - for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, capsule) - cfrags.append(cfrag) - - # If the proof is lost (e.g., it is chopped off a serialized CFrag or similar), - #  then the CFrag cannot be attached. - cfrags[0].proof = None - with pytest.raises(CapsuleFrag.NoProofProvided): - capsule.attach_cfrag(cfrags[0]) - - # The remaining CFrags are fine, so they can be attached correctly - for cfrag in cfrags[1:]: - capsule.attach_cfrag(cfrag) - - -def test_kfrags_signed_without_correctness_keys(alices_keys, bobs_keys, capsule): - delegating_privkey, signing_privkey = alices_keys - delegating_pubkey = delegating_privkey.get_pubkey() - verifying_key = signing_privkey.get_pubkey() - - receiving_privkey, receiving_pubkey = bobs_keys - - kfrags = pre.generate_kfrags(delegating_privkey=delegating_privkey, - signer=Signer(signing_privkey), - receiving_pubkey=receiving_pubkey, - threshold=6, - N=10, - sign_delegating_key=False, - sign_receiving_key=False) - - for kfrag in kfrags: - # You can verify the KFrag specifying only the verifying key - assert kfrag.verify(signing_pubkey=verifying_key) - - # ... or if it is set in the capsule, using the capsule - capsule.set_correctness_keys(verifying=verifying_key) - assert kfrag.verify_for_capsule(capsule) - - # It should even work when other keys are set in the capsule - assert kfrag.verify(signing_pubkey=verifying_key, - delegating_pubkey=delegating_pubkey, - receiving_pubkey=receiving_pubkey) diff --git a/tests/functional/test_correctness_keys.py b/tests/functional/test_correctness_keys.py deleted file mode 100644 index aca88251..00000000 --- a/tests/functional/test_correctness_keys.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral import pre -from umbral.keys import UmbralPrivateKey -from umbral.kfrags import KFrag - - -def test_set_correctness_keys(alices_keys, bobs_keys, capsule, kfrags): - """ - If the three keys do appear together, along with the capsule, - we can attach them all at once. - """ - - delegating_privkey, signing_privkey = alices_keys - _receiving_privkey, receiving_pubkey = bobs_keys - - capsule.set_correctness_keys(delegating_privkey.get_pubkey(), - receiving_pubkey, - signing_privkey.get_pubkey() - ) - - for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, capsule) - capsule.attach_cfrag(cfrag) - - -def test_setting_one_correctness_keys(alices_keys, capsule): - # The capsule doesn't have any correctness keys set initially - assert capsule.get_correctness_keys()['delegating'] is None - assert capsule.get_correctness_keys()['receiving'] is None - assert capsule.get_correctness_keys()['verifying'] is None - - # Let's set only one of them, e.g., the delegating key - delegating_privkey, _signing_privkey = alices_keys - delegating_pubkey = delegating_privkey.get_pubkey() - - details = capsule.set_correctness_keys(delegating=delegating_pubkey) - - # Since we are only setting the first key ("delegating"), - # the other keys are not set - assert details == (True, False, False) - - assert capsule.get_correctness_keys()['delegating'] == delegating_pubkey - assert capsule.get_correctness_keys()['receiving'] is None - assert capsule.get_correctness_keys()['verifying'] is None - - -def test_set_invalid_correctness_keys(alices_keys, capsule, kfrags): - """ - If the three keys do appear together, along with the capsule, - we can attach them all at once. - """ - - delegating_privkey, signing_privkey = alices_keys - unrelated_receiving_pubkey = UmbralPrivateKey.gen_key().get_pubkey() - - capsule.set_correctness_keys(delegating_privkey.get_pubkey(), - unrelated_receiving_pubkey, - signing_privkey.get_pubkey() - ) - - for kfrag in kfrags: - with pytest.raises(KFrag.NotValid): - cfrag = pre.reencrypt(kfrag, capsule) diff --git a/tests/functional/test_pre_api.py b/tests/functional/test_pre_api.py deleted file mode 100644 index 1928a0ad..00000000 --- a/tests/functional/test_pre_api.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral import pre -from umbral.signing import Signer -from ..conftest import wrong_parameters - - -def test_public_key_encryption(alices_keys): - delegating_privkey, _ = alices_keys - plain_data = b'peace at dawn' - ciphertext, capsule = pre.encrypt(delegating_privkey.get_pubkey(), plain_data) - cleartext = pre.decrypt(ciphertext, capsule, delegating_privkey) - assert cleartext == plain_data - - -@pytest.mark.parametrize("N, M", wrong_parameters) -def test_wrong_N_M_in_split_rekey(N, M, alices_keys, bobs_keys): - delegating_privkey, signing_privkey = alices_keys - signer = Signer(signing_privkey) - _receiving_privkey, receiving_pubkey = bobs_keys - - with pytest.raises(ValueError): - _kfrags = pre.generate_kfrags(delegating_privkey=delegating_privkey, - signer=signer, - receiving_pubkey=receiving_pubkey, - threshold=M, - N=N) - - -def test_decryption_error(alices_keys, bobs_keys, ciphertext_and_capsule, message): - delegating_privkey, _signing_privkey = alices_keys - receiving_privkey, _receiving_pubkey = bobs_keys - ciphertext, capsule = ciphertext_and_capsule - - cleartext = pre.decrypt(ciphertext, capsule, delegating_privkey) - assert message == cleartext - - with pytest.raises(pre.UmbralDecryptionError) as e: - _cleartext = pre.decrypt(ciphertext, capsule, receiving_privkey) diff --git a/tests/functional/test_vectors.py b/tests/functional/test_vectors.py deleted file mode 100644 index b3e4717c..00000000 --- a/tests/functional/test_vectors.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import json -import os - -from umbral.curvebn import CurveBN -from umbral.point import Point -from umbral.keys import UmbralPublicKey -from umbral.config import default_params -from umbral.kfrags import KFrag -from umbral.cfrags import CapsuleFrag -from umbral.random_oracles import hash_to_curvebn, unsafe_hash_to_point, kdf -from umbral import pre - -def test_curvebn_operations(): - - vector_file = os.path.join('vectors', 'vectors_curvebn_operations.json') - try: - with open(vector_file) as f: - vector_suite = json.load(f) - except OSError: - raise - - bn1 = CurveBN.from_bytes(bytes.fromhex(vector_suite['first operand'])) - bn2 = CurveBN.from_bytes(bytes.fromhex(vector_suite['second operand'])) - - expected = dict() - for op_result in vector_suite['vectors']: - result = bytes.fromhex(op_result['result']) - expected[op_result['operation']] = CurveBN.from_bytes(result) - - test = [('Addition', bn1 + bn2), - ('Subtraction', bn1 - bn2), - ('Multiplication', bn1 * bn2), - ('Division', bn1 / bn2), - ('Pow', bn1 ** bn2), - ('Mod', bn1 % bn2), - ('Inverse', ~bn1), - ('Neg', -bn1), - ] - - for (operation, result) in test: - assert result == expected[operation], 'Error in {}'.format(operation) - -def test_curvebn_hash(): - - vector_file = os.path.join('vectors', 'vectors_curvebn_hash.json') - try: - with open(vector_file) as f: - vector_suite = json.load(f) - except OSError: - raise - - params = default_params() - - for vector in vector_suite['vectors']: - hash_input = [bytes.fromhex(item['bytes']) for item in vector['input']] - expected = CurveBN.from_bytes(bytes.fromhex(vector['output'])) - assert hash_to_curvebn(*hash_input, params=params) == expected - - -def test_point_operations(): - - vector_file = os.path.join('vectors', 'vectors_point_operations.json') - try: - with open(vector_file) as f: - vector_suite = json.load(f) - except OSError: - raise - - point1 = Point.from_bytes(bytes.fromhex(vector_suite['first Point operand'])) - point2 = Point.from_bytes(bytes.fromhex(vector_suite['second Point operand'])) - bn1 = CurveBN.from_bytes(bytes.fromhex(vector_suite['CurveBN operand'])) - - expected = dict() - for op_result in vector_suite['vectors']: - expected[op_result['operation']] = bytes.fromhex(op_result['result']) - - test = [('Addition', point1 + point2), - ('Subtraction', point1 - point2), - ('Multiplication', bn1 * point1), - ('Inversion', -point1), - ] - - for (operation, result) in test: - assert result == Point.from_bytes(expected[operation]), 'Error in {}'.format(operation) - - test = [('To_affine.X', point1.to_affine()[0]), - ('To_affine.Y', point1.to_affine()[1]), - ] - - for (operation, result) in test: - assert result == int.from_bytes(expected[operation], 'big'), 'Error in {}'.format(operation) - - assert kdf(point1, pre.DEM_KEYSIZE) == expected['kdf'] - - -def test_unsafe_hash_to_point(): - - vector_file = os.path.join('vectors', 'vectors_unsafe_hash_to_point.json') - try: - with open(vector_file) as f: - vector_suite = json.load(f) - except OSError: - raise - - params = default_params() - - for item in vector_suite['vectors']: - data = bytes.fromhex(item['data']) - label = bytes.fromhex(item['label']) - expected = Point.from_bytes(bytes.fromhex(item['point'])) - assert expected == unsafe_hash_to_point(label=label, data=data, params=params) - - -def test_kfrags(): - - vector_file = os.path.join('vectors', 'vectors_kfrags.json') - try: - with open(vector_file) as f: - vector_suite = json.load(f) - except OSError: - raise - - verifying_key = UmbralPublicKey.from_bytes(bytes.fromhex(vector_suite['verifying_key'])) - delegating_key = UmbralPublicKey.from_bytes(bytes.fromhex(vector_suite['delegating_key'])) - receiving_key = UmbralPublicKey.from_bytes(bytes.fromhex(vector_suite['receiving_key'])) - - for json_kfrag in vector_suite['vectors']: - kfrag = KFrag.from_bytes(bytes.fromhex(json_kfrag['kfrag'])) - assert kfrag.verify(signing_pubkey=verifying_key, - delegating_pubkey=delegating_key, - receiving_pubkey=receiving_key), \ - 'Invalid KFrag {}'.format(kfrag.to_bytes().hex()) - - -def test_cfrags(): - - vector_file = os.path.join('vectors', 'vectors_cfrags.json') - try: - with open(vector_file) as f: - vector_suite = json.load(f) - except OSError: - raise - - params = default_params() - - capsule = pre.Capsule.from_bytes(bytes.fromhex(vector_suite['capsule']), - params=params) - - verifying_key = UmbralPublicKey.from_bytes(bytes.fromhex(vector_suite['verifying_key'])) - delegating_key = UmbralPublicKey.from_bytes(bytes.fromhex(vector_suite['delegating_key'])) - receiving_key = UmbralPublicKey.from_bytes(bytes.fromhex(vector_suite['receiving_key'])) - - kfrags_n_cfrags = [(KFrag.from_bytes(bytes.fromhex(json_kfrag['kfrag'])), - CapsuleFrag.from_bytes(bytes.fromhex(json_kfrag['cfrag']))) - for json_kfrag in vector_suite['vectors']] - - capsule.set_correctness_keys(delegating=delegating_key, - receiving=receiving_key, - verifying=verifying_key) - - for kfrag, cfrag in kfrags_n_cfrags: - assert kfrag.verify(signing_pubkey=verifying_key, - delegating_pubkey=delegating_key, - receiving_pubkey=receiving_key), \ - 'Invalid KFrag {}'.format(kfrag.to_bytes().hex()) - - new_cfrag = pre.reencrypt(kfrag, capsule, provide_proof=False) - assert new_cfrag.point_e1 == cfrag.point_e1 - assert new_cfrag.point_v1 == cfrag.point_v1 - assert new_cfrag.kfrag_id == cfrag.kfrag_id - assert new_cfrag.point_precursor == cfrag.point_precursor - assert new_cfrag.proof is None - assert cfrag.to_bytes() == new_cfrag.to_bytes() diff --git a/tests/metrics/reencryption_benchmark.py b/tests/metrics/reencryption_benchmark.py deleted file mode 100644 index dce7eaa0..00000000 --- a/tests/metrics/reencryption_benchmark.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import sys -import os -import time - -sys.path.append(os.path.abspath(os.getcwd())) - - -import pytest -from umbral import keys, pre -from umbral.config import default_curve -from umbral.params import UmbralParameters -from umbral.signing import Signer - - -# -# Setup -# - - -CURVE = default_curve() -PARAMS = UmbralParameters(curve=CURVE) - -# Faster -# (M, N) # | -FRAG_VALUES = ((1, 1), # | - (2, 3), # | - (5, 8), # | - (6, 10), # | - (10, 30), # | - # (20, 30), # | # FIXME: CircleCi build killed - # (10, 100) # | - # | - ) # | -# Slower - - -def __standard_encryption_api() -> tuple: - - delegating_privkey = keys.UmbralPrivateKey.gen_key(params=PARAMS) - delegating_pubkey = delegating_privkey.get_pubkey() - - signing_privkey = keys.UmbralPrivateKey.gen_key(params=PARAMS) - signer = Signer(signing_privkey) - - receiving_privkey = keys.UmbralPrivateKey.gen_key(params=PARAMS) - receiving_pubkey = receiving_privkey.get_pubkey() - - plain_data = os.urandom(32) - ciphertext, capsule = pre.encrypt(delegating_pubkey, plain_data) - - capsule.set_correctness_keys(delegating=delegating_pubkey, - receiving=receiving_pubkey, - verifying=signing_privkey.get_pubkey()) - - return delegating_privkey, signer, receiving_pubkey, ciphertext, capsule - - -# -# KFrag Generation Benchmarks -# - - -@pytest.mark.benchmark(group="Reencryption Key Generation Performance", - disable_gc=True, - warmup=True, - warmup_iterations=10) -@pytest.mark.parametrize("m, n", FRAG_VALUES) -def test_generate_kfrags_performance(benchmark, m: int, n: int) -> None: - - def __setup(): - delegating_privkey, signer, receiving_pubkey, ciphertext, capsule = __standard_encryption_api() - args = (delegating_privkey, receiving_pubkey) - kwargs = {"threshold": m, "N": n, "signer": signer} - return args, kwargs - - print("\nBenchmarking {function} with M:{M} of N:{N}...".format(function="pre.generate_kfrags", M=m, N=n)) - benchmark.pedantic(pre.generate_kfrags, setup=__setup, rounds=1000) - assert True # ensure function finishes and succeeds. - - -# -# Reencryption Benchmarks -# - -@pytest.mark.benchmark(group="Reencryption Performance", - timer=time.perf_counter, - disable_gc=True, - warmup=True, - warmup_iterations=10) -@pytest.mark.parametrize("m, n", ((6, 10), )) -def test_random_frag_reencryption_performance(benchmark, m: int, n: int) -> None: - - def __setup(): - delegating_privkey, signer, receiving_pubkey, ciphertext, capsule = __standard_encryption_api() - kfrags = pre.generate_kfrags(delegating_privkey, receiving_pubkey, m, n, signer) - one_kfrag, *remaining_kfrags = kfrags - args, kwargs = tuple(), {"kfrag": one_kfrag, "capsule": capsule}, - return args, kwargs - - print("\nBenchmarking {} with randomly created fragments...".format("pre.reencrypt")) - benchmark.pedantic(pre.reencrypt, setup=__setup, rounds=1000) - assert True # ensure function finishes and succeeds. - - -@pytest.mark.benchmark(group="Reencryption Performance", - timer=time.perf_counter, - disable_gc=True, - min_time=0.00005, - max_time=0.005, - min_rounds=7, - warmup=True, - warmup_iterations=10) -@pytest.mark.parametrize("m, n", ((6, 10), )) -def test_single_frag_reencryption_performance(benchmark, m: int, n: int) -> None: - - delegating_privkey, signer, receiving_pubkey, ciphertext, capsule = __standard_encryption_api() - kfrags = pre.generate_kfrags(delegating_privkey, receiving_pubkey, m, n, signer) - one_kfrag, *remaining_kfrags = kfrags - args, kwargs = tuple(), {"kfrag": one_kfrag, "capsule": capsule}, - - print("\nBenchmarking {} with the same fragment({M} of {N}) repeatedly...".format("pre.reencrypt", M=m, N=n)) - benchmark.pedantic(pre.reencrypt, args=args, kwargs=kwargs, iterations=20, rounds=100) - assert True # ensure function finishes and succeeds. diff --git a/tests/metrics/reencryption_firehose.py b/tests/metrics/reencryption_firehose.py deleted file mode 100644 index 17039563..00000000 --- a/tests/metrics/reencryption_firehose.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import os -import sys -from typing import Tuple, List - -sys.path.append(os.path.abspath(os.getcwd())) - -from umbral.kfrags import KFrag -from umbral.pre import Capsule -from umbral import keys, pre -from umbral.config import default_curve -from umbral.params import UmbralParameters -from umbral.signing import Signer - -CURVE = default_curve() -PARAMS = UmbralParameters(curve=CURVE) -REENCRYPTIONS = 1000 - - -def __produce_kfrags_and_capsule(m: int, n: int) -> Tuple[List[KFrag], Capsule]: - - delegating_privkey = keys.UmbralPrivateKey.gen_key(params=PARAMS) - delegating_pubkey = delegating_privkey.get_pubkey() - - signing_privkey = keys.UmbralPrivateKey.gen_key(params=PARAMS) - signer = Signer(signing_privkey) - - receiving_privkey = keys.UmbralPrivateKey.gen_key(params=PARAMS) - receiving_pubkey = receiving_privkey.get_pubkey() - - plain_data = os.urandom(32) - ciphertext, capsule = pre.encrypt(delegating_pubkey, plain_data) - - kfrags = pre.generate_kfrags(delegating_privkey, receiving_pubkey, m, n, signer) - - capsule.set_correctness_keys(delegating=delegating_pubkey, - receiving=receiving_pubkey, - verifying=signing_privkey.get_pubkey()) - - return kfrags, capsule - - -def firehose(m: int=6, n: int=10) -> None: - - print("Making kfrags...") - kfrags, capsule = __produce_kfrags_and_capsule(m=m, n=n) - one_kfrag, *remaining_kfrags = kfrags - - print('Re-encrypting...') - successful_reencryptions = 0 - for iteration in range(int(REENCRYPTIONS)): - - _cfrag = pre.reencrypt(one_kfrag, capsule) # <<< REENCRYPTION HAPPENS HERE - - successful_reencryptions += 1 - if iteration % 20 == 0: - print('Performed {} Re-encryptions...'.format(iteration)) - - failure_message = "A Reencryption failed. {} of {} succeeded".format(successful_reencryptions, REENCRYPTIONS) - assert successful_reencryptions == REENCRYPTIONS, failure_message - print("Successfully performed {} reencryptions".format(successful_reencryptions), end='\n') - - -if __name__ == "__main__": - firehose() # do diff --git a/tests/scenario/__init__.py b/tests/scenario/__init__.py deleted file mode 100644 index b1419391..00000000 --- a/tests/scenario/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" diff --git a/tests/scenario/test_lifecycle_multidomain.py b/tests/scenario/test_lifecycle_multidomain.py deleted file mode 100644 index 1649f804..00000000 --- a/tests/scenario/test_lifecycle_multidomain.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral import pre -from umbral.cfrags import CapsuleFrag -from umbral.kfrags import KFrag -from umbral.config import default_curve -from umbral.params import UmbralParameters -from umbral.signing import Signer -from umbral.keys import UmbralPrivateKey, UmbralPublicKey -from ..conftest import parameters, other_supported_curves, kfrag_signing_modes - - -@pytest.mark.parametrize("N, M", parameters) -@pytest.mark.parametrize("signing_mode", kfrag_signing_modes) -def test_lifecycle_with_serialization(N, M, signing_mode, curve=default_curve()): - """ - This test is a variant of test_simple_api, but with intermediate - serialization/deserialization steps, modeling how pyUmbral artifacts - (such as keys, ciphertexts, etc) will actually be used. - These intermediate steps are in between the different 'usage domains' - in NuCypher, namely, key generation, delegation, encryption, decryption by - Alice, re-encryption by Ursula, and decryption by Bob. - - Manually injects UmbralParameters for multi-curve testing. - """ - - # Convenience method to avoid replicating key generation code - def new_keypair_bytes(): - privkey = UmbralPrivateKey.gen_key(params=params) - return privkey.to_bytes(), privkey.get_pubkey().to_bytes() - - ## SETUP - params = UmbralParameters(curve=curve) - - delegating_privkey_bytes, delegating_pubkey_bytes = new_keypair_bytes() - signing_privkey_bytes, signing_pubkey_bytes = new_keypair_bytes() - receiving_privkey_bytes, receiving_pubkey_bytes = new_keypair_bytes() - - ## DELEGATION DOMAIN: - ## Alice delegates decryption rights to some Bob by generating a set of - ## KFrags, using her delegating private key and Bob's receiving public key - - delegating_privkey = UmbralPrivateKey.from_bytes(delegating_privkey_bytes, params=params) - signing_privkey = UmbralPrivateKey.from_bytes(signing_privkey_bytes, params=params) - receiving_pubkey = UmbralPublicKey.from_bytes(receiving_pubkey_bytes, params=params) - - signer = Signer(signing_privkey) - - sign_delegating_key, sign_receiving_key = signing_mode - - kfrags = pre.generate_kfrags(delegating_privkey=delegating_privkey, - receiving_pubkey=receiving_pubkey, - threshold=M, - N=N, - signer=signer, - sign_delegating_key=sign_delegating_key, - sign_receiving_key=sign_receiving_key) - - kfrags_bytes = tuple(map(bytes, kfrags)) - - del kfrags - del signer - del delegating_privkey - del signing_privkey - del receiving_pubkey - del params - - ## ENCRYPTION DOMAIN ## - - params = UmbralParameters(curve=curve) - - delegating_pubkey = UmbralPublicKey.from_bytes(delegating_pubkey_bytes, params) - - plain_data = b'peace at dawn' - ciphertext, capsule = pre.encrypt(delegating_pubkey, plain_data) - capsule_bytes = bytes(capsule) - - del capsule - del delegating_pubkey - del params - - ## DECRYPTION BY ALICE ## - - params = UmbralParameters(curve=curve) - - delegating_privkey = UmbralPrivateKey.from_bytes(delegating_privkey_bytes, params=params) - capsule = pre.Capsule.from_bytes(capsule_bytes, params) - cleartext = pre.decrypt(ciphertext, capsule, delegating_privkey) - assert cleartext == plain_data - - del delegating_privkey - del capsule - del params - - ## RE-ENCRYPTION DOMAIN (i.e., Ursula's side) - - cfrags_bytes = list() - for kfrag_bytes in kfrags_bytes: - params = UmbralParameters(curve=curve) - delegating_pubkey = UmbralPublicKey.from_bytes(delegating_pubkey_bytes, params) - signing_pubkey = UmbralPublicKey.from_bytes(signing_pubkey_bytes, params) - receiving_pubkey = UmbralPublicKey.from_bytes(receiving_pubkey_bytes, params) - - capsule = pre.Capsule.from_bytes(capsule_bytes, params) - capsule.set_correctness_keys(delegating=delegating_pubkey, - receiving=receiving_pubkey, - verifying=signing_pubkey) - - # TODO: use params instead of curve? - kfrag = KFrag.from_bytes(kfrag_bytes, params.curve) - - assert kfrag.verify(signing_pubkey, delegating_pubkey, receiving_pubkey, params) - - cfrag_bytes = bytes(pre.reencrypt(kfrag, capsule)) - cfrags_bytes.append(cfrag_bytes) - - del capsule - del kfrag - del params - del delegating_pubkey - del signing_pubkey - del receiving_pubkey - - ## DECRYPTION DOMAIN (i.e., Bob's side) - params = UmbralParameters(curve=curve) - - capsule = pre.Capsule.from_bytes(capsule_bytes, params) - delegating_pubkey = UmbralPublicKey.from_bytes(delegating_pubkey_bytes, params) - signing_pubkey = UmbralPublicKey.from_bytes(signing_pubkey_bytes, params) - receiving_privkey = UmbralPrivateKey.from_bytes(receiving_privkey_bytes, params=params) - receiving_pubkey = receiving_privkey.get_pubkey() - - capsule.set_correctness_keys(delegating=delegating_pubkey, - receiving=receiving_pubkey, - verifying=signing_pubkey) - - for cfrag_bytes in cfrags_bytes: - # TODO: use params instead of curve? - cfrag = CapsuleFrag.from_bytes(cfrag_bytes, params.curve) - capsule.attach_cfrag(cfrag) - - reenc_cleartext = pre.decrypt(ciphertext, capsule, receiving_privkey) - assert reenc_cleartext == plain_data - - -@pytest.mark.parametrize("curve", other_supported_curves) -@pytest.mark.parametrize("N, M", parameters) -@pytest.mark.parametrize("signing_mode", kfrag_signing_modes) -def test_lifecycle_with_serialization_on_multiple_curves(N, M, signing_mode, curve): - test_lifecycle_with_serialization(N, M, signing_mode, curve) diff --git a/tests/scenario/test_simple_api.py b/tests/scenario/test_simple_api.py deleted file mode 100644 index afd3ce75..00000000 --- a/tests/scenario/test_simple_api.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral import pre -from umbral.config import default_curve -from umbral.params import UmbralParameters -from umbral.signing import Signer -from umbral.keys import UmbralPrivateKey -from ..conftest import parameters, other_supported_curves - - -@pytest.mark.parametrize("N, M", parameters) -def test_simple_api(N, M, curve=default_curve()): - """ - This test models the main interactions between NuCypher actors (i.e., Alice, - Bob, Data Source, and Ursulas) and artifacts (i.e., public and private keys, - ciphertexts, capsules, KFrags, CFrags, etc). - - The test covers all the main stages of data sharing with NuCypher: - key generation, delegation, encryption, decryption by - Alice, re-encryption by Ursula, and decryption by Bob. - - Manually injects umbralparameters for multi-curve testing.""" - - # Generation of global parameters - params = UmbralParameters(curve=curve) - - # Key Generation (Alice) - delegating_privkey = UmbralPrivateKey.gen_key(params=params) - delegating_pubkey = delegating_privkey.get_pubkey() - - signing_privkey = UmbralPrivateKey.gen_key(params=params) - signing_pubkey = signing_privkey.get_pubkey() - signer = Signer(signing_privkey) - - # Key Generation (Bob) - receiving_privkey = UmbralPrivateKey.gen_key(params=params) - receiving_pubkey = receiving_privkey.get_pubkey() - - # Encryption by an unnamed data source - plain_data = b'peace at dawn' - ciphertext, capsule = pre.encrypt(delegating_pubkey, plain_data) - - # Decryption by Alice - cleartext = pre.decrypt(ciphertext, capsule, delegating_privkey) - assert cleartext == plain_data - - # Split Re-Encryption Key Generation (aka Delegation) - kfrags = pre.generate_kfrags(delegating_privkey, receiving_pubkey, M, N, signer) - - - # Capsule preparation (necessary before re-encryotion and activation) - capsule.set_correctness_keys(delegating=delegating_pubkey, - receiving=receiving_pubkey, - verifying=signing_pubkey) - - # Bob requests re-encryption to some set of M ursulas - cfrags = list() - for kfrag in kfrags[:M]: - # Ursula checks that the received kfrag is valid - assert kfrag.verify(signing_pubkey, delegating_pubkey, receiving_pubkey, params) - - # Re-encryption by an Ursula - cfrag = pre.reencrypt(kfrag, capsule) - - # Bob collects the result - cfrags.append(cfrag) - - # Capsule activation (by Bob) - for cfrag in cfrags: - capsule.attach_cfrag(cfrag) - - # Decryption by Bob - reenc_cleartext = pre.decrypt(ciphertext, capsule, receiving_privkey) - assert reenc_cleartext == plain_data - - -@pytest.mark.parametrize("curve", other_supported_curves) -@pytest.mark.parametrize("N, M", parameters) -def test_simple_api_on_multiple_curves(N, M, curve): - test_simple_api(N, M, curve) - diff --git a/tests/unit/test_capsule_correctness_checks.py b/tests/unit/test_capsule_correctness_checks.py deleted file mode 100644 index 6b218791..00000000 --- a/tests/unit/test_capsule_correctness_checks.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import os - -import pytest - -from umbral.curvebn import CurveBN -from umbral.cfrags import CapsuleFrag -from umbral.keys import UmbralPrivateKey -from umbral.point import Point -from umbral.pre import Capsule -from umbral.config import default_params - - -def test_cannot_attach_cfrag_without_keys(): - """ - We need the proper keys to verify the correctness of CFrags - in order to attach them to a Capsule. - """ - params = default_params() - - capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - cfrag = CapsuleFrag(point_e1=Point.gen_rand(), - point_v1=Point.gen_rand(), - kfrag_id=os.urandom(10), - point_precursor=Point.gen_rand(), - ) - - with pytest.raises(TypeError): - capsule.attach_cfrag(cfrag) - - -def test_cannot_attach_cfrag_without_proof(): - """ - However, even when properly attaching keys, we can't attach the CFrag - if it is unproven. - """ - params = default_params() - - capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - cfrag = CapsuleFrag(point_e1=Point.gen_rand(), - point_v1=Point.gen_rand(), - kfrag_id=os.urandom(10), - point_precursor=Point.gen_rand(), - ) - key_details = capsule.set_correctness_keys( - UmbralPrivateKey.gen_key().get_pubkey(), - UmbralPrivateKey.gen_key().get_pubkey(), - UmbralPrivateKey.gen_key().get_pubkey()) - - delegating_details, receiving_details, verifying_details = key_details - - assert all((delegating_details, receiving_details, verifying_details)) - - with pytest.raises(cfrag.NoProofProvided): - capsule.attach_cfrag(cfrag) - - -def test_cannot_set_different_keys(): - """ - Once a key is set on a Capsule, it can't be changed to a different key. - """ - params = default_params() - - capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - capsule.set_correctness_keys(delegating=UmbralPrivateKey.gen_key().get_pubkey(), - receiving=UmbralPrivateKey.gen_key().get_pubkey(), - verifying=UmbralPrivateKey.gen_key().get_pubkey()) - - with pytest.raises(ValueError): - capsule.set_correctness_keys(delegating=UmbralPrivateKey.gen_key().get_pubkey()) - - with pytest.raises(ValueError): - capsule.set_correctness_keys(receiving=UmbralPrivateKey.gen_key().get_pubkey()) - - with pytest.raises(ValueError): - capsule.set_correctness_keys(verifying=UmbralPrivateKey.gen_key().get_pubkey()) diff --git a/tests/unit/test_capsule_operations.py b/tests/unit/test_capsule_operations.py deleted file mode 100644 index 4f4588bd..00000000 --- a/tests/unit/test_capsule_operations.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral import pre -from umbral.curvebn import CurveBN -from umbral.point import Point -from umbral.pre import Capsule -from umbral.signing import Signer -from umbral.keys import UmbralPrivateKey -from umbral.config import default_params - - -def test_capsule_creation(alices_keys): - - params = default_params() - - with pytest.raises(TypeError): - rare_capsule = Capsule(params) # Alice cannot make a capsule this way. - - - - # Some users may create capsules their own way. - custom_capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - assert isinstance(custom_capsule, Capsule) - - # Typical Alice, constructing a typical capsule - delegating_privkey, _signing_key = alices_keys - plaintext = b'peace at dawn' - ciphertext, typical_capsule = pre.encrypt(delegating_privkey.get_pubkey(), plaintext) - - assert isinstance(typical_capsule, Capsule) - - -def test_capsule_equality(): - params = default_params() - - one_capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - another_capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - assert one_capsule != another_capsule - - -def test_decapsulation_by_alice(alices_keys): - params = default_params() - - delegating_privkey, _signing_privkey = alices_keys - - sym_key, capsule = pre._encapsulate(delegating_privkey.get_pubkey()) - assert len(sym_key) == 32 - - # The symmetric key sym_key is perhaps used for block cipher here in a real-world scenario. - sym_key_2 = pre._decapsulate_original(delegating_privkey, capsule) - assert sym_key_2 == sym_key - - -def test_bad_capsule_fails_reencryption(kfrags): - params = default_params() - - bollocks_capsule = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=CurveBN.gen_rand()) - - for kfrag in kfrags: - with pytest.raises(Capsule.NotValid): - pre.reencrypt(kfrag, bollocks_capsule) - - -def test_capsule_as_dict_key(alices_keys, bobs_keys): - delegating_privkey, signing_privkey = alices_keys - signer_alice = Signer(signing_privkey) - delegating_pubkey = delegating_privkey.get_pubkey() - signing_pubkey = signing_privkey.get_pubkey() - - receiving_privkey, receiving_pubkey = bobs_keys - - plain_data = b'peace at dawn' - ciphertext, capsule = pre.encrypt(delegating_pubkey, plain_data) - - # We can use the capsule as a key, and successfully lookup using it. - some_dict = {capsule: "Thing that Bob wants to try per-Capsule"} - assert some_dict[capsule] == "Thing that Bob wants to try per-Capsule" - - # And if we change the value for this key, all is still well. - some_dict[capsule] = "Bob has changed his mind." - assert some_dict[capsule] == "Bob has changed his mind." - assert len(some_dict.keys()) == 1 - - -def test_capsule_length(prepared_capsule, kfrags): - capsule = prepared_capsule - - for counter, kfrag in enumerate(kfrags): - assert len(capsule) == counter - cfrag = pre.reencrypt(kfrag, capsule) - capsule.attach_cfrag(cfrag) - - -def test_capsule_clear(prepared_capsule, kfrags): - capsule = prepared_capsule - - for counter, kfrag in enumerate(kfrags): - assert len(capsule) == counter - cfrag = pre.reencrypt(kfrag, capsule) - capsule.attach_cfrag(cfrag) - - capsule.clear_cfrags() - assert len(capsule) == 0 \ No newline at end of file diff --git a/tests/unit/test_capsule_serializers.py b/tests/unit/test_capsule_serializers.py deleted file mode 100644 index 84e42df2..00000000 --- a/tests/unit/test_capsule_serializers.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral.pre import Capsule -from umbral.curvebn import CurveBN -from umbral.point import Point - - -def test_capsule_serialization(capsule: Capsule): - params = capsule.params - capsule_bytes = capsule.to_bytes() - capsule_bytes_casted = bytes(capsule) - assert capsule_bytes == capsule_bytes_casted - - # A Capsule can be represented as the 98 total bytes of two Points (33 each) and a CurveBN (32). - assert len(capsule_bytes) == Capsule.expected_bytes_length() - - new_capsule = Capsule.from_bytes(capsule_bytes, params) - - # Three ways to think about equality. - # First, the public approach for the Capsule. Simply: - assert new_capsule == capsule - - # Second, we show that the original components (which is all we have here since we haven't activated) are the same: - assert new_capsule.components() == capsule.components() - - # Third, we can directly compare the private original component attributes - # (though this is not a supported approach): - assert new_capsule.point_e == capsule.point_e - assert new_capsule.point_v == capsule.point_v - assert new_capsule.bn_sig == capsule.bn_sig - - -def test_cannot_create_capsule_from_bogus_material(alices_keys): - params = alices_keys[0].params - - with pytest.raises(TypeError): - _capsule_of_questionable_parentage = Capsule(params, - point_e=Point.gen_rand(), - point_v=42, - bn_sig=CurveBN.gen_rand()) - - with pytest.raises(TypeError): - _capsule_of_questionable_parentage = Capsule(params, - point_e=Point.gen_rand(), - point_v=Point.gen_rand(), - bn_sig=42) diff --git a/tests/unit/test_cfrags.py b/tests/unit/test_cfrags.py deleted file mode 100644 index d707157c..00000000 --- a/tests/unit/test_cfrags.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from umbral import pre -from umbral.cfrags import CapsuleFrag, CorrectnessProof - - -def test_cfrag_serialization_with_proof_and_metadata(prepared_capsule, kfrags): - - # Example of potential metadata to describe the re-encryption request - metadata = b'This is an example of metadata for re-encryption request' - for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, prepared_capsule, provide_proof=True, metadata=metadata) - cfrag_bytes = cfrag.to_bytes() - - proof = cfrag.proof - assert proof is not None - assert proof.metadata is not None - - new_cfrag = CapsuleFrag.from_bytes(cfrag_bytes) - assert new_cfrag.point_e1 == cfrag.point_e1 - assert new_cfrag.point_v1 == cfrag.point_v1 - assert new_cfrag.kfrag_id == cfrag.kfrag_id - assert new_cfrag.point_precursor == cfrag.point_precursor - - new_proof = new_cfrag.proof - assert new_proof is not None - assert new_proof.point_e2 == proof.point_e2 - assert new_proof.point_v2 == proof.point_v2 - assert new_proof.point_kfrag_commitment == proof.point_kfrag_commitment - assert new_proof.point_kfrag_pok == proof.point_kfrag_pok - assert new_proof.bn_sig == proof.bn_sig - assert new_proof.metadata == metadata - assert new_proof.metadata == proof.metadata - - -def test_cfrag_serialization_with_proof_but_no_metadata(prepared_capsule, kfrags): - - for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, prepared_capsule, provide_proof=True) - cfrag_bytes = cfrag.to_bytes() - - proof = cfrag.proof - assert proof is not None - assert proof.metadata is None - - # A CFrag can be represented as the 131 total bytes of three Points (33 each) and a CurveBN (32). - # TODO: Figure out final size for CFrags with proofs - # assert len(cfrag_bytes) == 33 + 33 + 33 + 32 == 131 - - new_cfrag = CapsuleFrag.from_bytes(cfrag_bytes) - assert new_cfrag.point_e1 == cfrag.point_e1 - assert new_cfrag.point_v1 == cfrag.point_v1 - assert new_cfrag.kfrag_id == cfrag.kfrag_id - assert new_cfrag.point_precursor == cfrag.point_precursor - - new_proof = new_cfrag.proof - assert new_proof is not None - assert new_proof.point_e2 == proof.point_e2 - assert new_proof.point_v2 == proof.point_v2 - assert new_proof.point_kfrag_commitment == proof.point_kfrag_commitment - assert new_proof.point_kfrag_pok == proof.point_kfrag_pok - assert new_proof.bn_sig == proof.bn_sig - assert new_proof.metadata is None - - -def test_cfrag_serialization_no_proof_no_metadata(prepared_capsule, kfrags): - - for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, prepared_capsule, provide_proof=False) - cfrag_bytes = cfrag.to_bytes() - - proof = cfrag.proof - assert proof is None - - assert len(cfrag_bytes) == CapsuleFrag.expected_bytes_length() - - new_cfrag = CapsuleFrag.from_bytes(cfrag_bytes) - assert new_cfrag.point_e1 == cfrag.point_e1 - assert new_cfrag.point_v1 == cfrag.point_v1 - assert new_cfrag.kfrag_id == cfrag.kfrag_id - assert new_cfrag.point_precursor == cfrag.point_precursor - - new_proof = new_cfrag.proof - assert new_proof is None - - -def test_correctness_proof_serialization(prepared_capsule, kfrags): - - # Example of potential metadata to describe the re-encryption request - metadata = b"This is an example of metadata for re-encryption request" - - for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, prepared_capsule, metadata=metadata) - proof = cfrag.proof - proof_bytes = proof.to_bytes() - - # A CorrectnessProof can be represented as - # the 228 total bytes of four Points (33 each) and three BigNums (32 each). - # TODO: Figure out final size for CorrectnessProofs - # assert len(proof_bytes) == (33 * 4) + (32 * 3) == 228 - - new_proof = CorrectnessProof.from_bytes(proof_bytes) - assert new_proof.point_e2 == proof.point_e2 - assert new_proof.point_v2 == proof.point_v2 - assert new_proof.point_kfrag_commitment == proof.point_kfrag_commitment - assert new_proof.point_kfrag_pok == proof.point_kfrag_pok - assert new_proof.bn_sig == proof.bn_sig - assert new_proof.kfrag_signature == proof.kfrag_signature - assert new_proof.metadata == proof.metadata - - \ No newline at end of file diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py deleted file mode 100644 index cf130519..00000000 --- a/tests/unit/test_config.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import importlib -import pytest -import warnings - -from umbral.config import _CONFIG -from umbral.curve import SECP256K1, SECP256R1 - - -def _copy_config_for_testing(): - """ - NEVER do this. This is for testing only. - This is absolutely not a thing to actually do in production code. At all. Ever. - """ - config_module_spec = importlib.util.find_spec("umbral.config") - config_copy = importlib.util.module_from_spec(config_module_spec) - config_module_spec.loader.exec_module(config_copy) - assert hasattr(config_copy, "default_curve") - assert config_copy is not _CONFIG - return config_copy - - -def test_try_to_use_curve_with_no_default_curve(): - config = _copy_config_for_testing() - - # No curve is set. - assert config._CONFIG._CONFIG__curve is None - - # Getting the default curve if we haven't set one yet sets one and gives us a warning. - with warnings.catch_warnings(record=True) as caught_warnings: - assert len(caught_warnings) == 0 - config.default_curve() - assert len(caught_warnings) == 1 - assert caught_warnings[0].message.args[0] == config._CONFIG._CONFIG__WARNING_IF_NO_DEFAULT_SET - assert caught_warnings[0].category == RuntimeWarning - - # Now, a default curve has been set. - assert config._CONFIG._CONFIG__curve == SECP256K1 - - -def test_try_to_use_default_params_with_no_default_curve(): - config = _copy_config_for_testing() - - # Again, no curve is set. - assert config._CONFIG._CONFIG__curve is None - - # This time, we'll try to use default_params() and get the same warning as above. - with warnings.catch_warnings(record=True) as caught_warnings: - assert len(caught_warnings) == 0 - config.default_params() - assert len(caught_warnings) == 1 - assert caught_warnings[0].message.args[0] == config._CONFIG._CONFIG__WARNING_IF_NO_DEFAULT_SET - assert caught_warnings[0].category == RuntimeWarning - - # Now, a default curve has been set. - assert config._CONFIG._CONFIG__curve == SECP256K1 - - -def test_cannot_set_default_curve_twice(): - config = _copy_config_for_testing() - - # pyumbral even supports NIST curves! - config.set_default_curve(SECP256R1) - - # Our default curve has been set... - assert config.default_curve() == SECP256R1 - - # ...but once set, you can't set the default curve again, even if you've found a better one. - with pytest.raises(config._CONFIG.UmbralConfigurationError): - config.set_default_curve(SECP256K1) diff --git a/tests/unit/test_curve.py b/tests/unit/test_curve.py deleted file mode 100644 index 16b14ec2..00000000 --- a/tests/unit/test_curve.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral.curve import Curve - - -def test_supported_curves(): - - # Ensure we have the correct number opf supported curves hardcoded - number_of_supported_curves = 3 - assert len(Curve._supported_curves) == number_of_supported_curves - - # Manually ensure the `_supported curves` dict contains only valid supported curves - assert Curve._supported_curves[415] == 'secp256r1' - assert Curve._supported_curves[714] == 'secp256k1' - assert Curve._supported_curves[715] == 'secp384r1' - - nid, name = 714, 'secp256k1' - - # - # Create by NID - # - - # supported - _curve_714 = Curve(nid=nid) - assert _curve_714.curve_nid == nid - assert _curve_714.name == name - - # unsuported - with pytest.raises(NotImplementedError): - _ = Curve(711) - - - # - # Create by Name - # - - # Supported - _curve_secp256k1 = Curve.from_name(name) - assert _curve_secp256k1.name == name - assert _curve_secp256k1.curve_nid == nid - - # Unsupported - with pytest.raises(NotImplementedError): - _ = Curve.from_name('abcd123e4') - - # Import curve constants - from umbral.curve import SECP256R1, SECP256K1, SECP384R1 - test_p256 = SECP256R1 - test_secp256k1 = SECP256K1 - test_p384 = SECP384R1 - - # Test the hardcoded curve NIDs are correct: - assert test_p256.curve_nid == 415 - assert test_secp256k1.curve_nid == 714 - assert test_p384.curve_nid == 715 - - # Ensure every curve constant is in the CURVES collection - from umbral.curve import CURVES - assert len(CURVES) == number_of_supported_curves - - # Ensure all supported curves can be initialized - for nid, name in Curve._supported_curves.items(): - _curve_nid, _curve_name = Curve(nid=nid), Curve.from_name(name) - assert _curve_nid.name == name - assert _curve_name.curve_nid == nid diff --git a/tests/unit/test_dem.py b/tests/unit/test_dem.py deleted file mode 100644 index 55b4327a..00000000 --- a/tests/unit/test_dem.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest -import os - -from umbral.dem import UmbralDEM, DEM_KEYSIZE, DEM_NONCE_SIZE -from cryptography.exceptions import InvalidTag - - -def test_encrypt_decrypt(): - key = os.urandom(32) - - dem = UmbralDEM(key) - - plaintext = b'peace at dawn' - - ciphertext0 = dem.encrypt(plaintext) - ciphertext1 = dem.encrypt(plaintext) - - assert ciphertext0 != plaintext - assert ciphertext1 != plaintext - - # Ciphertext should be different even with same plaintext. - assert ciphertext0 != ciphertext1 - - # Nonce should be different - assert ciphertext0[:DEM_NONCE_SIZE] != ciphertext1[:DEM_NONCE_SIZE] - - cleartext0 = dem.decrypt(ciphertext0) - cleartext1 = dem.decrypt(ciphertext1) - - assert cleartext0 == plaintext - assert cleartext1 == plaintext - - -def test_encrypt_decrypt_associated_data(): - key = os.urandom(32) - aad = b'secret code 1234' - - dem = UmbralDEM(key) - - plaintext = b'peace at dawn' - - ciphertext0 = dem.encrypt(plaintext, authenticated_data=aad) - ciphertext1 = dem.encrypt(plaintext, authenticated_data=aad) - - assert ciphertext0 != plaintext - assert ciphertext1 != plaintext - - assert ciphertext0 != ciphertext1 - - assert ciphertext0[:DEM_NONCE_SIZE] != ciphertext1[:DEM_NONCE_SIZE] - - cleartext0 = dem.decrypt(ciphertext0, authenticated_data=aad) - cleartext1 = dem.decrypt(ciphertext1, authenticated_data=aad) - - assert cleartext0 == plaintext - assert cleartext1 == plaintext - - # Attempt decryption with invalid associated data - with pytest.raises(InvalidTag): - cleartext2 = dem.decrypt(ciphertext0, authenticated_data=b'wrong data') diff --git a/tests/unit/test_kfrags.py b/tests/unit/test_kfrags.py deleted file mode 100644 index 1d3297f2..00000000 --- a/tests/unit/test_kfrags.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from umbral.kfrags import KFrag - - -def test_kfrag_serialization(alices_keys, bobs_keys, kfrags): - - delegating_privkey, signing_privkey = alices_keys - _receiving_privkey, receiving_pubkey = bobs_keys - - for kfrag in kfrags: - kfrag_bytes = kfrag.to_bytes() - assert len(kfrag_bytes) == KFrag.expected_bytes_length() - - new_kfrag = KFrag.from_bytes(kfrag_bytes) - assert new_kfrag.id == kfrag.id - assert new_kfrag.bn_key == kfrag.bn_key - assert new_kfrag.point_precursor == kfrag.point_precursor - assert new_kfrag.point_commitment == kfrag.point_commitment - assert new_kfrag.keys_in_signature == kfrag.keys_in_signature - assert new_kfrag.signature_for_proxy == kfrag.signature_for_proxy - assert new_kfrag.signature_for_bob == kfrag.signature_for_bob - - assert new_kfrag.verify(signing_pubkey=signing_privkey.get_pubkey(), - delegating_pubkey=delegating_privkey.get_pubkey(), - receiving_pubkey=receiving_pubkey) - - assert new_kfrag == kfrag - - -def test_kfrag_verify_for_capsule(prepared_capsule, kfrags): - for kfrag in kfrags: - assert kfrag.verify_for_capsule(prepared_capsule) - - # If we alter some element, the verification fails - previous_id, kfrag.id = kfrag.id, bytes(32) - assert not kfrag.verify_for_capsule(prepared_capsule) - - # Let's restore the KFrag, and alter the re-encryption key instead - kfrag.id = previous_id - kfrag.bn_key += kfrag.bn_key - assert not kfrag.verify_for_capsule(prepared_capsule) - - -def test_kfrag_as_dict_key(kfrags): - dict_with_kfrags_as_keys = dict() - dict_with_kfrags_as_keys[kfrags[0]] = "Some llamas. Definitely some llamas." - dict_with_kfrags_as_keys[kfrags[1]] = "No llamas here. Definitely not." - - assert dict_with_kfrags_as_keys[kfrags[0]] != dict_with_kfrags_as_keys[kfrags[1]] - - diff --git a/tests/unit/test_primitives/conftest.py b/tests/unit/test_primitives/conftest.py deleted file mode 100644 index cc118005..00000000 --- a/tests/unit/test_primitives/conftest.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import contextlib - -import pytest -from cryptography.hazmat.backends.openssl import backend - -from umbral.curvebn import CurveBN -from umbral.point import Point - - -@pytest.fixture() -def mock_openssl(mocker, random_ec_point1: Point, random_ec_curvebn1: CurveBN, random_ec_curvebn2: CurveBN): - """ - Patches openssl backend methods for testing. - For all functions, 1 is returned for success, 0 on error. - - """ - - actual_backend = { - # Point - 'EC_POINT_mul': backend._lib.EC_POINT_mul, - 'EC_POINT_cmp': backend._lib.EC_POINT_cmp, - 'EC_POINT_add': backend._lib.EC_POINT_add, - 'EC_POINT_invert': backend._lib.EC_POINT_invert, - - # Bignum - 'BN_cmp': backend._lib.BN_cmp, - 'BN_mod_exp': backend._lib.BN_mod_exp, - 'BN_mod_mul': backend._lib.BN_mod_mul, - 'BN_mod_inverse': backend._lib.BN_mod_inverse, - 'BN_mod_add': backend._lib.BN_mod_add, - 'BN_mod_sub': backend._lib.BN_mod_sub, - 'BN_nnmod': backend._lib.BN_nnmod, - } - - def check_curvebn_ctypes(*curvebns): - for bn in curvebns: - assert 'BIGNUM' in str(bn) - assert bn.__class__.__name__ == 'CDataGCP' - - def check_point_ctypes(*ec_points): - for point in ec_points: - assert 'EC_POINT' in str(point) - assert point.__class__.__name__ == 'CDataGCP' - - @contextlib.contextmanager - def mocked_openssl_backend(): - def mocked_ec_point_equality(group, ec_point, other_point, context): - check_point_ctypes(ec_point, other_point) - assert 'BN_CTX' in str(context) - assert 'EC_GROUP' in str(group) - assert random_ec_point1.curve.ec_group == group - assert not bool(actual_backend['EC_POINT_cmp'](group, random_ec_point1.ec_point, ec_point, context)) - result = actual_backend['EC_POINT_cmp'](group, random_ec_point1.ec_point, other_point, context) - assert not bool(result) - return result - - def mocked_ec_point_addition(group, sum, ec_point, other_point, context): - check_point_ctypes(sum, other_point) - assert 'BN_CTX' in str(context) - assert random_ec_point1.group == group - assert not bool(actual_backend['EC_POINT_cmp'](group, random_ec_point1.ec_point, ec_point, context)) - return actual_backend['EC_POINT_add'](group, sum, ec_point, other_point, context) - - def mocked_ec_point_multiplication(group, product, null, ec_point, curvebn, context): - check_point_ctypes(ec_point, product) - assert 'BN_CTX' in str(context) - assert 'EC_GROUP' in str(group) - assert 'NULL' in str(null) - assert random_ec_point1.group == group - assert random_ec_curvebn1.curve_nid == random_ec_point1.curve_nid - assert not bool(actual_backend['EC_POINT_cmp'](group, random_ec_point1.ec_point, ec_point, context)) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - return actual_backend['EC_POINT_mul'](group, product, null, ec_point, curvebn, context) - - def mocked_ec_point_inversion(group, inverse, context): - check_point_ctypes(inverse) - assert 'BN_CTX' in str(context) - assert random_ec_point1.group == group - return actual_backend['EC_POINT_invert'](group, inverse, context) - - def mocked_bn_compare(curvebn, other): - check_curvebn_ctypes(curvebn, other) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - return actual_backend['BN_cmp'](curvebn, other) - - def mocked_bn_mod_exponent(power, curvebn, other, order, context): - check_curvebn_ctypes(curvebn, other, power, order) - assert 'BN_CTX' in str(context) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn2.bignum, other)) - return actual_backend['BN_mod_exp'](power, curvebn, other, order, context) - - def mocked_bn_mod_multiplication(product, curvebn, other, order, context): - check_curvebn_ctypes(curvebn, other, product, order) - assert 'BN_CTX' in str(context) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn2.bignum, other)) - return actual_backend['BN_mod_mul'](product, curvebn, other, order, context) - - def mocked_bn_inverse(null, curvebn, order, context): - check_curvebn_ctypes(curvebn, order) - assert 'BN_CTX' in str(context) - assert 'NULL' in str(null) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - return actual_backend['BN_mod_inverse'](null, curvebn, order, context) - - def mocked_bn_addition(sum, curvebn, other, order, context): - check_curvebn_ctypes(curvebn, other, sum, order) - assert 'BN_CTX' in str(context) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn2.bignum, other)) - return actual_backend['BN_mod_add'](sum, curvebn, other, order, context) - - def mocked_bn_subtraction(diff, curvebn, other, order, context): - check_curvebn_ctypes(curvebn, other, diff, order) - assert 'BN_CTX' in str(context) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn2.bignum, other)) - return actual_backend['BN_mod_sub'](diff, curvebn, other, order, context) - - def mocked_bn_nmodulus(rem, curvebn, other, context): - check_curvebn_ctypes(curvebn, other, rem) - assert 'BN_CTX' in str(context) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn1.bignum, curvebn)) - assert not bool(actual_backend['BN_cmp'](random_ec_curvebn2.bignum, other)) - return actual_backend['BN_nnmod'](rem, curvebn, other, context) - - mock_load = { - # Point - 'EC_POINT_mul': mocked_ec_point_multiplication, - 'EC_POINT_cmp': mocked_ec_point_equality, - 'EC_POINT_add': mocked_ec_point_addition, - 'EC_POINT_invert': mocked_ec_point_inversion, - - # Bignum - 'BN_cmp': mocked_bn_compare, - 'BN_mod_exp': mocked_bn_mod_exponent, - 'BN_mod_mul': mocked_bn_mod_multiplication, - 'BN_mod_inverse': mocked_bn_inverse, - 'BN_mod_add': mocked_bn_addition, - 'BN_mod_sub': mocked_bn_subtraction, - 'BN_nnmod': mocked_bn_nmodulus, - } - - with contextlib.ExitStack() as stack: - for method, patch in mock_load.items(): - stack.enter_context(mocker.mock_module.patch.object(backend._lib, method, patch)) - yield - - return mocked_openssl_backend diff --git a/tests/unit/test_primitives/test_curvebn_arithmetic.py b/tests/unit/test_primitives/test_curvebn_arithmetic.py deleted file mode 100644 index 4f7e9474..00000000 --- a/tests/unit/test_primitives/test_curvebn_arithmetic.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from cryptography.hazmat.backends.openssl import backend -from umbral.curvebn import CurveBN - - -def test_mocked_openssl_curvebn_arithmetic(mock_openssl, random_ec_curvebn1, random_ec_curvebn2): - - operations_that_construct = ( - random_ec_curvebn1 * random_ec_curvebn2, # __mul__ - random_ec_curvebn1 ** random_ec_curvebn2, # __pow__ - random_ec_curvebn1 ** int(random_ec_curvebn2), # __pow__ (as int) - random_ec_curvebn1 + random_ec_curvebn2, # __add__ - random_ec_curvebn1 - random_ec_curvebn2, # __sub__ - -random_ec_curvebn1, # __neg__ - random_ec_curvebn1 % random_ec_curvebn2, # __mod__ - random_ec_curvebn1 % int(random_ec_curvebn2), # __mod__ (as int) - ~random_ec_curvebn1, # __invert__ - random_ec_curvebn1 / random_ec_curvebn2 # __truediv__ - ) - - with mock_openssl(): - assert random_ec_curvebn1 == random_ec_curvebn1 # __eq__ - for operator_result in operations_that_construct: - assert operator_result - assert isinstance(operator_result, CurveBN) - - order = backend._bn_to_int(random_ec_curvebn1.curve.order) - random_ec_curvebn1 = int(random_ec_curvebn1) - random_ec_curvebn2 = int(random_ec_curvebn2) - - # For simplicity, we test these two cases separately - assert (int(operations_that_construct[-2]) * random_ec_curvebn1) % order == 1 - assert (int(operations_that_construct[-1]) * random_ec_curvebn2) % order == random_ec_curvebn1 - - # The remaining cases can be tested in bulk - expected_results = ( - (random_ec_curvebn1 * random_ec_curvebn2) % order, # __mul__ - pow(random_ec_curvebn1, random_ec_curvebn2, order), # __pow__ - pow(random_ec_curvebn1, random_ec_curvebn2, order), # __pow__ (as int) - (random_ec_curvebn1 + random_ec_curvebn2) % order, # __add__ - (random_ec_curvebn1 - random_ec_curvebn2) % order, # __sub__ - (-random_ec_curvebn1) % order, # __neg__ - random_ec_curvebn1 % random_ec_curvebn2, # __mod__ - random_ec_curvebn1 % int(random_ec_curvebn2), # __mod__ (as int) - ) - - for (result, expected) in zip(operations_that_construct[:-2], expected_results): - assert result == expected diff --git a/tests/unit/test_primitives/test_curvebn_methods.py b/tests/unit/test_primitives/test_curvebn_methods.py deleted file mode 100644 index a714dada..00000000 --- a/tests/unit/test_primitives/test_curvebn_methods.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from umbral.curvebn import CurveBN -from umbral.random_oracles import hash_to_curvebn -import pytest - - -def test_cast_curvebn_to_int(): - x = CurveBN.gen_rand() - - x_as_int_from_dunder = x.__int__() - x_as_int_type_caster = int(x) - assert x_as_int_from_dunder == x_as_int_type_caster - x = x_as_int_type_caster - - y = CurveBN.from_int(x) - assert x == y - - -def test_cant_hash_arbitrary_object_into_bignum(): - whatever = object() - with pytest.raises(TypeError): - hash_to_curvebn(whatever) - diff --git a/tests/unit/test_primitives/test_curvebn_serializers.py b/tests/unit/test_primitives/test_curvebn_serializers.py deleted file mode 100644 index 9782846f..00000000 --- a/tests/unit/test_primitives/test_curvebn_serializers.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest - -from umbral.curvebn import CurveBN -from umbral.curve import CURVES -from cryptography.hazmat.backends import default_backend - -@pytest.mark.parametrize("curve", CURVES) -def test_serialization_rotations_of_1(curve): - - size_in_bytes = CurveBN.expected_bytes_length(curve) - for i in range(size_in_bytes): - lonely_one = 1 << i - bn = CurveBN.from_int(lonely_one, curve) - lonely_one_in_bytes = lonely_one.to_bytes(size_in_bytes, 'big') - - # Check serialization - assert bn.to_bytes() == lonely_one_in_bytes - - # Check deserialization - assert CurveBN.from_bytes(lonely_one_in_bytes, curve) == bn - -@pytest.mark.parametrize("curve", CURVES) -def test_invalid_deserialization(curve): - size_in_bytes = CurveBN.expected_bytes_length(curve) - - # All-zeros bytestring are invalid (i.e., 0 < bn < order of the curve) - zero_bytes = bytes(size_in_bytes) - with pytest.raises(ValueError): - _bn = CurveBN.from_bytes(zero_bytes, curve) - - # All-ones bytestring is invalid too (since it's greater than order) - lots_of_ones = 2**(8*size_in_bytes) - 1 - lots_of_ones = lots_of_ones.to_bytes(size_in_bytes, 'big') - with pytest.raises(ValueError): - _bn = CurveBN.from_bytes(lots_of_ones, curve) - - # Serialization of `order` is invalid since it's not strictly lower than - # the order of the curve - order = default_backend()._bn_to_int(curve.order) - with pytest.raises(ValueError): - _bn = CurveBN.from_bytes(order.to_bytes(size_in_bytes, 'big'), curve) - - # On the other hand, serialization of `order - 1` is valid - order -= 1 - _bn = CurveBN.from_bytes(order.to_bytes(size_in_bytes, 'big'), curve) diff --git a/tests/unit/test_primitives/test_point_arithmetic.py b/tests/unit/test_primitives/test_point_arithmetic.py deleted file mode 100644 index a8d02c81..00000000 --- a/tests/unit/test_primitives/test_point_arithmetic.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from umbral.curvebn import CurveBN -from umbral.point import Point - - -def test_mocked_openssl_point_arithmetic(mock_openssl, random_ec_point1, random_ec_point2, random_ec_curvebn1): - - operations_that_construct = ( - random_ec_point1 * random_ec_curvebn1, # __mul__ - random_ec_point1 + random_ec_point2, # __add__ - random_ec_point1 - random_ec_point2, # __sub__ - -random_ec_point1 # __neg__ - ) - - with mock_openssl(): - assert random_ec_point1 == random_ec_point1 # __eq__ - for operator_result in operations_that_construct: - assert operator_result - assert isinstance(operator_result, Point) - - -def test_point_curve_multiplication_regression(): - k256_point_bytes = b'\x03\xe0{\x1bQ\xbf@\x1f\x95\x8d\xe1\x17\xa7\xbe\x9e-G`T\xbf\xd7\x9e\xa7\x10\xc8uA\xc0z$\xc0\x92\x8a' - k256_bn_bytes = b'4u\xd70-\xa0h\xdeG\xf0\x143\x06!\x91\x05{\xe4jC\n\xf1h\xed7a\xf8\x9d\xec^\x19\x8c' - - k256_point = Point.from_bytes(k256_point_bytes) - k256_bn = CurveBN.from_bytes(k256_bn_bytes) - - product_with_star_operator = k256_point * k256_bn - - # Make sure we have instantiated a new, unequal point in the same curve and group - assert isinstance(product_with_star_operator, Point), "Point.__mul__ did not return a point instance" - assert k256_point != product_with_star_operator - assert k256_point.curve == product_with_star_operator.curve - - product_bytes = b'\x03\xc9\xda\xa2\x88\xe2\xa0+\xb1N\xb6\xe6\x1c\xa5(\xe6\xe0p\xf6\xf4\xa9\xfc\xb1\xfaUV\xd3\xb3\x0e4\x94\xbe\x12' - product_point = Point.from_bytes(product_bytes) - assert product_with_star_operator.to_bytes() == product_bytes - assert product_point == product_with_star_operator - - # Repeating the operation, should return the same result. - product_with_star_operator_again = k256_point * k256_bn - assert product_with_star_operator == product_with_star_operator_again diff --git a/tests/unit/test_primitives/test_point_serializers.py b/tests/unit/test_primitives/test_point_serializers.py deleted file mode 100644 index ec9e881e..00000000 --- a/tests/unit/test_primitives/test_point_serializers.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest -from cryptography.exceptions import InternalError - -from umbral.curve import SECP256K1, SECP256R1 -from umbral.point import Point - - -def generate_test_points_bytes(quantity=2): - points_bytes = [ - (SECP256K1, 714, b'\x02x{DR\x94\x8f\x17\xb8\xa2\x14t\x11\xdb\xb1VK\xdb\xc2\xa0T\x97iCK\x8cz~\xea\xa3\xb7AJ'), - ] - for _ in range(quantity): - args = (SECP256K1, 714, Point.gen_rand(curve=SECP256K1).to_bytes()) - points_bytes.append(args) - return points_bytes - - -def generate_test_points_affine(quantity=2): - points_affine = [ - (SECP256K1, 714, (54495335564072000415434275044935054036617226655045445809732056033758606213450, - 26274482902044210718566767736429706729731617411738990314884135712590488065008)), - ] - for _ in range(quantity): - args = (SECP256K1, 714, Point.gen_rand(curve=SECP256K1).to_affine()) - points_affine.append(args) - return points_affine - - -def test_generate_random_points(): - for _ in range(10): - point = Point.gen_rand() - another_point = Point.gen_rand() - assert isinstance(point, Point) - assert isinstance(another_point, Point) - assert point != another_point - - -@pytest.mark.parametrize("curve, nid, point_bytes", generate_test_points_bytes()) -def test_bytes_serializers(point_bytes, nid, curve): - point_with_curve = Point.from_bytes(point_bytes, curve=curve) # from curve - assert isinstance(point_with_curve, Point) - - the_same_point_bytes = point_with_curve.to_bytes() - assert point_bytes == the_same_point_bytes - - representations = (point_bytes, # Compressed representation - point_with_curve.to_bytes(is_compressed=False)) # Uncompressed - - for point_representation in representations: - - malformed_point_bytes = point_representation + b'0x' - with pytest.raises(InternalError): - _ = Point.from_bytes(malformed_point_bytes) - - malformed_point_bytes = point_representation[1:] - with pytest.raises(InternalError): - _ = Point.from_bytes(malformed_point_bytes) - - malformed_point_bytes = point_representation[:-1] - with pytest.raises(InternalError): - _ = Point.from_bytes(malformed_point_bytes) - - -@pytest.mark.parametrize("curve, nid, point_affine", generate_test_points_affine()) -def test_affine(point_affine, nid, curve): - point = Point.from_affine(point_affine, curve=curve) # from curve - assert isinstance(point, Point) - point_affine2 = point.to_affine() - assert point_affine == point_affine2 - - -def test_invalid_points(random_ec_point2): - - point_bytes = bytearray(random_ec_point2.to_bytes(is_compressed=False)) - point_bytes[-1] = point_bytes[-1] ^ 0x01 # Flips last bit - point_bytes = bytes(point_bytes) - - with pytest.raises(InternalError) as e: - _point = Point.from_bytes(point_bytes) - - # We want to catch specific InternalExceptions: - # - Point not in the curve (code 107) - # - Invalid compressed point (code 110) - # https://github.com/openssl/openssl/blob/master/include/openssl/ecerr.h#L228 - assert e.value.err_code[0].reason in (107, 110) - - -def test_generator_point(): - """http://www.secg.org/SEC2-Ver-1.0.pdf Section 2.7.1""" - g1 = Point.get_generator_from_curve() - - g_compressed = 0x0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 - g_uncompressed = 0x0479BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 - - g_compressed = g_compressed.to_bytes(32+1, byteorder='big') - g_uncompressed = g_uncompressed.to_bytes(64+1, byteorder='big') - - g2 = Point.from_bytes(g_compressed) - assert g1 == g2 - - g3 = Point.from_bytes(g_uncompressed) - assert g1 == g3 - assert g2 == g3 - - -def test_point_not_on_curve(): - """ - We want to be unable to create a Point that's not on the curve. - - When we try, we get cryptography.exceptions.InternalError - is that specifically because it isn't - on the curve? It seems to be reliably raised in the event of the Point being off the curve. - - The OpenSSL docs don't explicitly say that they raise an error for this reason: - https://www.openssl.org/docs/man1.1.0/crypto/EC_GFp_simple_method.html - """ - point_on_koblitz256_but_not_P256 = Point.from_bytes(b'\x03%\x98Dk\x88\xe2\x97\xab?\xabZ\xef\xd4' \ - b'\x9e\xaa\xc6\xb3\xa4\xa3\x89\xb2\xd7b.\x8f\x16Ci_&\xe0\x7f', curve=SECP256K1) - - from cryptography.exceptions import InternalError - with pytest.raises(InternalError): - Point.from_bytes(point_on_koblitz256_but_not_P256.to_bytes(), curve=SECP256R1) - - -def test_serialize_point_at_infinity(): - - p = Point.gen_rand() - point_at_infinity = p - p - - bytes_point_at_infinity = point_at_infinity.to_bytes() - assert bytes_point_at_infinity == b'\x00' - - -def test_coords_with_special_characteristics(): - - # Testing that a point with x coordinate greater than the curve order is still valid. - # In particular, we will test the last valid point from the default curve (secp256k1) - # whose x coordinate is `field_order - 3` and is greater than the order of the curve - - field_order = 2**256 - 0x1000003D1 - compressed = b'\x02' + (field_order-3).to_bytes(32, 'big') - - last_point = Point.from_bytes(compressed) - - # The same point, but obtained through the from_affine method - coords = (115792089237316195423570985008687907853269984665640564039457584007908834671660, - 109188863561374057667848968960504138135859662956057034999983532397866404169138) - - assert last_point == Point.from_affine(coords) diff --git a/tests/unit/test_serialization_property_based.py b/tests/unit/test_serialization_property_based.py deleted file mode 100644 index b203d82d..00000000 --- a/tests/unit/test_serialization_property_based.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from cryptography.hazmat.backends.openssl import backend -from hypothesis import HealthCheck, given, settings, unlimited -from hypothesis.strategies import binary, booleans, integers, tuples -from umbral.config import default_curve -from umbral.curvebn import CurveBN -from umbral.cfrags import CorrectnessProof -from umbral.kfrags import KFrag -from umbral.keys import UmbralPrivateKey, UmbralPublicKey -from umbral.params import UmbralParameters -from umbral.point import Point -from umbral.random_oracles import unsafe_hash_to_point -from umbral.pre import Capsule - -# test parameters -max_examples = 1000 - -# crypto constants -curve = default_curve() -params = UmbralParameters(curve) -bn_size = curve.group_order_size_in_bytes - -# generators -bns = integers(min_value=1, max_value=backend._bn_to_int(curve.order)).map( - lambda x: CurveBN.from_int(x)) - -points = binary(min_size=1).map( - lambda x: unsafe_hash_to_point(x, label=b'hypothesis', params=params)) - -signatures = tuples(integers(min_value=1, max_value=backend._bn_to_int(curve.order)), - integers(min_value=1, max_value=backend._bn_to_int(curve.order))).map( - lambda tup: tup[0].to_bytes(bn_size, 'big') + tup[1].to_bytes(bn_size, 'big')) - -# # utility -def assert_kfrag_eq(k0, k1): - assert(all([ k0.id == k1.id - , k0.bn_key == k1.bn_key - , k0.point_precursor == k1.point_precursor - , k0.point_commitment == k1.point_commitment - , k0.signature_for_bob == k1.signature_for_bob - , k0.signature_for_proxy == k1.signature_for_proxy - ])) - -def assert_cp_eq(c0, c1): - assert(all([ c0.point_e2 == c1.point_e2 - , c0.point_v2 == c1.point_v2 - , c0.point_kfrag_commitment == c1.point_kfrag_commitment - , c0.point_kfrag_pok == c1.point_kfrag_pok - , c0.kfrag_signature == c1.kfrag_signature - , c0.bn_sig == c1.bn_sig - , c0.metadata == c1.metadata - ])) - -# tests - -@given(bns) -@settings(max_examples=max_examples, timeout=unlimited) -def test_bn_roundtrip(bn): - assert(bn == CurveBN.from_bytes(bn.to_bytes())) - -@given(points, booleans()) -@settings(max_examples=max_examples, timeout=unlimited) -def test_point_roundtrip(p, c): - assert(p == Point.from_bytes(p.to_bytes(is_compressed=c))) - -@given(binary(min_size=bn_size, max_size=bn_size), bns, points, points, signatures, signatures) -@settings(max_examples=max_examples, timeout=unlimited) -def test_kfrag_roundtrip(d, b0, p0, p1, sig_proxy, sig_bob): - k = KFrag(identifier=d, bn_key=b0, point_commitment=p0, point_precursor=p1, - signature_for_proxy=sig_proxy, signature_for_bob=sig_bob) - assert_kfrag_eq(k, KFrag.from_bytes(k.to_bytes())) - -@given(points, points, bns) -@settings(max_examples=max_examples, timeout=unlimited) -def test_capsule_roundtrip_0(p0, p1, b): - c = Capsule(params=params, point_e=p0, point_v=p1, bn_sig=b) - assert(c == Capsule.from_bytes(c.to_bytes(), params=params)) - -@given(points, points, points, points, bns, signatures) -@settings(max_examples=max_examples, timeout=unlimited) -def test_cp_roundtrip(p0, p1, p2, p3, b0, sig): - c = CorrectnessProof(p0, p1, p2, p3, b0, sig) - assert_cp_eq(c, CorrectnessProof.from_bytes(c.to_bytes())) - -@given(points) -@settings(max_examples=max_examples, timeout=unlimited) -def test_pubkey_roundtrip(p): - k = UmbralPublicKey(p, params) - assert(k == UmbralPublicKey.from_bytes(k.to_bytes(), params=params)) - -@given(binary(min_size=1)) -@settings(max_examples=20, timeout=unlimited, suppress_health_check=[HealthCheck.hung_test]) -def test_privkey_roundtrip(p): - insecure_scrypt_cost = 5 # This is deliberately insecure, just to make it faster - k = UmbralPrivateKey.gen_key() - rt = UmbralPrivateKey.from_bytes(k.to_bytes(password=p, _scrypt_cost=insecure_scrypt_cost), - password=p, - _scrypt_cost=insecure_scrypt_cost) - assert(k.get_pubkey() == rt.get_pubkey()) \ No newline at end of file diff --git a/tests/unit/test_signing.py b/tests/unit/test_signing.py deleted file mode 100644 index 3de31465..00000000 --- a/tests/unit/test_signing.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import pytest -from cryptography.hazmat.backends.openssl import backend -from cryptography.hazmat.primitives import hashes -from umbral.keys import UmbralPrivateKey -from umbral.signing import Signer, Signature, DEFAULT_HASH_ALGORITHM - - -@pytest.mark.parametrize('execution_number', range(20)) # Run this test 20 times. -def test_sign_and_verify(execution_number): - privkey = UmbralPrivateKey.gen_key() - pubkey = privkey.get_pubkey() - signer = Signer(private_key=privkey) - message = b"peace at dawn" - signature = signer(message=message) - - # Basic signature verification - assert signature.verify(message, pubkey) - assert not signature.verify(b"another message", pubkey) - another_pubkey = UmbralPrivateKey.gen_key().pubkey - assert not signature.verify(message, another_pubkey) - - # Signature serialization - sig_bytes = bytes(signature) - assert len(sig_bytes) == Signature.expected_bytes_length() - restored_signature = Signature.from_bytes(sig_bytes) - assert restored_signature == signature - assert restored_signature.verify(message, pubkey) - - -@pytest.mark.parametrize('execution_number', range(20)) # Run this test 20 times. -def test_prehashed_message(execution_number): - privkey = UmbralPrivateKey.gen_key() - pubkey = privkey.get_pubkey() - signer = Signer(private_key=privkey) - - message = b"peace at dawn" - hash_function = hashes.Hash(DEFAULT_HASH_ALGORITHM(), backend=backend) - hash_function.update(message) - prehashed_message = hash_function.finalize() - - signature = signer(message=prehashed_message, is_prehashed=True) - - assert signature.verify(message=prehashed_message, - verifying_key=pubkey, - is_prehashed=True) diff --git a/tests/unit/test_umbral_keys.py b/tests/unit/test_umbral_keys.py deleted file mode 100644 index 46b092b5..00000000 --- a/tests/unit/test_umbral_keys.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import base64 -import os -import pytest -import string - -from umbral.config import default_params -from umbral.keys import UmbralPublicKey, UmbralPrivateKey, UmbralKeyingMaterial -from umbral.point import Point - - -def test_gen_key(): - # Pass in the parameters to test that manual param selection works - umbral_priv_key = UmbralPrivateKey.gen_key() - assert type(umbral_priv_key) == UmbralPrivateKey - - umbral_pub_key = umbral_priv_key.get_pubkey() - assert type(umbral_pub_key) == UmbralPublicKey - - -def test_derive_key_from_label(): - umbral_keying_material = UmbralKeyingMaterial() - - label = b"my_healthcare_information" - - priv_key1 = umbral_keying_material.derive_privkey_by_label(label) - assert type(priv_key1) == UmbralPrivateKey - - pub_key1 = priv_key1.get_pubkey() - assert type(pub_key1) == UmbralPublicKey - - # Check that key derivation is reproducible - priv_key2 = umbral_keying_material.derive_privkey_by_label(label) - pub_key2 = priv_key2.get_pubkey() - assert priv_key1.bn_key == priv_key2.bn_key - assert pub_key1 == pub_key2 - - # A salt can be used too, but of course it affects the derived key - salt = b"optional, randomly generated salt" - priv_key3 = umbral_keying_material.derive_privkey_by_label(label, salt=salt) - assert priv_key3.bn_key != priv_key1.bn_key - - # Different labels on the same master secret create different keys - label = b"my_tax_information" - priv_key4 = umbral_keying_material.derive_privkey_by_label(label) - pub_key4 = priv_key4.get_pubkey() - assert priv_key1.bn_key != priv_key4.bn_key - - -def test_private_key_serialization(random_ec_curvebn1): - priv_key = random_ec_curvebn1 - umbral_key = UmbralPrivateKey(priv_key, default_params()) - - encoded_key = umbral_key.to_bytes() - - decoded_key = UmbralPrivateKey.from_bytes(encoded_key) - assert priv_key == decoded_key.bn_key - - -def test_private_key_serialization_with_encryption(random_ec_curvebn1): - priv_key = random_ec_curvebn1 - umbral_key = UmbralPrivateKey(priv_key, default_params()) - - insecure_cost = 15 # This is deliberately insecure, just to make the tests faster - encoded_key = umbral_key.to_bytes(password=b'test', - _scrypt_cost=insecure_cost) - - decoded_key = UmbralPrivateKey.from_bytes(encoded_key, - password=b'test', - _scrypt_cost=insecure_cost) - assert priv_key == decoded_key.bn_key - - -def test_public_key_serialization(random_ec_curvebn1): - umbral_key = UmbralPrivateKey(random_ec_curvebn1, default_params()).get_pubkey() - pub_point = umbral_key.point_key - - encoded_key = umbral_key.to_bytes() - - decoded_key = UmbralPublicKey.from_bytes(encoded_key) - assert pub_point == decoded_key.point_key - - -def test_public_key_to_compressed_bytes(random_ec_curvebn1): - umbral_key = UmbralPrivateKey(random_ec_curvebn1, default_params()).get_pubkey() - key_bytes = bytes(umbral_key) - assert len(key_bytes) == Point.expected_bytes_length(is_compressed=True) - - -def test_public_key_to_uncompressed_bytes(random_ec_curvebn1): - umbral_key = UmbralPrivateKey(random_ec_curvebn1, default_params()).get_pubkey() - key_bytes = umbral_key.to_bytes(is_compressed=False) - assert len(key_bytes) == Point.expected_bytes_length(is_compressed=False) - - -def test_key_encoder_decoder(random_ec_curvebn1): - priv_key = random_ec_curvebn1 - umbral_key = UmbralPrivateKey(priv_key, default_params()) - - encoded_key = umbral_key.to_bytes(encoder=base64.urlsafe_b64encode) - - decoded_key = UmbralPrivateKey.from_bytes(encoded_key, - decoder=base64.urlsafe_b64decode) - assert decoded_key.to_bytes() == umbral_key.to_bytes() - - -def test_public_key_as_hex(random_ec_curvebn1): - pubkey = UmbralPrivateKey(random_ec_curvebn1, default_params()).get_pubkey() - hex_string = pubkey.hex() - - assert set(hex_string).issubset(set(string.hexdigits)) - assert len(hex_string) == 2 * UmbralPublicKey.expected_bytes_length() - - decoded_pubkey = UmbralPublicKey.from_hex(hex_string) - - assert pubkey == decoded_pubkey - - hex_string = pubkey.hex(is_compressed=False) - - assert set(hex_string).issubset(set(string.hexdigits)) - assert len(hex_string) == 2 * UmbralPublicKey.expected_bytes_length(is_compressed=False) - - decoded_pubkey = UmbralPublicKey.from_hex(hex_string) - assert pubkey == decoded_pubkey - - -def test_umbral_key_to_cryptography_keys(): - umbral_priv_key = UmbralPrivateKey.gen_key() - umbral_pub_key = umbral_priv_key.get_pubkey() - - crypto_privkey = umbral_priv_key.to_cryptography_privkey() - assert int(umbral_priv_key.bn_key) == crypto_privkey.private_numbers().private_value - - crypto_pubkey = umbral_pub_key.to_cryptography_pubkey() - umbral_affine = umbral_pub_key.point_key.to_affine() - x, y = crypto_pubkey.public_numbers().x, crypto_pubkey.public_numbers().y - assert umbral_affine == (x, y) - - -def test_keying_material_serialization(): - umbral_keying_material = UmbralKeyingMaterial() - - encoded_keying_material = umbral_keying_material.to_bytes() - - decoded_keying_material = UmbralKeyingMaterial.from_bytes(encoded_keying_material) - - label = os.urandom(32) - privkey_bytes = umbral_keying_material.derive_privkey_by_label(label).to_bytes() - assert privkey_bytes == decoded_keying_material.derive_privkey_by_label(label).to_bytes() - - -def test_keying_material_serialization_with_encryption(): - umbral_keying_material = UmbralKeyingMaterial() - - insecure_cost = 15 # This is deliberately insecure, just to make the tests faster - encoded_keying_material = umbral_keying_material.to_bytes(password=b'test', - _scrypt_cost=insecure_cost) - - decoded_keying_material = UmbralKeyingMaterial.from_bytes(encoded_keying_material, - password=b'test', - _scrypt_cost=insecure_cost) - - label = os.urandom(32) - privkey_bytes = umbral_keying_material.derive_privkey_by_label(label).to_bytes() - assert privkey_bytes == decoded_keying_material.derive_privkey_by_label(label).to_bytes() - - -def test_umbral_public_key_equality(): - umbral_priv_key = UmbralPrivateKey.gen_key() - umbral_pub_key = umbral_priv_key.get_pubkey() - - as_bytes = bytes(umbral_pub_key) - assert umbral_pub_key == as_bytes - - reconstructed = UmbralPublicKey.from_bytes(as_bytes) - assert reconstructed == umbral_pub_key - - assert not umbral_pub_key == b"some whatever bytes" - - another_umbral_priv_key = UmbralPrivateKey.gen_key() - another_umbral_pub_key = another_umbral_priv_key.get_pubkey() - - assert not umbral_pub_key == another_umbral_pub_key - - # Also not equal to a totally disparate type. - assert not umbral_pub_key == 107 - - -def test_umbral_public_key_as_dict_key(): - umbral_priv_key = UmbralPrivateKey.gen_key() - umbral_pub_key = umbral_priv_key.get_pubkey() - - d = {umbral_pub_key: 19} - assert d[umbral_pub_key] == 19 - - another_umbral_priv_key = UmbralPrivateKey.gen_key() - another_umbral_pub_key = another_umbral_priv_key.get_pubkey() - - with pytest.raises(KeyError): - _ = d[another_umbral_pub_key] - - d[another_umbral_pub_key] = False - - assert d[umbral_pub_key] == 19 - d[umbral_pub_key] = 20 - assert d[umbral_pub_key] == 20 - assert d[another_umbral_pub_key] is False diff --git a/umbral/__about__.py b/umbral/__about__.py index 66448cd9..7615ffe1 100644 --- a/umbral/__about__.py +++ b/umbral/__about__.py @@ -1,20 +1,3 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published b -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - from __future__ import absolute_import, division, print_function __all__ = [ diff --git a/umbral/__init__.py b/umbral/__init__.py index 97d7c652..313ba250 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -1,20 +1,3 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published b -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - from umbral.__about__ import ( __author__, __license__, __summary__, __title__, __version__, __copyright__, __email__, __url__ ) diff --git a/umbral/cfrags.py b/umbral/cfrags.py deleted file mode 100644 index 9679bdb5..00000000 --- a/umbral/cfrags.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from typing import Optional, Any - -from bytestring_splitter import BytestringSplitter - -from umbral.config import default_curve -from umbral.curvebn import CurveBN -from umbral.point import Point -from umbral.signing import Signature -from umbral.curve import Curve -from umbral.random_oracles import hash_to_curvebn, ExtendedKeccak - - -class CorrectnessProof: - def __init__(self, point_e2: Point, point_v2: Point, point_kfrag_commitment: Point, - point_kfrag_pok: Point, bn_sig: CurveBN, kfrag_signature: Signature, - metadata: Optional[bytes] = None) -> None: - self.point_e2 = point_e2 - self.point_v2 = point_v2 - self.point_kfrag_commitment = point_kfrag_commitment - self.point_kfrag_pok = point_kfrag_pok - self.bn_sig = bn_sig - self.metadata = metadata - self.kfrag_signature = kfrag_signature - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None): - """ - Returns the size (in bytes) of a CorrectnessProof without the metadata. - If no curve is given, it will use the default curve. - """ - curve = curve if curve is not None else default_curve() - bn_size = CurveBN.expected_bytes_length(curve=curve) - point_size = Point.expected_bytes_length(curve=curve) - - return (bn_size * 3) + (point_size * 4) - - @classmethod - def from_bytes(cls, data: bytes, curve: Optional[Curve] = None) -> 'CorrectnessProof': - """ - Instantiate CorrectnessProof from serialized data. - """ - curve = curve if curve is not None else default_curve() - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - arguments = {'curve': curve} - splitter = BytestringSplitter( - (Point, point_size, arguments), # point_e2 - (Point, point_size, arguments), # point_v2 - (Point, point_size, arguments), # point_kfrag_commitment - (Point, point_size, arguments), # point_kfrag_pok - (CurveBN, bn_size, arguments), # bn_sig - (Signature, Signature.expected_bytes_length(curve), arguments), # kfrag_signature - ) - components = splitter(data, return_remainder=True) - components.append(components.pop() or None) - - return cls(*components) - - def to_bytes(self) -> bytes: - """ - Serialize the CorrectnessProof to a bytestring. - """ - e2 = self.point_e2.to_bytes() - v2 = self.point_v2.to_bytes() - kfrag_commitment = self.point_kfrag_commitment.to_bytes() - kfrag_pok = self.point_kfrag_pok.to_bytes() - - result = e2 \ - + v2 \ - + kfrag_commitment \ - + kfrag_pok \ - + self.bn_sig.to_bytes() \ - + self.kfrag_signature - - result += self.metadata or b'' - - return result - - def __bytes__(self): - return self.to_bytes() - - -class CapsuleFrag: - def __init__(self, - point_e1: Point, - point_v1: Point, - kfrag_id: bytes, - point_precursor: Point, - proof: Optional[CorrectnessProof] = None) -> None: - self.point_e1 = point_e1 - self.point_v1 = point_v1 - self.kfrag_id = kfrag_id - self.point_precursor = point_precursor - self.proof = proof - - class NoProofProvided(TypeError): - """ - Raised when a cfrag is assessed for correctness, but no proof is attached. - """ - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None) -> int: - """ - Returns the size (in bytes) of a CapsuleFrag given the curve without - the CorrectnessProof. - If no curve is provided, it will use the default curve. - """ - curve = curve if curve is not None else default_curve() - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - - return (bn_size * 1) + (point_size * 3) - - @classmethod - def from_bytes(cls, data: bytes, curve: Optional[Curve] = None) -> 'CapsuleFrag': - """ - Instantiates a CapsuleFrag object from the serialized data. - """ - curve = curve if curve is not None else default_curve() - - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - arguments = {'curve': curve} - - splitter = BytestringSplitter( - (Point, point_size, arguments), # point_e1 - (Point, point_size, arguments), # point_v1 - bn_size, # kfrag_id - (Point, point_size, arguments), # point_precursor - ) - components = splitter(data, return_remainder=True) - - proof = components.pop() or None - components.append(CorrectnessProof.from_bytes(proof, curve) if proof else None) - - return cls(*components) - - def to_bytes(self) -> bytes: - """ - Serialize the CapsuleFrag into a bytestring. - """ - e1 = self.point_e1.to_bytes() - v1 = self.point_v1.to_bytes() - precursor = self.point_precursor.to_bytes() - - serialized_cfrag = e1 + v1 + self.kfrag_id + precursor - - if self.proof is not None: - serialized_cfrag += self.proof.to_bytes() - - return serialized_cfrag - - def prove_correctness(self, - capsule, - kfrag, - metadata: Optional[bytes] = None): - - params = capsule.params - - # Check correctness of original ciphertext - if not capsule.verify(): - raise capsule.NotValid("Capsule verification failed.") - - rk = kfrag.bn_key - t = CurveBN.gen_rand(params.curve) - #### - # Here are the formulaic constituents shared with `verify_correctness`. - #### - e = capsule.point_e - v = capsule.point_v - - e1 = self.point_e1 - v1 = self.point_v1 - - u = params.u - u1 = kfrag.point_commitment - - e2 = t * e # type: Any - v2 = t * v # type: Any - u2 = t * u # type: Any - - hash_input = [e, e1, e2, v, v1, v2, u, u1, u2] - if metadata is not None: - hash_input.append(metadata) - - h = hash_to_curvebn(*hash_input, params=params, hash_class=ExtendedKeccak) - ######## - - z3 = t + h * rk - - self.attach_proof(e2, v2, u1, u2, metadata=metadata, z3=z3, kfrag_signature=kfrag.signature_for_bob) - - def verify_correctness(self, capsule) -> bool: - if self.proof is None: - raise CapsuleFrag.NoProofProvided - - correctness_keys = capsule.get_correctness_keys() - - delegating_pubkey = correctness_keys['delegating'] - signing_pubkey = correctness_keys['verifying'] - receiving_pubkey = correctness_keys['receiving'] - - params = capsule.params - - #### - # Here are the formulaic constituents shared with `prove_correctness`. - #### - e = capsule.point_e - v = capsule.point_v - - e1 = self.point_e1 - v1 = self.point_v1 - - u = params.u - u1 = self.proof.point_kfrag_commitment - - e2 = self.proof.point_e2 - v2 = self.proof.point_v2 - u2 = self.proof.point_kfrag_pok - - hash_input = [e, e1, e2, v, v1, v2, u, u1, u2] - if self.proof.metadata is not None: - hash_input.append(self.proof.metadata) - - h = hash_to_curvebn(*hash_input, params=params, hash_class=ExtendedKeccak) - ######## - - precursor = self.point_precursor - kfrag_id = self.kfrag_id - - validity_input = (kfrag_id, delegating_pubkey, receiving_pubkey, u1, precursor) - - kfrag_validity_message = bytes().join(bytes(item) for item in validity_input) - valid_kfrag_signature = self.proof.kfrag_signature.verify(kfrag_validity_message, signing_pubkey) - - z3 = self.proof.bn_sig - correct_reencryption_of_e = z3 * e == e2 + (h * e1) - - correct_reencryption_of_v = z3 * v == v2 + (h * v1) - - correct_rk_commitment = z3 * u == u2 + (h * u1) - - return valid_kfrag_signature \ - & correct_reencryption_of_e \ - & correct_reencryption_of_v \ - & correct_rk_commitment - - def attach_proof(self, - e2: Point, - v2: Point, - u1: Point, - u2: Point, - z3: CurveBN, - kfrag_signature: Signature, - metadata: Optional[bytes]) -> None: - - self.proof = CorrectnessProof(point_e2=e2, - point_v2=v2, - point_kfrag_commitment=u1, - point_kfrag_pok=u2, - bn_sig=z3, - kfrag_signature=kfrag_signature, - metadata=metadata, - ) - - def __bytes__(self) -> bytes: - return self.to_bytes() - - def __repr__(self): - return "CFrag:{}".format(self.point_e1.to_bytes().hex()[2:17]) diff --git a/umbral/config.py b/umbral/config.py deleted file mode 100644 index 9e4a17c8..00000000 --- a/umbral/config.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from typing import Optional, Type -from warnings import warn - -from umbral.curve import Curve, SECP256K1 -from umbral.params import UmbralParameters - - -class _CONFIG: - __curve = None - __params = None - __CURVE_TO_USE_IF_NO_DEFAULT_IS_SET_BY_USER = SECP256K1 - __WARNING_IF_NO_DEFAULT_SET = "No default curve has been set. " \ - "Using SECP256K1. " \ - "A slight performance penalty has been " \ - "incurred for only this call. Set a default " \ - "curve with umbral.config.set_default_curve()." - - class UmbralConfigurationError(RuntimeError): - """Raised when somebody does something dumb re: configuration.""" - - @classmethod - def __set_curve_by_default(cls): - warn(cls.__WARNING_IF_NO_DEFAULT_SET, RuntimeWarning) - cls.set_curve(cls.__CURVE_TO_USE_IF_NO_DEFAULT_IS_SET_BY_USER) - - @classmethod - def params(cls) -> UmbralParameters: - if not cls.__params: - cls.__set_curve_by_default() - return cls.__params # type: ignore - - @classmethod - def curve(cls) -> Curve: - if not cls.__curve: - cls.__set_curve_by_default() - return cls.__curve # type: ignore - - @classmethod - def set_curve(cls, curve: Optional[Curve] = None) -> None: - if cls.__curve: - raise cls.UmbralConfigurationError( - "You can only set the default curve once. Do it once and then leave it alone.") - else: - from umbral.params import UmbralParameters - if curve is None: - curve = _CONFIG.__CURVE_TO_USE_IF_NO_DEFAULT_IS_SET_BY_USER - cls.__curve = curve - cls.__params = UmbralParameters(curve) - - -def set_default_curve(curve: Optional[Curve] = None) -> None: - return _CONFIG.set_curve(curve) - - -def default_curve() -> Curve: - return _CONFIG.curve() - - -def default_params() -> UmbralParameters: - return _CONFIG.params() diff --git a/umbral/curve.py b/umbral/curve.py deleted file mode 100644 index 7185e166..00000000 --- a/umbral/curve.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from cryptography.hazmat.backends import default_backend - -from umbral import openssl - - -class Curve: - """ - Acts as a container to store constant variables such as the OpenSSL - curve_nid, the EC_GROUP struct, and the order of the curve. - - Contains a whitelist of supported elliptic curves used in pyUmbral. - - """ - - _supported_curves = { - 415: 'secp256r1', - 714: 'secp256k1', - 715: 'secp384r1' - } - - def __init__(self, nid: int) -> None: - """ - Instantiates an OpenSSL curve with the provided curve_nid and derives - the proper EC_GROUP struct and order. You can _only_ instantiate curves - with supported nids (see `Curve.supported_curves`). - """ - - try: - self.__curve_name = self._supported_curves[nid] - except KeyError: - raise NotImplementedError("Curve NID {} is not supported.".format(nid)) - - # set only once - self.__curve_nid = nid - self.__ec_group = openssl._get_ec_group_by_curve_nid(self.__curve_nid) - self.__order = openssl._get_ec_order_by_group(self.ec_group) - self.__generator = openssl._get_ec_generator_by_group(self.ec_group) - - # Init cache - self.__field_order_size_in_bytes = 0 - self.__group_order_size_in_bytes = 0 - - @classmethod - def from_name(cls, name: str) -> 'Curve': - """ - Alternate constructor to generate a curve instance by its name. - - Raises NotImplementedError if the name cannot be mapped to a known - supported curve NID. - - """ - - name = name.casefold() # normalize - - for supported_nid, supported_name in cls._supported_curves.items(): - if name == supported_name: - instance = cls(nid=supported_nid) - break - else: - message = "{} is not supported curve name.".format(name) - raise NotImplementedError(message) - - return instance - - def __eq__(self, other): - return self.__curve_nid == other.curve_nid - - def __repr__(self): - return "".format(self.__curve_nid, self.__curve_name) - - # - # Immutable Curve Data - # - - @property - def field_order_size_in_bytes(self) -> int: - if not self.__field_order_size_in_bytes: - size_in_bits = openssl._get_ec_group_degree(self.__ec_group) - self.__field_order_size_in_bytes = (size_in_bits + 7) // 8 - return self.__field_order_size_in_bytes - - @property - def group_order_size_in_bytes(self) -> int: - if not self.__group_order_size_in_bytes: - BN_num_bytes = default_backend()._lib.BN_num_bytes - self.__group_order_size_in_bytes = BN_num_bytes(self.order) - return self.__group_order_size_in_bytes - - @property - def curve_nid(self) -> int: - return self.__curve_nid - - @property - def name(self) -> str: - return self.__curve_name - - @property - def ec_group(self): - return self.__ec_group - - @property - def order(self): - return self.__order - - @property - def generator(self): - return self.__generator - - -# -# Global Curve Instances -# - -SECP256R1 = Curve.from_name('secp256r1') -SECP256K1 = Curve.from_name('secp256k1') -SECP384R1 = Curve.from_name('secp384r1') - -CURVES = (SECP256K1, SECP256R1, SECP384R1) diff --git a/umbral/curvebn.py b/umbral/curvebn.py deleted file mode 100644 index 1ab85670..00000000 --- a/umbral/curvebn.py +++ /dev/null @@ -1,272 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from typing import Optional, Union, cast - -from cryptography.hazmat.backends.openssl import backend - -from umbral import openssl -from umbral.config import default_curve -from umbral.curve import Curve - - -class CurveBN: - """ - Represents an OpenSSL Bignum modulo the order of a curve. Some of these - operations will only work with prime numbers - By default, the underlying OpenSSL BIGNUM has BN_FLG_CONSTTIME set for - constant time operations. - """ - - def __init__(self, bignum, curve: Curve) -> None: - on_curve = openssl._bn_is_on_curve(bignum, curve) - if not on_curve: - raise ValueError("The provided BIGNUM is not on the provided curve.") - - self.bignum = bignum - self.curve = curve - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None) -> int: - """ - Returns the size (in bytes) of a CurveBN given the curve, - which comes from the size of the order of the generated group. - If no curve is provided, it uses the default. - """ - curve = curve if curve is not None else default_curve() - return curve.group_order_size_in_bytes - - @classmethod - def gen_rand(cls, curve: Optional[Curve] = None) -> 'CurveBN': - """ - Returns a CurveBN object with a cryptographically secure OpenSSL BIGNUM - based on the given curve. - By default, the underlying OpenSSL BIGNUM has BN_FLG_CONSTTIME set for - constant time operations. - """ - curve = curve if curve is not None else default_curve() - - new_rand_bn = openssl._get_new_BN() - rand_res = backend._lib.BN_rand_range(new_rand_bn, curve.order) - backend.openssl_assert(rand_res == 1) - - if not openssl._bn_is_on_curve(new_rand_bn, curve): - new_rand_bn = cls.gen_rand(curve=curve) - return new_rand_bn - - return cls(new_rand_bn, curve) - - @classmethod - def from_int(cls, num: int, curve: Optional[Curve] = None) -> 'CurveBN': - """ - Returns a CurveBN object from a given integer on a curve. - By default, the underlying OpenSSL BIGNUM has BN_FLG_CONSTTIME set for - constant time operations. - """ - curve = curve if curve is not None else default_curve() - conv_bn = openssl._int_to_bn(num, curve) - return cls(conv_bn, curve) - - @classmethod - def from_bytes(cls, data: bytes, curve: Optional[Curve] = None) -> 'CurveBN': - """ - Returns a CurveBN object from the given byte data that's within the size - of the provided curve's order. - By default, the underlying OpenSSL BIGNUM has BN_FLG_CONSTTIME set for - constant time operations. - """ - curve = curve if curve is not None else default_curve() - - size = backend._lib.BN_num_bytes(curve.order) - if len(data) != size: - raise ValueError("Expected {} B for CurveBNs".format(size)) - bignum = openssl._bytes_to_bn(data) - return cls(bignum, curve) - - def to_bytes(self) -> bytes: - """ - Returns the CurveBN as bytes. - """ - size = backend._lib.BN_num_bytes(self.curve.order) - return openssl._bn_to_bytes(self.bignum, size) - - def __int__(self) -> int: - """ - Converts the CurveBN to a Python int. - """ - return backend._bn_to_int(self.bignum) - - def __eq__(self, other) -> bool: - """ - Compares the two BIGNUMS or int. - """ - # TODO: Should this stay in or not? - if type(other) == int: - other = openssl._int_to_bn(other) - other = CurveBN(other, self.curve) - - # -1 less than, 0 is equal to, 1 is greater than - return not bool(backend._lib.BN_cmp(self.bignum, other.bignum)) - - def __pow__(self, other: Union[int, 'CurveBN']) -> 'CurveBN': - """ - Performs a BN_mod_exp on two BIGNUMS. - - WARNING: Only in constant time if BN_FLG_CONSTTIME is set on the BN. - """ - # TODO: Should this stay in or not? - if type(other) == int: - other = openssl._int_to_bn(other) - other = CurveBN(other, self.curve) - - other = cast('CurveBN', other) # This is just for mypy - - power = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx, openssl._tmp_bn_mont_ctx(self.curve.order) as bn_mont_ctx: - res = backend._lib.BN_mod_exp_mont( - power, self.bignum, other.bignum, self.curve.order, bn_ctx, bn_mont_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(power, self.curve) - - def __mul__(self, other) -> 'CurveBN': - """ - Performs a BN_mod_mul between two BIGNUMS. - """ - if type(other) != CurveBN: - return NotImplemented - - product = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_mul( - product, self.bignum, other.bignum, self.curve.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(product, self.curve) - - def __truediv__(self, other: 'CurveBN') -> 'CurveBN': - """ - Performs a BN_div on two BIGNUMs (modulo the order of the curve). - - WARNING: Only in constant time if BN_FLG_CONSTTIME is set on the BN. - """ - product = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - inv_other = backend._lib.BN_mod_inverse( - backend._ffi.NULL, other.bignum, self.curve.order, bn_ctx - ) - backend.openssl_assert(inv_other != backend._ffi.NULL) - inv_other = backend._ffi.gc(inv_other, backend._lib.BN_clear_free) - - res = backend._lib.BN_mod_mul( - product, self.bignum, inv_other, self.curve.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(product, self.curve) - - def __add__(self, other : Union[int, 'CurveBN']) -> 'CurveBN': - """ - Performs a BN_mod_add on two BIGNUMs. - """ - if type(other) == int: - other = openssl._int_to_bn(other) - other = CurveBN(other, self.curve) - - other = cast('CurveBN', other) # This is just for mypy - - op_sum = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_add( - op_sum, self.bignum, other.bignum, self.curve.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(op_sum, self.curve) - - def __sub__(self, other : Union[int, 'CurveBN']) -> 'CurveBN': - """ - Performs a BN_mod_sub on two BIGNUMS. - """ - if type(other) == int: - other = openssl._int_to_bn(other) - other = CurveBN(other, self.curve) - - other = cast('CurveBN', other) # This is just for mypy - - diff = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_sub( - diff, self.bignum, other.bignum, self.curve.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(diff, self.curve) - - def __invert__(self) -> 'CurveBN': - """ - Performs a BN_mod_inverse. - - WARNING: Only in constant time if BN_FLG_CONSTTIME is set on the BN. - - """ - with backend._tmp_bn_ctx() as bn_ctx: - inv = backend._lib.BN_mod_inverse( - backend._ffi.NULL, self.bignum, self.curve.order, bn_ctx - ) - backend.openssl_assert(inv != backend._ffi.NULL) - inv = backend._ffi.gc(inv, backend._lib.BN_clear_free) - - return CurveBN(inv, self.curve) - - def __neg__(self) -> 'CurveBN': - """ - Computes the modular opposite (i.e., additive inverse) of a BIGNUM - - """ - zero = backend._int_to_bn(0) - zero = backend._ffi.gc(zero, backend._lib.BN_clear_free) - - the_opposite = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_sub( - the_opposite, zero, self.bignum, self.curve.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(the_opposite, self.curve) - - def __mod__(self, other: Union[int, 'CurveBN']) -> 'CurveBN': - """ - Performs a BN_nnmod on two BIGNUMS. - """ - if type(other) == int: - other = openssl._int_to_bn(other) - other = CurveBN(other, self.curve) - - other = cast('CurveBN', other) # This is just for mypy - - rem = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_nnmod( - rem, self.bignum, other.bignum, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveBN(rem, self.curve) diff --git a/umbral/dem.py b/umbral/dem.py deleted file mode 100644 index b372a82d..00000000 --- a/umbral/dem.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import os -from typing import Optional - -from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 - - -DEM_KEYSIZE = 32 -DEM_NONCE_SIZE = 12 - - -class UmbralDEM: - def __init__(self, symm_key: bytes) -> None: - """ - Initializes an UmbralDEM object. Requires a key to perform - ChaCha20-Poly1305. - """ - if len(symm_key) != DEM_KEYSIZE: - raise ValueError( - "Invalid key size, must be {} bytes".format(DEM_KEYSIZE) - ) - - self.cipher = ChaCha20Poly1305(symm_key) - - def encrypt(self, data: bytes, authenticated_data: Optional[bytes] = None) -> bytes: - """ - Encrypts data using ChaCha20-Poly1305 with optional authenticated data. - """ - nonce = os.urandom(DEM_NONCE_SIZE) - enc_data = self.cipher.encrypt(nonce, data, authenticated_data) - # Ciphertext will be a 12 byte nonce, the ciphertext, and a 16 byte tag. - return nonce + enc_data - - def decrypt(self, ciphertext: bytes, authenticated_data: Optional[bytes] = None) -> bytes: - """ - Decrypts data using ChaCha20-Poly1305 and validates the provided - authenticated data. - """ - nonce = ciphertext[:DEM_NONCE_SIZE] - ciphertext = ciphertext[DEM_NONCE_SIZE:] - cleartext = self.cipher.decrypt(nonce, ciphertext, authenticated_data) - return cleartext diff --git a/umbral/keys.py b/umbral/keys.py deleted file mode 100644 index b012e166..00000000 --- a/umbral/keys.py +++ /dev/null @@ -1,472 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import os -from typing import Callable, Optional, Any - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey, _EllipticCurvePublicKey -from cryptography.exceptions import InternalError -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives.kdf.scrypt import Scrypt as CryptographyScrypt -from nacl.secret import SecretBox - -from umbral import openssl -from umbral.config import default_params -from umbral.curvebn import CurveBN -from umbral.params import UmbralParameters -from umbral.point import Point -from umbral.curve import Curve -from umbral.random_oracles import hash_to_curvebn - - -__SALT_SIZE = 32 - - -class Scrypt: - __DEFAULT_SCRYPT_COST = 20 - - def __call__(self, - password: bytes, - salt: bytes, - **kwargs) -> bytes: - """ - Derives a symmetric encryption key from a pair of password and salt. - It also accepts an additional _scrypt_cost argument. - WARNING: RFC7914 recommends that you use a 2^20 cost value for sensitive - files. It is NOT recommended to change the `_scrypt_cost` value unless - you know what you are doing. - :param password: byte-encoded password used to derive a symmetric key - :param salt: cryptographic salt added during key derivation - :return: - """ - - _scrypt_cost = kwargs.get('_scrypt_cost', Scrypt.__DEFAULT_SCRYPT_COST) - try: - derived_key = CryptographyScrypt( - salt=salt, - length=SecretBox.KEY_SIZE, - n=2 ** _scrypt_cost, - r=8, - p=1, - backend=default_backend() - ).derive(password) - except InternalError as e: - required_memory = 128 * 2**_scrypt_cost * 8 // (10**6) - if e.err_code[0].reason == 65: - raise MemoryError( - "Scrypt key derivation requires at least {} MB of memory. " - "Please free up some memory and try again.".format(required_memory) - ) - else: - raise e - else: - return derived_key - - -def derive_key_from_password(password: bytes, - salt: bytes, - **kwargs) -> bytes: - """ - Derives a symmetric encryption key from a pair of password and salt. - It uses Scrypt by default. - """ - kdf = kwargs.get('kdf', Scrypt)() - derived_key = kdf(password, salt, **kwargs) - return derived_key - - -def wrap_key(key_to_wrap: bytes, - wrapping_key: Optional[bytes] = None, - password: Optional[bytes] = None, - **kwargs) -> bytes: - """ - Wraps a key using a provided wrapping key. Alternatively, it can derive - the wrapping key from a password. - :param key_to_wrap: - :param wrapping_key: - :param password: - :return: - """ - if not(bool(password) ^ bool(wrapping_key)): - raise ValueError("Either password or wrapping_key must be passed") - - wrapped_key = b'' - if password: - salt = os.urandom(__SALT_SIZE) - wrapping_key = derive_key_from_password(password=password, - salt=salt, - **kwargs) - wrapped_key = salt - - wrapped_key += SecretBox(wrapping_key).encrypt(key_to_wrap) - return wrapped_key - - -def unwrap_key(wrapped_key: bytes, - wrapping_key: Optional[bytes] = None, - password: Optional[bytes] = None, - **kwargs) -> bytes: - """ - Unwraps a key using a provided wrapping key. Alternatively, it can derive - the wrapping key from a password. - :param wrapped_key: - :param wrapping_key: - :param password: - :return: - """ - if all((password, wrapping_key)) or not any((password, wrapping_key)): - raise ValueError("Either password or wrapping_key must be passed") - - if password: - salt = wrapped_key[:__SALT_SIZE] - wrapped_key = wrapped_key[__SALT_SIZE:] - wrapping_key = derive_key_from_password(password=password, - salt=salt, - **kwargs) - - key = SecretBox(wrapping_key).decrypt(wrapped_key) - return key - - -class UmbralPrivateKey: - def __init__(self, bn_key: CurveBN, params: UmbralParameters) -> None: - """ - Initializes an Umbral private key. - """ - self.params = params - self.bn_key = bn_key - self.pubkey = UmbralPublicKey(self.bn_key * params.g, params=params) # type: ignore - - @classmethod - def gen_key(cls, params: Optional[UmbralParameters] = None) -> 'UmbralPrivateKey': - """ - Generates a private key and returns it. - """ - if params is None: - params = default_params() - - bn_key = CurveBN.gen_rand(params.curve) - return cls(bn_key, params) - - @classmethod - def from_bytes(cls, - key_bytes: bytes, - wrapping_key: Optional[bytes] = None, - password: Optional[bytes] = None, - params: Optional[UmbralParameters] = None, - decoder: Optional[Callable] = None, - **kwargs) -> 'UmbralPrivateKey': - """ - Loads an Umbral private key from bytes. - Optionally, allows a decoder function to be passed as a param to decode - the data provided before converting to an Umbral key. - Optionally, uses a wrapping key to unwrap an encrypted Umbral private key. - Alternatively, if a password is provided it will derive the wrapping key - from it. - """ - if params is None: - params = default_params() - - if decoder: - key_bytes = decoder(key_bytes) - - if any((wrapping_key, password)): - key_bytes = unwrap_key(wrapped_key=key_bytes, - wrapping_key=wrapping_key, - password=password, - **kwargs) - - bn_key = CurveBN.from_bytes(key_bytes, params.curve) - return cls(bn_key, params) - - def to_bytes(self, - wrapping_key: Optional[bytes] = None, - password: Optional[bytes] = None, - encoder: Optional[Callable] = None, - **kwargs) -> bytes: - """ - Returns an UmbralPrivateKey as bytes with optional symmetric - encryption via nacl's Salsa20-Poly1305. - If a password is provided instead of a wrapping key, it will use - Scrypt for key derivation. - Optionally, allows an encoder to be passed in as a param to encode the - data before returning it. - """ - - key_bytes = self.bn_key.to_bytes() - - if wrapping_key or password: - key_bytes = wrap_key(key_to_wrap=key_bytes, - wrapping_key=wrapping_key, - password=password, - **kwargs) - - if encoder: - key_bytes = encoder(key_bytes) - - return key_bytes - - def get_pubkey(self) -> 'UmbralPublicKey': - """ - Calculates and returns the public key of the private key. - """ - return self.pubkey - - def to_cryptography_privkey(self) -> _EllipticCurvePrivateKey: - """ - Returns a cryptography.io EllipticCurvePrivateKey from the Umbral key. - """ - backend = default_backend() - - backend.openssl_assert(self.bn_key.curve.ec_group != backend._ffi.NULL) - backend.openssl_assert(self.bn_key.bignum != backend._ffi.NULL) - - ec_key = backend._lib.EC_KEY_new() - backend.openssl_assert(ec_key != backend._ffi.NULL) - ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) - - set_group_result = backend._lib.EC_KEY_set_group( - ec_key, self.bn_key.curve.ec_group - ) - backend.openssl_assert(set_group_result == 1) - - set_privkey_result = backend._lib.EC_KEY_set_private_key( - ec_key, self.bn_key.bignum - ) - backend.openssl_assert(set_privkey_result == 1) - - # Get public key - point = openssl._get_new_EC_POINT(self.params.curve) - with backend._tmp_bn_ctx() as bn_ctx: - mult_result = backend._lib.EC_POINT_mul( - self.bn_key.curve.ec_group, point, self.bn_key.bignum, - backend._ffi.NULL, backend._ffi.NULL, bn_ctx - ) - backend.openssl_assert(mult_result == 1) - - set_pubkey_result = backend._lib.EC_KEY_set_public_key(ec_key, point) - backend.openssl_assert(set_pubkey_result == 1) - - evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) - return _EllipticCurvePrivateKey(backend, ec_key, evp_pkey) - - -class UmbralPublicKey: - def __init__(self, point_key: Point, params: UmbralParameters) -> None: - """ - Initializes an Umbral public key. - """ - self.params = params - - if not isinstance(point_key, Point): - raise TypeError("point_key can only be a Point. Don't pass anything else.") - - self.point_key = point_key - - @classmethod - def from_bytes(cls, - key_bytes: bytes, - params: Optional[UmbralParameters] = None, - decoder: Optional[Callable] = None) -> 'UmbralPublicKey': - """ - Loads an Umbral public key from bytes. - Optionally, if an decoder function is provided it will be used to decode - the data before returning it as an Umbral key. - """ - if params is None: - params = default_params() - - if decoder: - key_bytes = decoder(key_bytes) - - point_key = Point.from_bytes(key_bytes, params.curve) - return cls(point_key, params) - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None, - is_compressed: bool = True) -> int: - """ - Returns the size (in bytes) of an UmbralPublicKey given a curve. - If no curve is provided, it uses the default curve. - By default, it assumes compressed representation (is_compressed = True). - """ - return Point.expected_bytes_length(curve=curve, is_compressed=is_compressed) - - def to_bytes(self, encoder: Callable = None, is_compressed: bool = True) -> bytes: - """ - Returns an Umbral public key as bytes. - Optionally, if an encoder function is provided it will be used to encode - the data before returning it. - """ - umbral_pubkey = self.point_key.to_bytes(is_compressed=is_compressed) - - if encoder: - umbral_pubkey = encoder(umbral_pubkey) - - return umbral_pubkey - - def hex(self, is_compressed: bool = True) -> str: - """ - Returns an Umbral public key as hex string. - """ - return self.to_bytes(is_compressed=is_compressed).hex() - - @classmethod - def from_hex(cls, hex_string) -> 'UmbralPublicKey': - return cls.from_bytes(key_bytes=hex_string, decoder=bytes.fromhex) - - def to_cryptography_pubkey(self) -> _EllipticCurvePublicKey: - """ - Returns a cryptography.io EllipticCurvePublicKey from the Umbral key. - """ - backend = default_backend() - - backend.openssl_assert(self.point_key.curve.ec_group != backend._ffi.NULL) - backend.openssl_assert(self.point_key.ec_point != backend._ffi.NULL) - - ec_key = backend._lib.EC_KEY_new() - backend.openssl_assert(ec_key != backend._ffi.NULL) - ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) - - set_group_result = backend._lib.EC_KEY_set_group( - ec_key, self.point_key.curve.ec_group - ) - backend.openssl_assert(set_group_result == 1) - - set_pubkey_result = backend._lib.EC_KEY_set_public_key( - ec_key, self.point_key.ec_point - ) - backend.openssl_assert(set_pubkey_result == 1) - - evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) - return _EllipticCurvePublicKey(backend, ec_key, evp_pkey) - - def __bytes__(self) -> bytes: - """ - Returns an Umbral Public key as a bytestring. - """ - return self.point_key.to_bytes() - - def __repr__(self): - return "{}:{}".format(self.__class__.__name__, self.point_key.to_bytes().hex()[:15]) - - def __eq__(self, other: Any) -> bool: - if type(other) == bytes: - is_eq = bytes(other) == bytes(self) - elif hasattr(other, "point_key") and hasattr(other, "params"): - is_eq = (self.point_key, self.params) == (other.point_key, other.params) - else: - is_eq = False - return is_eq - - def __hash__(self) -> int: - return int.from_bytes(self.to_bytes(), byteorder="big") - - -class UmbralKeyingMaterial: - """ - This class handles keying material for Umbral, by allowing deterministic - derivation of UmbralPrivateKeys based on labels. - Don't use this key material directly as a key. - """ - - def __init__(self, keying_material: Optional[bytes] = None) -> None: - """ - Initializes an UmbralKeyingMaterial. - """ - if keying_material: - if len(keying_material) < 32: - raise ValueError("UmbralKeyingMaterial must have size at least 32 bytes.") - self.__keying_material = keying_material - else: - self.__keying_material = os.urandom(64) - - def derive_privkey_by_label(self, - label: bytes, - salt: Optional[bytes] = None, - params: Optional[UmbralParameters] = None) -> UmbralPrivateKey: - """ - Derives an UmbralPrivateKey using a KDF from this instance of - UmbralKeyingMaterial, a label, and an optional salt. - """ - params = params if params is not None else default_params() - - key_material = HKDF( - algorithm=hashes.BLAKE2b(64), - length=64, - salt=salt, - info=b"NuCypher/KeyDerivation/"+label, - backend=default_backend() - ).derive(self.__keying_material) - - bn_key = hash_to_curvebn(key_material, params=params) - return UmbralPrivateKey(bn_key, params) - - @classmethod - def from_bytes(cls, - key_bytes: bytes, - wrapping_key: Optional[bytes] = None, - password: Optional[bytes] = None, - decoder: Optional[Callable] = None, - **kwargs) -> 'UmbralKeyingMaterial': - """ - Loads an UmbralKeyingMaterial from bytes. - Optionally, allows a decoder function to be passed as a param to decode - the data provided before converting to an Umbral key. - Optionally, uses a wrapping key to unwrap an encrypted UmbralKeyingMaterial. - Alternatively, if a password is provided it will derive the wrapping key - from it. - """ - if decoder: - key_bytes = decoder(key_bytes) - - if any((password, wrapping_key)): - key_bytes = unwrap_key(wrapped_key=key_bytes, - wrapping_key=wrapping_key, - password=password, - **kwargs) - - return cls(keying_material=key_bytes) - - def to_bytes(self, - wrapping_key: Optional[bytes] = None, - password: Optional[bytes] = None, - encoder: Optional[Callable] = None, - **kwargs) -> bytes: - """ - Returns an UmbralKeyingMaterial as bytes with optional symmetric - encryption via nacl's Salsa20-Poly1305. - If a password is provided instead of a wrapping key, it will use - Scrypt for key derivation. - Optionally, allows an encoder to be passed in as a param to encode the - data before returning it. - """ - - key_bytes = self.__keying_material - - if any((password, wrapping_key)): - key_bytes = wrap_key(key_to_wrap=key_bytes, - wrapping_key=wrapping_key, - password=password, - **kwargs) - - if encoder: - key_bytes = encoder(key_bytes) - - return key_bytes diff --git a/umbral/kfrags.py b/umbral/kfrags.py deleted file mode 100644 index f468978a..00000000 --- a/umbral/kfrags.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import hmac -from typing import Optional - -from bytestring_splitter import BytestringSplitter - -from umbral.config import default_curve, default_params -from umbral.curvebn import CurveBN -from umbral.keys import UmbralPublicKey -from umbral.point import Point -from umbral.signing import Signature -from umbral.params import UmbralParameters -from umbral.curve import Curve - -NO_KEY = b'\x00' -DELEGATING_ONLY = b'\x01' -RECEIVING_ONLY = b'\x02' -DELEGATING_AND_RECEIVING = b'\x03' - - -class KFrag: - - def __init__(self, - identifier: bytes, - bn_key: CurveBN, - point_commitment: Point, - point_precursor: Point, - signature_for_proxy: Signature, - signature_for_bob: Signature, - keys_in_signature=DELEGATING_AND_RECEIVING, - ) -> None: - self.id = identifier - self.bn_key = bn_key - self.point_commitment = point_commitment - self.point_precursor = point_precursor - self.signature_for_proxy = signature_for_proxy - self.signature_for_bob = signature_for_bob - self.keys_in_signature = keys_in_signature - - class NotValid(ValueError): - """ - raised if the KFrag does not pass verification. - """ - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None) -> int: - """ - Returns the size (in bytes) of a KFrag given the curve. - If no curve is provided, it will use the default curve. - """ - curve = curve if curve is not None else default_curve() - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - - # self.id --> 1 bn_size - # self.bn_key --> 1 bn_size - # self.point_commitment --> 1 point_size - # self.point_precursor --> 1 point_size - # self.signature_for_proxy --> 2 bn_size - # self.signature_for_bob --> 2 bn_size - # self.keys_in_signature --> 1 - - return bn_size * 6 + point_size * 2 + 1 - - @classmethod - def from_bytes(cls, data: bytes, curve: Optional[Curve] = None) -> 'KFrag': - """ - Instantiate a KFrag object from the serialized data. - """ - curve = curve if curve is not None else default_curve() - - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - signature_size = Signature.expected_bytes_length(curve) - arguments = {'curve': curve} - - splitter = BytestringSplitter( - bn_size, # id - (CurveBN, bn_size, arguments), # bn_key - (Point, point_size, arguments), # point_commitment - (Point, point_size, arguments), # point_precursor - 1, # keys_in_signature - (Signature, signature_size, arguments), # signature_for_proxy - (Signature, signature_size, arguments), # signature_for_bob - ) - components = splitter(data) - - return cls(identifier=components[0], - bn_key=components[1], - point_commitment=components[2], - point_precursor=components[3], - keys_in_signature=components[4], - signature_for_proxy=components[5], - signature_for_bob=components[6]) - - def to_bytes(self) -> bytes: - """ - Serialize the KFrag into a bytestring. - """ - key = self.bn_key.to_bytes() - commitment = self.point_commitment.to_bytes() - precursor = self.point_precursor.to_bytes() - signature_for_proxy = bytes(self.signature_for_proxy) - signature_for_bob = bytes(self.signature_for_bob) - mode = bytes(self.keys_in_signature) - - return self.id + key + commitment + precursor \ - + mode + signature_for_proxy + signature_for_bob - - def verify(self, - signing_pubkey: UmbralPublicKey, - delegating_pubkey: Optional[UmbralPublicKey] = None, - receiving_pubkey: Optional[UmbralPublicKey] = None, - params: Optional[UmbralParameters] = None, - ) -> bool: - if params is None: - params = default_params() - - if signing_pubkey is None: - raise ValueError("The verifying pubkey is required to verify this KFrag.") - - if self.delegating_key_in_signature(): - if delegating_pubkey is None: - raise ValueError("The delegating pubkey is required to verify this KFrag.") - elif delegating_pubkey.params != params: - raise ValueError("The delegating pubkey has different UmbralParameters.") - - if self.receiving_key_in_signature(): - if receiving_pubkey is None: - raise ValueError("The receiving pubkey is required to verify this KFrag.") - elif receiving_pubkey.params != params: - raise ValueError("The receiving pubkey has different UmbralParameters.") - - u = params.u - - kfrag_id = self.id - key = self.bn_key - commitment = self.point_commitment - precursor = self.point_precursor - - #  We check that the commitment is well-formed - correct_commitment = commitment == key * u - - validity_input = [kfrag_id, commitment, precursor, self.keys_in_signature] - - if self.delegating_key_in_signature(): - validity_input.append(delegating_pubkey) - - if self.receiving_key_in_signature(): - validity_input.append(receiving_pubkey) - - kfrag_validity_message = bytes().join(bytes(item) for item in validity_input) - valid_kfrag_signature = self.signature_for_proxy.verify(kfrag_validity_message, signing_pubkey) - - return correct_commitment & valid_kfrag_signature - - def verify_for_capsule(self, capsule) -> bool: - correctness_keys = capsule.get_correctness_keys() - - return self.verify(params=capsule.params, - signing_pubkey=correctness_keys["verifying"], - delegating_pubkey=correctness_keys["delegating"], - receiving_pubkey=correctness_keys["receiving"]) - - def delegating_key_in_signature(self): - return self.keys_in_signature == DELEGATING_ONLY or \ - self.keys_in_signature == DELEGATING_AND_RECEIVING - - def receiving_key_in_signature(self): - return self.keys_in_signature == RECEIVING_ONLY or \ - self.keys_in_signature == DELEGATING_AND_RECEIVING - - def __bytes__(self) -> bytes: - return self.to_bytes() - - def __eq__(self, other): - return hmac.compare_digest(bytes(self), bytes(other)) - - def __hash__(self): - return hash(bytes(self.id)) - - def __repr__(self): - return "{}:{}".format(self.__class__.__name__, self.id.hex()[:15]) diff --git a/umbral/openssl.py b/umbral/openssl.py deleted file mode 100644 index cc52d7fc..00000000 --- a/umbral/openssl.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import typing -from contextlib import contextmanager -from cryptography.hazmat.backends.openssl import backend - - -@typing.no_type_check -def _get_new_BN(set_consttime_flag=True): - """ - Returns a new and initialized OpenSSL BIGNUM. - The set_consttime_flag is set to True by default. When this instance of a - CurveBN object has BN_FLG_CONSTTIME set, OpenSSL will use constant time - operations whenever this CurveBN is passed. - """ - new_bn = backend._lib.BN_new() - backend.openssl_assert(new_bn != backend._ffi.NULL) - new_bn = backend._ffi.gc(new_bn, backend._lib.BN_clear_free) - - if set_consttime_flag: - backend._lib.BN_set_flags(new_bn, backend._lib.BN_FLG_CONSTTIME) - return new_bn - - -@typing.no_type_check -def _get_ec_group_by_curve_nid(curve_nid: int): - """ - Returns the group of a given curve via its OpenSSL nid. This must be freed - after each use otherwise it leaks memory. - """ - group = backend._lib.EC_GROUP_new_by_curve_name(curve_nid) - backend.openssl_assert(group != backend._ffi.NULL) - - return group - - -@typing.no_type_check -def _get_ec_order_by_group(ec_group): - """ - Returns the order of a given curve via its OpenSSL EC_GROUP. - """ - ec_order = _get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_GROUP_get_order(ec_group, ec_order, bn_ctx) - backend.openssl_assert(res == 1) - return ec_order - - -@typing.no_type_check -def _get_ec_generator_by_group(ec_group): - """ - Returns the generator point of a given curve via its OpenSSL EC_GROUP. - """ - generator = backend._lib.EC_GROUP_get0_generator(ec_group) - backend.openssl_assert(generator != backend._ffi.NULL) - generator = backend._ffi.gc(generator, backend._lib.EC_POINT_clear_free) - - return generator - - -@typing.no_type_check -def _get_ec_group_degree(ec_group): - """ - Returns the number of bits needed to represent the order of the finite - field upon the curve is based. - """ - return backend._lib.EC_GROUP_get_degree(ec_group) - - -@typing.no_type_check -def _bn_is_on_curve(check_bn, curve: 'Curve'): - """ - Checks if a given OpenSSL BIGNUM is within the provided curve's order. - Returns True if the provided BN is on the curve, or False if the BN is zero - or not on the curve. - """ - zero = backend._int_to_bn(0) - zero = backend._ffi.gc(zero, backend._lib.BN_clear_free) - - check_sign = backend._lib.BN_cmp(check_bn, zero) - range_check = backend._lib.BN_cmp(check_bn, curve.order) - return check_sign == 1 and range_check == -1 - - -@typing.no_type_check -def _int_to_bn(py_int: int, curve: 'Curve'=None, set_consttime_flag=True): - """ - Converts the given Python int to an OpenSSL BIGNUM. If a curve is - provided, it will check if the Python integer is within the order of that - curve. If it's not within the order, it will raise a ValueError. - - If set_consttime_flag is set to True, OpenSSL will use constant time - operations when using this CurveBN. - """ - conv_bn = backend._int_to_bn(py_int) - conv_bn = backend._ffi.gc(conv_bn, backend._lib.BN_clear_free) - - if curve: - on_curve = _bn_is_on_curve(conv_bn, curve) - if not on_curve: - raise ValueError("The Python integer given is not on the provided curve.") - - if set_consttime_flag: - backend._lib.BN_set_flags(conv_bn, backend._lib.BN_FLG_CONSTTIME) - return conv_bn - -@typing.no_type_check -def _bytes_to_bn(bytes_seq: bytes, set_consttime_flag=True): - """ - Converts the given byte sequence to an OpenSSL BIGNUM. - If set_consttime_flag is set to True, OpenSSL will use constant time - operations when using this BIGNUM. - """ - bn = _get_new_BN(set_consttime_flag) - backend._lib.BN_bin2bn(bytes_seq, len(bytes_seq), bn) - backend.openssl_assert(bn != backend._ffi.NULL) - return bn - -@typing.no_type_check -def _bn_to_bytes(bignum, length : int = None): - """ - Converts the given OpenSSL BIGNUM into a Python bytes sequence. - If length is given, the return bytes will have such length. - If the BIGNUM doesn't fit, it raises a ValueError. - """ - - if bignum is None or bignum == backend._ffi.NULL: - raise ValueError("Input BIGNUM must have a value") - - bn_num_bytes = backend._lib.BN_num_bytes(bignum) - if length is None: - length = bn_num_bytes - elif bn_num_bytes > length: - raise ValueError("Input BIGNUM doesn't fit in {} B".format(length)) - - bin_ptr = backend._ffi.new("unsigned char []", length) - bin_len = backend._lib.BN_bn2bin(bignum, bin_ptr) - return bytes.rjust(backend._ffi.buffer(bin_ptr, bin_len)[:], length, b'\0') - - -@typing.no_type_check -def _get_new_EC_POINT(curve: 'Curve'): - """ - Returns a new and initialized OpenSSL EC_POINT given the group of a curve. - If __curve_nid is provided, it retrieves the group from the curve provided. - """ - new_point = backend._lib.EC_POINT_new(curve.ec_group) - backend.openssl_assert(new_point != backend._ffi.NULL) - new_point = backend._ffi.gc(new_point, backend._lib.EC_POINT_clear_free) - - return new_point - - -@typing.no_type_check -def _get_EC_POINT_via_affine(affine_x, affine_y, curve: 'Curve'): - """ - Returns an EC_POINT given the group of a curve and the affine coordinates - provided. - """ - new_point = _get_new_EC_POINT(curve) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_set_affine_coordinates_GFp( - curve.ec_group, new_point, affine_x, affine_y, bn_ctx - ) - backend.openssl_assert(res == 1) - return new_point - - -@typing.no_type_check -def _get_affine_coords_via_EC_POINT(ec_point, curve: 'Curve'): - """ - Returns the affine coordinates of a given point on the provided ec_group. - """ - affine_x = _get_new_BN() - affine_y = _get_new_BN() - - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_get_affine_coordinates_GFp( - curve.ec_group, ec_point, affine_x, affine_y, bn_ctx - ) - backend.openssl_assert(res == 1) - return (affine_x, affine_y) - - -@typing.no_type_check -@contextmanager -def _tmp_bn_mont_ctx(modulus): - """ - Initializes and returns a BN_MONT_CTX for Montgomery ops. - Requires a modulus to place in the Montgomery structure. - """ - bn_mont_ctx = backend._lib.BN_MONT_CTX_new() - backend.openssl_assert(bn_mont_ctx != backend._ffi.NULL) - # Don't set the garbage collector. Only free it when the context is done - # or else you'll get a null pointer error. - - try: - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_MONT_CTX_set(bn_mont_ctx, modulus, bn_ctx) - backend.openssl_assert(res == 1) - yield bn_mont_ctx - finally: - backend._lib.BN_MONT_CTX_free(bn_mont_ctx) diff --git a/umbral/params.py b/umbral/params.py deleted file mode 100644 index c0dfdb2a..00000000 --- a/umbral/params.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from umbral.curve import Curve - - -class UmbralParameters: - def __init__(self, curve: Curve) -> None: - from umbral.point import Point - from umbral.random_oracles import unsafe_hash_to_point - - self.curve = curve - self.CURVE_KEY_SIZE_BYTES = self.curve.field_order_size_in_bytes - - self.g = Point.get_generator_from_curve(curve=curve) - g_bytes = self.g.to_bytes() - - parameters_seed = b'NuCypher/UmbralParameters/' - self.u = unsafe_hash_to_point(g_bytes, self, parameters_seed + b'u') - - def __eq__(self, other) -> bool: - - # TODO: This is not comparing the order, which currently is an OpenSSL pointer - self_attributes = self.curve, self.g, self.CURVE_KEY_SIZE_BYTES, self.u - others_attributes = other.curve, other.g, other.CURVE_KEY_SIZE_BYTES, other.u - - return self_attributes == others_attributes diff --git a/umbral/point.py b/umbral/point.py deleted file mode 100644 index 26b4a2e3..00000000 --- a/umbral/point.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from typing import Optional, Tuple - -from cryptography.hazmat.backends.openssl import backend - -from umbral import openssl -from umbral.config import default_curve -from umbral.curve import Curve -from umbral.curvebn import CurveBN - - -class Point: - """ - Represents an OpenSSL EC_POINT except more Pythonic - """ - - def __init__(self, ec_point, curve: Curve) -> None: - self.ec_point = ec_point - self.curve = curve - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None, - is_compressed: bool = True): - """ - Returns the size (in bytes) of a Point given a curve. - If no curve is provided, it uses the default curve. - By default, it assumes compressed representation (is_compressed = True). - """ - curve = curve if curve is not None else default_curve() - - coord_size = curve.field_order_size_in_bytes - - if is_compressed: - return 1 + coord_size - else: - return 1 + 2 * coord_size - - @classmethod - def gen_rand(cls, curve: Optional[Curve] = None) -> 'Point': - """ - Returns a Point object with a cryptographically secure EC_POINT based - on the provided curve. - """ - curve = curve if curve is not None else default_curve() - - rand_point = openssl._get_new_EC_POINT(curve) - rand_bn = CurveBN.gen_rand(curve).bignum - - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_mul( - curve.ec_group, rand_point, backend._ffi.NULL, curve.generator, - rand_bn, bn_ctx - ) - backend.openssl_assert(res == 1) - - return cls(rand_point, curve) - - @classmethod - def from_affine(cls, coords: Tuple[int, int], curve: Optional[Curve] = None) -> 'Point': - """ - Returns a Point object from the given affine coordinates in a tuple in - the format of (x, y) and a given curve. - """ - curve = curve if curve is not None else default_curve() - - affine_x, affine_y = coords - if type(affine_x) == int: - affine_x = openssl._int_to_bn(affine_x, curve=None) - - if type(affine_y) == int: - affine_y = openssl._int_to_bn(affine_y, curve=None) - - ec_point = openssl._get_EC_POINT_via_affine(affine_x, affine_y, curve) - return cls(ec_point, curve) - - def to_affine(self): - """ - Returns a tuple of Python ints in the format of (x, y) that represents - the point in the curve. - """ - affine_x, affine_y = openssl._get_affine_coords_via_EC_POINT( - self.ec_point, self.curve) - return (backend._bn_to_int(affine_x), backend._bn_to_int(affine_y)) - - @classmethod - def from_bytes(cls, data: bytes, curve: Optional[Curve] = None) -> 'Point': - """ - Returns a Point object from the given byte data on the curve provided. - """ - curve = curve if curve is not None else default_curve() - - point = openssl._get_new_EC_POINT(curve) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_oct2point( - curve.ec_group, point, data, len(data), bn_ctx); - backend.openssl_assert(res == 1) - - return cls(point, curve) - - def to_bytes(self, is_compressed: bool=True) -> bytes: - """ - Returns the Point serialized as bytes. It will return a compressed form - if is_compressed is set to True. - """ - length = self.expected_bytes_length(self.curve, is_compressed) - - if is_compressed: - point_conversion_form = backend._lib.POINT_CONVERSION_COMPRESSED - else: - point_conversion_form = backend._lib.POINT_CONVERSION_UNCOMPRESSED - - bin_ptr = backend._ffi.new("unsigned char[]", length) - with backend._tmp_bn_ctx() as bn_ctx: - bin_len = backend._lib.EC_POINT_point2oct( - self.curve.ec_group, self.ec_point, point_conversion_form, - bin_ptr, length, bn_ctx - ) - backend.openssl_assert(bin_len != 0) - - return bytes(backend._ffi.buffer(bin_ptr, bin_len)[:]) - - @classmethod - def get_generator_from_curve(cls, curve: Optional[Curve] = None) -> 'Point': - """ - Returns the generator Point from the given curve as a Point object. - """ - curve = curve if curve is not None else default_curve() - return cls(curve.generator, curve) - - def __eq__(self, other): - """ - Compares two EC_POINTS for equality. - """ - with backend._tmp_bn_ctx() as bn_ctx: - is_equal = backend._lib.EC_POINT_cmp( - self.curve.ec_group, self.ec_point, other.ec_point, bn_ctx - ) - backend.openssl_assert(is_equal != -1) - - # 1 is not-equal, 0 is equal, -1 is error - return not bool(is_equal) - - def __mul__(self, other: CurveBN) -> 'Point': - """ - Performs an EC_POINT_mul on an EC_POINT and a BIGNUM. - """ - # TODO: Check that both points use the same curve. - prod = openssl._get_new_EC_POINT(self.curve) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_mul( - self.curve.ec_group, prod, backend._ffi.NULL, - self.ec_point, other.bignum, bn_ctx - ) - backend.openssl_assert(res == 1) - - return Point(prod, self.curve) - - __rmul__ = __mul__ - - def __add__(self, other) -> 'Point': - """ - Performs an EC_POINT_add on two EC_POINTS. - """ - op_sum = openssl._get_new_EC_POINT(self.curve) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_add( - self.curve.ec_group, op_sum, self.ec_point, other.ec_point, bn_ctx - ) - backend.openssl_assert(res == 1) - return Point(op_sum, self.curve) - - def __sub__(self, other): - """ - Performs subtraction by adding the inverse of the `other` to the point. - """ - return (self + (-other)) - - def __neg__(self) -> 'Point': - """ - Computes the additive inverse of a Point, by performing an - EC_POINT_invert on itself. - """ - inv = backend._lib.EC_POINT_dup(self.ec_point, self.curve.ec_group) - backend.openssl_assert(inv != backend._ffi.NULL) - inv = backend._ffi.gc(inv, backend._lib.EC_POINT_clear_free) - - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_invert( - self.curve.ec_group, inv, bn_ctx - ) - backend.openssl_assert(res == 1) - return Point(inv, self.curve) - - def __bytes__(self) -> bytes: - return self.to_bytes() diff --git a/umbral/pre.py b/umbral/pre.py deleted file mode 100644 index 60fbacef..00000000 --- a/umbral/pre.py +++ /dev/null @@ -1,518 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import os -import typing -from typing import Dict, List, Optional, Tuple, Union, Any - -from bytestring_splitter import BytestringSplitter -from cryptography.exceptions import InvalidTag -from constant_sorrow import constants - -from umbral.cfrags import CapsuleFrag -from umbral.config import default_curve -from umbral.curve import Curve -from umbral.curvebn import CurveBN -from umbral.dem import UmbralDEM, DEM_KEYSIZE, DEM_NONCE_SIZE -from umbral.keys import UmbralPrivateKey, UmbralPublicKey -from umbral.kfrags import KFrag, NO_KEY, DELEGATING_ONLY, RECEIVING_ONLY, DELEGATING_AND_RECEIVING -from umbral.params import UmbralParameters -from umbral.point import Point -from umbral.random_oracles import kdf, hash_to_curvebn -from umbral.signing import Signer -from umbral.utils import poly_eval, lambda_coeff - - -class GenericUmbralError(Exception): - pass - - -class UmbralCorrectnessError(GenericUmbralError): - def __init__(self, message: str, offending_cfrags: List[CapsuleFrag]) -> None: - super().__init__(message) - self.offending_cfrags = offending_cfrags - - -class UmbralDecryptionError(GenericUmbralError): - def __init__(self) -> None: - super().__init__("Decryption of ciphertext failed: " - "either someone tampered with the ciphertext or " - "you are using an incorrect decryption key.") - - -class Capsule: - - def __init__(self, - params: UmbralParameters, - point_e: Point, - point_v: Point, - bn_sig: CurveBN, - ) -> None: - - self.params = params - - if not all((isinstance(point_e, Point), - isinstance(point_v, Point), - isinstance(bn_sig, CurveBN))): - raise TypeError("Need valid point_e, point_v, and bn_sig to make a Capsule.") - - self.point_e = point_e - self.point_v = point_v - self.bn_sig = bn_sig - - self._attached_cfrags = set() # type: set - self._cfrag_correctness_keys = { - 'delegating': None, 'receiving': None, 'verifying': None - } # type: dict - - class NotValid(ValueError): - """ - raised if the capsule does not pass verification. - """ - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None) -> int: - """ - Returns the size (in bytes) of a Capsule given the curve. - If no curve is provided, it will use the default curve. - """ - curve = curve if curve is not None else default_curve() - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - - return (bn_size * 1) + (point_size * 2) - - @classmethod - def from_bytes(cls, capsule_bytes: bytes, params: UmbralParameters) -> 'Capsule': - """ - Instantiates a Capsule object from the serialized data. - """ - curve = params.curve - - bn_size = CurveBN.expected_bytes_length(curve) - point_size = Point.expected_bytes_length(curve) - arguments = {'curve': curve} - - if len(capsule_bytes) == cls.expected_bytes_length(curve): - splitter = BytestringSplitter( - (Point, point_size, arguments), # point_e - (Point, point_size, arguments), # point_v - (CurveBN, bn_size, arguments) # bn_sig - ) - else: - raise ValueError("Byte string does not have a valid length for a Capsule") - - components = splitter(capsule_bytes) - return cls(params, *components) - - def _set_cfrag_correctness_key(self, key_type: str, key: Optional[UmbralPublicKey]) -> bool: - if key_type not in ("delegating", "receiving", "verifying"): - raise ValueError("You can only set 'delegating', 'receiving' or 'verifying' keys.") - - current_key = self._cfrag_correctness_keys[key_type] - - if current_key is None: - if key is None: - return False - elif self.params != key.params: - raise TypeError("You are trying to set a key with different UmbralParameters.") - else: - self._cfrag_correctness_keys[key_type] = key - return True - elif key in (None, current_key): - return False - else: - raise ValueError("The {} key is already set; you can't set it again.".format(key_type)) - - def get_correctness_keys(self) -> Dict[str, Union[UmbralPublicKey, None]]: - return dict(self._cfrag_correctness_keys) - - def set_correctness_keys(self, - delegating: Optional[UmbralPublicKey] = None, - receiving: Optional[UmbralPublicKey] = None, - verifying: Optional[UmbralPublicKey] = None, - ) -> Tuple[bool, bool, bool]: - - delegating_key_details = self._set_cfrag_correctness_key(key_type="delegating", key=delegating) - receiving_key_details = self._set_cfrag_correctness_key(key_type="receiving", key=receiving) - verifying_key_details = self._set_cfrag_correctness_key(key_type="verifying", key=verifying) - - return delegating_key_details, receiving_key_details, verifying_key_details - - def to_bytes(self) -> bytes: - """ - Serialize the Capsule into a bytestring. - """ - e, v, s = self.components() - return e.to_bytes() + v.to_bytes() + s.to_bytes() - - def verify(self) -> bool: - - g = self.params.g - e, v, s = self.components() - h = hash_to_curvebn(e, v, params=self.params) - - result = s * g == v + (h * e) # type: bool - return result - - def attach_cfrag(self, cfrag: CapsuleFrag) -> None: - if cfrag.verify_correctness(self): - self._attached_cfrags.add(cfrag) - else: - error_msg = "CFrag is not correct and cannot be attached to the Capsule" - raise UmbralCorrectnessError(error_msg, [cfrag]) - - def clear_cfrags(self): - self._attached_cfrags = set() - - def first_cfrag(self): - try: - return list(self._attached_cfrags)[0] - except IndexError: - raise TypeError("This Capsule doesn't have any CFrags attached. Ergo, you can't get the first one.") - - def components(self) -> Tuple[Point, Point, CurveBN]: - return self.point_e, self.point_v, self.bn_sig - - def __bytes__(self) -> bytes: - return self.to_bytes() - - def __contains__(self, cfrag): - return cfrag in self._attached_cfrags - - def __eq__(self, other) -> bool: - """ - Each component is compared to its counterpart in constant time per the __eq__ of Point and CurveBN. - """ - return hasattr(other, "components") and self.components() == other.components() and all(self.components()) - - @typing.no_type_check - def __hash__(self) -> int: - # In case this isn't obvious, don't use this as a secure hash. Use BLAKE2b or something. - component_bytes = tuple(component.to_bytes() for component in self.components()) - return hash(component_bytes) - - def __len__(self) -> int: - return len(self._attached_cfrags) - - def __repr__(self): - return "{}:{}".format(self.__class__.__name__, hex(int(self.bn_sig))[2:17]) - - -def generate_kfrags(delegating_privkey: UmbralPrivateKey, - receiving_pubkey: UmbralPublicKey, - threshold: int, - N: int, - signer: Signer, - sign_delegating_key: Optional[bool] = True, - sign_receiving_key: Optional[bool] = True, - ) -> List[KFrag]: - """ - Creates a re-encryption key from Alice's delegating public key to Bob's - receiving public key, and splits it in KFrags, using Shamir's Secret Sharing. - Requires a threshold number of KFrags out of N. - - Returns a list of N KFrags - """ - - if threshold <= 0 or threshold > N: - raise ValueError('Arguments threshold and N must satisfy 0 < threshold <= N') - - if delegating_privkey.params != receiving_pubkey.params: - raise ValueError("Keys must have the same parameter set.") - - params = delegating_privkey.params - - g = params.g - - delegating_pubkey = delegating_privkey.get_pubkey() - - bob_pubkey_point = receiving_pubkey.point_key - - # The precursor point is used as an ephemeral public key in a DH key exchange, - # and the resulting shared secret 'dh_point' is used to derive other secret values - private_precursor = CurveBN.gen_rand(params.curve) - precursor = private_precursor * g # type: Any - - dh_point = private_precursor * bob_pubkey_point - - # Secret value 'd' allows to make Umbral non-interactive - d = hash_to_curvebn(precursor, - bob_pubkey_point, - dh_point, - bytes(constants.NON_INTERACTIVE), - params=params) - - # Coefficients of the generating polynomial - coefficients = [delegating_privkey.bn_key * (~d)] - coefficients += [CurveBN.gen_rand(params.curve) for _ in range(threshold - 1)] - - bn_size = CurveBN.expected_bytes_length(params.curve) - - kfrags = list() - for _ in range(N): - kfrag_id = os.urandom(bn_size) - - # The index of the re-encryption key share (which in Shamir's Secret - # Sharing corresponds to x in the tuple (x, f(x)), with f being the - # generating polynomial), is used to prevent reconstruction of the - # re-encryption key without Bob's intervention - share_index = hash_to_curvebn(precursor, - bob_pubkey_point, - dh_point, - bytes(constants.X_COORDINATE), - kfrag_id, - params=params) - - # The re-encryption key share is the result of evaluating the generating - # polynomial for the index value - rk = poly_eval(coefficients, share_index) - - commitment = rk * params.u # type: Any - - validity_message_for_bob = (kfrag_id, - delegating_pubkey, - receiving_pubkey, - commitment, - precursor, - ) # type: Any - validity_message_for_bob = bytes().join(bytes(item) for item in validity_message_for_bob) - signature_for_bob = signer(validity_message_for_bob) - - if sign_delegating_key and sign_receiving_key: - mode = DELEGATING_AND_RECEIVING - elif sign_delegating_key: - mode = DELEGATING_ONLY - elif sign_receiving_key: - mode = RECEIVING_ONLY - else: - mode = NO_KEY - - validity_message_for_proxy = [kfrag_id, commitment, precursor, mode] # type: Any - - if sign_delegating_key: - validity_message_for_proxy.append(delegating_pubkey) - if sign_receiving_key: - validity_message_for_proxy.append(receiving_pubkey) - - validity_message_for_proxy = bytes().join(bytes(item) for item in validity_message_for_proxy) - signature_for_proxy = signer(validity_message_for_proxy) - - kfrag = KFrag(identifier=kfrag_id, - bn_key=rk, - point_commitment=commitment, - point_precursor=precursor, - signature_for_proxy=signature_for_proxy, - signature_for_bob=signature_for_bob, - keys_in_signature=mode, - ) - - kfrags.append(kfrag) - - return kfrags - - -def reencrypt(kfrag: KFrag, - capsule: Capsule, - provide_proof: bool = True, - metadata: Optional[bytes] = None, - verify_kfrag: bool = True) -> CapsuleFrag: - - if not isinstance(capsule, Capsule) or not capsule.verify(): - raise Capsule.NotValid - - if verify_kfrag: - if not isinstance(kfrag, KFrag) or not kfrag.verify_for_capsule(capsule): - raise KFrag.NotValid - - rk = kfrag.bn_key - e1 = rk * capsule.point_e # type: Any - v1 = rk * capsule.point_v # type: Any - - cfrag = CapsuleFrag(point_e1=e1, point_v1=v1, kfrag_id=kfrag.id, - point_precursor=kfrag.point_precursor) - - if provide_proof: - cfrag.prove_correctness(capsule, kfrag, metadata) - - return cfrag - - -def _encapsulate(alice_pubkey: UmbralPublicKey, - key_length: int = DEM_KEYSIZE) -> Tuple[bytes, Capsule]: - """Generates a symmetric key and its associated KEM ciphertext""" - - params = alice_pubkey.params - g = params.g - - priv_r = CurveBN.gen_rand(params.curve) - pub_r = priv_r * g # type: Any - - priv_u = CurveBN.gen_rand(params.curve) - pub_u = priv_u * g # type: Any - - h = hash_to_curvebn(pub_r, pub_u, params=params) - s = priv_u + (priv_r * h) - - shared_key = (priv_r + priv_u) * alice_pubkey.point_key # type: Any - - # Key to be used for symmetric encryption - key = kdf(shared_key, key_length) - - return key, Capsule(point_e=pub_r, point_v=pub_u, bn_sig=s, params=params) - - -def _decapsulate_original(private_key: UmbralPrivateKey, - capsule: Capsule, - key_length: int = DEM_KEYSIZE) -> bytes: - """Derive the same symmetric key""" - - if not capsule.verify(): - # Check correctness of original ciphertext - raise capsule.NotValid("Capsule verification failed.") - - shared_key = private_key.bn_key * (capsule.point_e + capsule.point_v) # type: Any - key = kdf(shared_key, key_length) - return key - - -def _decapsulate_reencrypted(receiving_privkey: UmbralPrivateKey, capsule: Capsule, - key_length: int = DEM_KEYSIZE) -> bytes: - """Derive the same symmetric encapsulated_key""" - - params = capsule.params - - pub_key = receiving_privkey.get_pubkey().point_key - priv_key = receiving_privkey.bn_key - - precursor = capsule.first_cfrag().point_precursor - dh_point = priv_key * precursor - - # Combination of CFrags via Shamir's Secret Sharing reconstruction - xs = list() - for cfrag in capsule._attached_cfrags: - x = hash_to_curvebn(precursor, - pub_key, - dh_point, - bytes(constants.X_COORDINATE), - cfrag.kfrag_id, - params=params) - xs.append(x) - - e_summands, v_summands = list(), list() - for cfrag, x in zip(capsule._attached_cfrags, xs): - if precursor != cfrag.point_precursor: - raise ValueError("Attached CFrags are not pairwise consistent") - lambda_i = lambda_coeff(x, xs) - e_summands.append(lambda_i * cfrag.point_e1) - v_summands.append(lambda_i * cfrag.point_v1) - - e_prime = sum(e_summands[1:], e_summands[0]) - v_prime = sum(v_summands[1:], v_summands[0]) - - # Secret value 'd' allows to make Umbral non-interactive - d = hash_to_curvebn(precursor, - pub_key, - dh_point, - bytes(constants.NON_INTERACTIVE), - params=params) - - e, v, s = capsule.components() - h = hash_to_curvebn(e, v, params=params) - - orig_pub_key = capsule.get_correctness_keys()['delegating'].point_key # type: ignore - - if not (s / d) * orig_pub_key == (h * e_prime) + v_prime: - raise GenericUmbralError() - - shared_key = d * (e_prime + v_prime) - encapsulated_key = kdf(shared_key, key_length) - return encapsulated_key - - -def encrypt(alice_pubkey: UmbralPublicKey, plaintext: bytes) -> Tuple[bytes, Capsule]: - """ - Performs an encryption using the UmbralDEM object and encapsulates a key - for the sender using the public key provided. - - Returns the ciphertext and the KEM Capsule. - """ - key, capsule = _encapsulate(alice_pubkey, DEM_KEYSIZE) - - capsule_bytes = bytes(capsule) - - dem = UmbralDEM(key) - ciphertext = dem.encrypt(plaintext, authenticated_data=capsule_bytes) - - return ciphertext, capsule - - -def _open_capsule(capsule: Capsule, receiving_privkey: UmbralPrivateKey, - check_proof: bool = True) -> bytes: - """ - Activates the Capsule from the attached CFrags, - opens the Capsule and returns what is inside. - - This will often be a symmetric key. - """ - - if check_proof: - offending_cfrags = [] - for cfrag in capsule._attached_cfrags: - if not cfrag.verify_correctness(capsule): - offending_cfrags.append(cfrag) - - if offending_cfrags: - error_msg = "Decryption error: Some CFrags are not correct" - raise UmbralCorrectnessError(error_msg, offending_cfrags) - - key = _decapsulate_reencrypted(receiving_privkey, capsule) - return key - - -def decrypt(ciphertext: bytes, capsule: Capsule, decrypting_key: UmbralPrivateKey, - check_proof: bool = True) -> bytes: - """ - Opens the capsule and gets what's inside. - - We hope that's a symmetric key, which we use to decrypt the ciphertext - and return the resulting cleartext. - """ - - if not isinstance(ciphertext, bytes) or len(ciphertext) < DEM_NONCE_SIZE: - raise ValueError("Input ciphertext must be a bytes object of length >= {}".format(DEM_NONCE_SIZE)) - elif not isinstance(capsule, Capsule) or not capsule.verify(): - raise Capsule.NotValid - elif not isinstance(decrypting_key, UmbralPrivateKey): - raise TypeError("The decrypting key is not an UmbralPrivateKey") - - if capsule._attached_cfrags: - # Since there are cfrags attached, we assume this is Bob opening the Capsule. - # (i.e., this is a re-encrypted capsule) - encapsulated_key = _open_capsule(capsule, decrypting_key, check_proof=check_proof) - else: - # Since there aren't cfrags attached, we assume this is Alice opening the Capsule. - # (i.e., this is an original capsule) - encapsulated_key = _decapsulate_original(decrypting_key, capsule) - - dem = UmbralDEM(encapsulated_key) - try: - cleartext = dem.decrypt(ciphertext, authenticated_data=bytes(capsule)) - except InvalidTag as e: - raise UmbralDecryptionError() from e - - return cleartext diff --git a/umbral/random_oracles.py b/umbral/random_oracles.py deleted file mode 100644 index 427e0a28..00000000 --- a/umbral/random_oracles.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" -from abc import abstractmethod, ABC -from typing import Optional, Type - -from cryptography.hazmat.backends.openssl import backend -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.exceptions import InternalError - -import sha3 - -from umbral import openssl -from umbral.curvebn import CurveBN -from umbral.point import Point -from umbral.params import UmbralParameters -from umbral.config import default_params - - -class Hash(ABC): - - CUSTOMIZATION_STRING_LENGTH = 64 - CUSTOMIZATION_STRING_PAD = b'\x00' - - @abstractmethod - def __init__(self, customization_string: bytes = b''): - - if len(customization_string) > Hash.CUSTOMIZATION_STRING_LENGTH: - raise ValueError("The maximum length of the customization string is " - "{} bytes".format(Hash.CUSTOMIZATION_STRING_LENGTH)) - - self.customization_string = customization_string.ljust( - Hash.CUSTOMIZATION_STRING_LENGTH, - Hash.CUSTOMIZATION_STRING_PAD - ) - self.update(self.customization_string) - - @abstractmethod - def update(self, data: bytes) -> None: - raise NotImplementedError - - @abstractmethod - def copy(self) -> 'Hash': - raise NotImplementedError - - @abstractmethod - def finalize(self) -> bytes: - raise NotImplementedError - - -class Blake2b(Hash): - def __init__(self, customization_string: bytes = b''): - # TODO: use a Blake2b implementation that supports personalization (see #155) - self._blake2b = hashes.Hash(hashes.BLAKE2b(64), backend=backend) - super().__init__(customization_string) - - def update(self, data: bytes) -> None: - self._blake2b.update(data) - - def copy(self) -> 'Blake2b': - replica = type(self)() - replica._blake2b = self._blake2b.copy() - return replica - - def finalize(self) -> bytes: - return self._blake2b.finalize() - - -class ExtendedKeccak(Hash): - - _UPPER_PREFIX = b'\x00' - _LOWER_PREFIX = b'\x01' - - def __init__(self, customization_string: bytes = b''): - self._upper = sha3.keccak_256() - self._lower = sha3.keccak_256() - - self._upper.update(self._UPPER_PREFIX) - self._lower.update(self._LOWER_PREFIX) - - super().__init__(customization_string) - - def update(self, data: bytes) -> None: - self._upper.update(data) - self._lower.update(data) - - def copy(self) -> 'ExtendedKeccak': - replica = type(self)() - replica._upper = self._upper.copy() - replica._lower = self._lower.copy() - return replica - - def finalize(self) -> bytes: - return self._upper.digest() + self._lower.digest() - - -def kdf(ecpoint: Point, - key_length: int, - salt: Optional[bytes] = None, - info: Optional[bytes] = None, - ) -> bytes: - - data = ecpoint.to_bytes(is_compressed=True) - hkdf = HKDF(algorithm=hashes.BLAKE2b(64), - length=key_length, - salt=salt, - info=info, - backend=default_backend()) - return hkdf.derive(data) - - -# TODO: Common API for all hash_to_curvebn functions. -# TODO: ^ It should check the correct number and type args, instead of current approach. -def hash_to_curvebn(*crypto_items, - params: UmbralParameters, - customization_string: bytes = b'', - hash_class: Type[Hash] = Blake2b) -> CurveBN: - - customization_string = b'hash_to_curvebn' + customization_string - hash_function = hash_class(customization_string=customization_string) - - for item in crypto_items: - try: - item_bytes = item.to_bytes() - except AttributeError: - if isinstance(item, bytes): - item_bytes = item - else: - raise TypeError("Input with type {} not accepted".format(type(item))) - hash_function.update(item_bytes) - - hash_digest = openssl._bytes_to_bn(hash_function.finalize()) - - one = backend._lib.BN_value_one() - - order_minus_1 = openssl._get_new_BN() - res = backend._lib.BN_sub(order_minus_1, params.curve.order, one) - backend.openssl_assert(res == 1) - - bignum = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod(bignum, hash_digest, order_minus_1, bn_ctx) - backend.openssl_assert(res == 1) - - res = backend._lib.BN_add(bignum, bignum, one) - backend.openssl_assert(res == 1) - - return CurveBN(bignum, params.curve) - - -def unsafe_hash_to_point(data: bytes = b'', - params: UmbralParameters = None, - label: bytes = b'', - hash_class = Blake2b, - ) -> 'Point': - """ - Hashes arbitrary data into a valid EC point of the specified curve, - using the try-and-increment method. - It admits an optional label as an additional input to the hash function. - It uses BLAKE2b (with a digest size of 64 bytes) as the internal hash function. - - WARNING: Do not use when the input data is secret, as this implementation is not - in constant time, and hence, it is not safe with respect to timing attacks. - """ - - params = params if params is not None else default_params() - - len_data = len(data).to_bytes(4, byteorder='big') - len_label = len(label).to_bytes(4, byteorder='big') - - label_data = len_label + label + len_data + data - - # We use an internal 32-bit counter as additional input - i = 0 - while i < 2**32: - ibytes = i.to_bytes(4, byteorder='big') - hash_function = hash_class() - hash_function.update(label_data + ibytes) - hash_digest = hash_function.finalize()[:1 + params.CURVE_KEY_SIZE_BYTES] - - sign = b'\x02' if hash_digest[0] & 1 == 0 else b'\x03' - compressed_point = sign + hash_digest[1:] - - try: - return Point.from_bytes(compressed_point, params.curve) - except InternalError as e: - # We want to catch specific InternalExceptions: - # - Point not in the curve (code 107) - # - Invalid compressed point (code 110) - # https://github.com/openssl/openssl/blob/master/include/openssl/ecerr.h#L228 - if e.err_code[0].reason in (107, 110): - pass - else: - # Any other exception, we raise it - raise e - i += 1 - - # Only happens with probability 2^(-32) - raise ValueError('Could not hash input into the curve') diff --git a/umbral/signing.py b/umbral/signing.py deleted file mode 100644 index d473d1fd..00000000 --- a/umbral/signing.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published b -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -import hmac -from typing import Optional, Type - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.hashes import HashAlgorithm, SHA256 -from cryptography.hazmat.primitives.asymmetric import utils -from cryptography.hazmat.primitives.asymmetric.ec import ECDSA - - -from umbral.config import default_curve -from umbral.curve import Curve -from umbral.curvebn import CurveBN -from umbral.keys import UmbralPublicKey, UmbralPrivateKey - - -DEFAULT_HASH_ALGORITHM = SHA256 - - -class Signature: - """ - Wrapper for ECDSA signatures. - We store signatures as r and s; this class allows interoperation - between (r, s) and DER formatting. - """ - - def __init__(self, - r: CurveBN, - s: CurveBN, - hash_algorithm: Type[HashAlgorithm] = DEFAULT_HASH_ALGORITHM) -> None: - self.r = r - self.s = s - self.hash_algorithm = hash_algorithm - - def __repr__(self): - return "ECDSA Signature: {}".format(bytes(self).hex()[:15]) - - @classmethod - def expected_bytes_length(cls, curve: Optional[Curve] = None) -> int: - curve = curve if curve is not None else default_curve() - return 2 * curve.group_order_size_in_bytes - - def verify(self, message: bytes, - verifying_key: UmbralPublicKey, - is_prehashed: bool = False) -> bool: - """ - Verifies that a message's signature was valid. - - :param message: The message to verify - :param verifying_key: UmbralPublicKey of the signer - :param is_prehashed: True if the message has been prehashed previously - :return: True if valid, False if invalid - """ - cryptography_pub_key = verifying_key.to_cryptography_pubkey() - if is_prehashed: - signature_algorithm = ECDSA(utils.Prehashed(self.hash_algorithm())) - else: - signature_algorithm = ECDSA(self.hash_algorithm()) - - # TODO: Raise error instead of returning boolean - try: - cryptography_pub_key.verify( - signature=self._der_encoded_bytes(), - data=message, - signature_algorithm=signature_algorithm - ) - except InvalidSignature: - return False - return True - - @classmethod - def from_bytes(cls, - signature_as_bytes: bytes, - der_encoded: bool = False, - curve: Optional[Curve] = None) -> 'Signature': - curve = curve if curve is not None else default_curve() - if der_encoded: - r, s = utils.decode_dss_signature(signature_as_bytes) - else: - expected_len = cls.expected_bytes_length(curve) - if not len(signature_as_bytes) == expected_len: - raise ValueError("Looking for exactly {} bytes if you call from_bytes \ - with der_encoded=False and curve={}.".format(expected_len, curve)) - else: - r = int.from_bytes(signature_as_bytes[:(expected_len//2)], "big") - s = int.from_bytes(signature_as_bytes[(expected_len//2):], "big") - - return cls(CurveBN.from_int(r, curve), CurveBN.from_int(s, curve)) - - def _der_encoded_bytes(self) -> bytes: - return utils.encode_dss_signature(int(self.r), int(self.s)) - - def __bytes__(self) -> bytes: - return self.r.to_bytes() + self.s.to_bytes() - - def __len__(self): - return len(bytes(self)) - - def __add__(self, other): - return bytes(self) + other - - def __radd__(self, other: bytes) -> bytes: - return other + bytes(self) - - def __eq__(self, other) -> bool: - simple_bytes_match = hmac.compare_digest(bytes(self), bytes(other)) - der_encoded_match = hmac.compare_digest(self._der_encoded_bytes(), bytes(other)) - return simple_bytes_match or der_encoded_match - - -class Signer: - """Callable wrapping ECDSA signing with UmbralPrivateKeys""" - - def __init__(self, - private_key: UmbralPrivateKey, - hash_algorithm: Type[HashAlgorithm] = DEFAULT_HASH_ALGORITHM) -> None: - self.__cryptography_private_key = private_key.to_cryptography_privkey() - self.curve = private_key.params.curve - self.hash_algorithm = hash_algorithm - - def __call__(self, message: bytes, is_prehashed: bool = False) -> Signature: - """ - Signs the message with this instance's private key. - - :param message: Message to hash and sign - :param is_prehashed: True if the message has been prehashed previously - :return: signature - """ - if is_prehashed: - signature_algorithm = ECDSA(utils.Prehashed(self.hash_algorithm())) - else: - signature_algorithm = ECDSA(self.hash_algorithm()) - - signature_der_bytes = self.__cryptography_private_key.sign(message, signature_algorithm) - return Signature.from_bytes(signature_der_bytes, der_encoded=True, curve=self.curve) diff --git a/umbral/utils.py b/umbral/utils.py deleted file mode 100644 index 0b06b572..00000000 --- a/umbral/utils.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published b -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -from typing import List - -from umbral.curvebn import CurveBN - - -def lambda_coeff(id_i: CurveBN, selected_ids: List[CurveBN]) -> CurveBN: - ids = [x for x in selected_ids if x != id_i] - - if not ids: - return CurveBN.from_int(1, id_i.curve) - - result = ids[0] / (ids[0] - id_i) - for id_j in ids[1:]: - result = result * id_j / (id_j - id_i) - - return result - - -def poly_eval(coeff: List[CurveBN], x: CurveBN) -> CurveBN: - result = coeff[-1] - for i in range(-2, -len(coeff) - 1, -1): - result = (result * x) + coeff[i] - - return result From 439b675e657e29abbe151cb96217a8c63b52cf53 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 22:35:49 -0700 Subject: [PATCH 02/25] Drop Py3.5 support --- .circleci/config.yml | 43 ------------------------------------------- setup.py | 1 - 2 files changed, 44 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 04f80ac8..312e6f66 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,6 @@ workflows: version: 2 build_test_deploy: jobs: - - pipenv_install_35: - filters: - tags: - only: /.*/ - pipenv_install_36: filters: tags: @@ -16,10 +12,6 @@ workflows: filters: tags: only: /.*/ - - pip_install_35: - filters: - tags: - only: /.*/ - pip_install_36: filters: tags: @@ -28,13 +20,6 @@ workflows: filters: tags: only: /.*/ - - run_tests_35: - filters: - tags: - only: /.*/ - requires: - - pipenv_install_35 - - pip_install_35 - run_tests_36: filters: tags: @@ -54,7 +39,6 @@ workflows: tags: only: /.*/ requires: - - run_tests_35 - run_tests_36 - run_tests_37 - doctests_36: @@ -62,7 +46,6 @@ workflows: tags: only: /.*/ requires: - - run_tests_35 - run_tests_36 - run_tests_37 - notebook_tests_36: @@ -70,7 +53,6 @@ workflows: tags: only: /.*/ requires: - - run_tests_35 - run_tests_36 - run_tests_37 - reencryption_memory_profile_36: @@ -78,7 +60,6 @@ workflows: tags: only: /.*/ requires: - - run_tests_35 - run_tests_36 - run_tests_37 - reencryption_benchmark_36: @@ -86,7 +67,6 @@ workflows: tags: only: /.*/ # requires: # Commented this section to speed up the build -# - run_tests_35 # - run_tests_36 # - run_tests_37 - test_deploy: @@ -119,11 +99,6 @@ workflows: branches: ignore: /.*/ -python_35_base: &python_35_base - working_directory: ~/pyUmbral-35 - docker: - - image: circleci/python:3.5 - python_36_base: &python_36_base working_directory: ~/pyUmbral-36 docker: @@ -190,12 +165,6 @@ commands: jobs: - pipenv_install_35: - <<: *python_35_base - steps: - - pipenv_install: - python_version: "3.5" - pipenv_install_36: <<: *python_36_base steps: @@ -208,11 +177,6 @@ jobs: - pipenv_install: python_version: "3.7" - pip_install_35: - <<: *python_35_base - steps: - - pip_install - pip_install_36: <<: *python_36_base steps: @@ -223,13 +187,6 @@ jobs: steps: - pip_install - run_tests_35: - <<: *python_35_base - parallelism: 4 - steps: - - run_tests: - python_version: "3.5" - run_tests_36: <<: *python_36_base parallelism: 4 diff --git a/setup.py b/setup.py index e7b2b8ee..dd6a57c4 100644 --- a/setup.py +++ b/setup.py @@ -105,7 +105,6 @@ def run(self): "Natural Language :: English", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Scientific/Engineering", From 2050e3c16813088395663b0c0c497fa7a5045a58 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 22:44:59 -0700 Subject: [PATCH 03/25] Remove unused dependencies --- Pipfile | 5 - Pipfile.lock | 1119 ++++++++++++++++++++++++++++---------------------- setup.py | 3 - 3 files changed, 628 insertions(+), 499 deletions(-) diff --git a/Pipfile b/Pipfile index d9c3c7f6..a6e4579a 100644 --- a/Pipfile +++ b/Pipfile @@ -5,13 +5,8 @@ name = "pypi" [packages] setuptools = "*" -# Third Party cryptography = ">=2.3" pynacl = "*" -pysha3 = "*" -# NuCypher -bytestring-splitter = "*" -constant-sorrow = ">=0.1.0a7" [dev-packages] bumpversion = "*" diff --git a/Pipfile.lock b/Pipfile.lock index b4d4b566..6e03671b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "60c034332445dc29a4f621268c67609c5efb4b0d76ee7fa586ea8436cf70866a" + "sha256": "55db9d5f9de5a65ef3a43b7931e19d8a1ee2cb9cbfa9e616fa2c512f8576116b" }, "pipfile-spec": 6, "requires": {}, @@ -14,157 +14,105 @@ ] }, "default": { - "asn1crypto": { - "hashes": [ - "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", - "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" - ], - "version": "==0.24.0" - }, - "bytestring-splitter": { - "hashes": [ - "sha256:708f70a171fbd5d28f8374e18b8fe00d931bdc908069611cb866397a8fbb34e5", - "sha256:a681e2fc5a6bd8b083ce740fc4353b34492af63d0891e59c05cd0300f79f91d8" - ], - "index": "pypi", - "version": "==1.0.0a4" - }, "cffi": { "hashes": [ - "sha256:0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6", - "sha256:27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709", - "sha256:2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a", - "sha256:3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282", - "sha256:358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556", - "sha256:37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134", - "sha256:39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d", - "sha256:42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da", - "sha256:5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1", - "sha256:5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509", - "sha256:5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1", - "sha256:7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3", - "sha256:7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96", - "sha256:7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2", - "sha256:8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28", - "sha256:9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c", - "sha256:9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033", - "sha256:a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d", - "sha256:aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea", - "sha256:bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0", - "sha256:bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469", - "sha256:bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071", - "sha256:c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719", - "sha256:d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a", - "sha256:df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6", - "sha256:f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a", - "sha256:f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf", - "sha256:f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729" - ], - "version": "==1.12.1" - }, - "constant-sorrow": { - "hashes": [ - "sha256:0ab5ddacde6484e986703a523b721517b7230e395d2bfddea5eced7153acbf9c", - "sha256:8ff487bbc15d4ddcae7f0f745c56494f4267e90b20b19102d81b390803e14ded" - ], - "index": "pypi", - "version": "==0.1.0a8" + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "version": "==1.14.5" }, "cryptography": { "hashes": [ - "sha256:05b3ded5e88747d28ee3ef493f2b92cbb947c1e45cf98cfef22e6d38bb67d4af", - "sha256:06826e7f72d1770e186e9c90e76b4f84d90cdb917b47ff88d8dc59a7b10e2b1e", - "sha256:08b753df3672b7066e74376f42ce8fc4683e4fd1358d34c80f502e939ee944d2", - "sha256:2cd29bd1911782baaee890544c653bb03ec7d95ebeb144d714b0f5c33deb55c7", - "sha256:31e5637e9036d966824edaa91bf0aa39dc6f525a1c599f39fd5c50340264e079", - "sha256:42fad67d7072216a49e34f923d8cbda9edacbf6633b19a79655e88a1b4857063", - "sha256:4946b67235b9d2ea7d31307be9d5ad5959d6c4a8f98f900157b47abddf698401", - "sha256:522fdb2809603ee97a4d0ef2f8d617bc791eb483313ba307cb9c0a773e5e5695", - "sha256:6f841c7272645dd7c65b07b7108adfa8af0aaea57f27b7f59e01d41f75444c85", - "sha256:7d335e35306af5b9bc0560ca39f740dfc8def72749645e193dd35be11fb323b3", - "sha256:8504661ffe324837f5c4607347eeee4cf0fcad689163c6e9c8d3b18cf1f4a4ad", - "sha256:9260b201ce584d7825d900c88700aa0bd6b40d4ebac7b213857bd2babee9dbca", - "sha256:9a30384cc402eac099210ab9b8801b2ae21e591831253883decdb4513b77a3cd", - "sha256:9e29af877c29338f0cab5f049ccc8bd3ead289a557f144376c4fbc7d1b98914f", - "sha256:ab50da871bc109b2d9389259aac269dd1b7c7413ee02d06fe4e486ed26882159", - "sha256:b13c80b877e73bcb6f012813c6f4a9334fcf4b0e96681c5a15dac578f2eedfa0", - "sha256:bfe66b577a7118e05b04141f0f1ed0959552d45672aa7ecb3d91e319d846001e", - "sha256:e091bd424567efa4b9d94287a952597c05d22155a13716bf5f9f746b9dc906d3", - "sha256:fa2b38c8519c5a3aa6e2b4e1cf1a549b54acda6adb25397ff542068e73d1ed00" + "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b", + "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336", + "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87", + "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7", + "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799", + "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b", + "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df", + "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0", + "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3", + "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724", + "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2", + "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964" ], "index": "pypi", - "version": "==2.5" - }, - "msgpack-python": { - "hashes": [ - "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" - ], - "version": "==0.5.6" + "version": "==3.4.6" }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "version": "==2.19" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" }, "pynacl": { "hashes": [ - "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", - "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", - "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", - "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", - "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", - "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", - "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", - "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", - "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", - "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", - "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", - "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", - "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", - "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", - "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", - "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", - "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", - "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", - "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" + "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", + "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", + "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634", + "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", + "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", + "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", + "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", + "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", + "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", + "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", + "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6", + "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", + "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", + "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" ], "index": "pypi", - "version": "==1.3.0" - }, - "pysha3": { - "hashes": [ - "sha256:0060a66be16665d90c432f55a0ba1f6480590cfb7d2ad389e688a399183474f0", - "sha256:11a2ba7a2e1d9669d0052fc8fb30f5661caed5512586ecbeeaf6bf9478ab5c48", - "sha256:386998ee83e313b6911327174e088021f9f2061cbfa1651b97629b761e9ef5c4", - "sha256:41be70b06c8775a9e4d4eeb52f2f6a3f356f17539a54eac61f43a29e42fd453d", - "sha256:4416f16b0f1605c25f627966f76873e432971824778b369bd9ce1bb63d6566d9", - "sha256:571a246308a7b63f15f5aa9651f99cf30f2a6acba18eddf28f1510935968b603", - "sha256:59111c08b8f34495575d12e5f2ce3bafb98bea470bc81e70c8b6df99aef0dd2f", - "sha256:5ec8da7c5c70a53b5fa99094af3ba8d343955b212bc346a0d25f6ff75853999f", - "sha256:684cb01d87ed6ff466c135f1c83e7e4042d0fc668fa20619f581e6add1d38d77", - "sha256:68c3a60a39f9179b263d29e221c1bd6e01353178b14323c39cc70593c30f21c5", - "sha256:6e6a84efb7856f5d760ee55cd2b446972cb7b835676065f6c4f694913ea8f8d9", - "sha256:827b308dc025efe9b6b7bae36c2e09ed0118a81f792d888548188e97b9bf9a3d", - "sha256:93abd775dac570cb9951c4e423bcb2bc6303a9d1dc0dc2b7afa2dd401d195b24", - "sha256:9c778fa8b161dc9348dc5cc361e94d54aa5ff18413788f4641f6600d4893a608", - "sha256:9fdd28884c5d0b4edfed269b12badfa07f1c89dbc5c9c66dd279833894a9896b", - "sha256:c7c2adcc43836223680ebdf91f1d3373543dc32747c182c8ca2e02d1b69ce030", - "sha256:c93a2676e6588abcfaecb73eb14485c81c63b94fca2000a811a7b4fb5937b8e8", - "sha256:cd5c961b603bd2e6c2b5ef9976f3238a561c58569945d4165efb9b9383b050ef", - "sha256:f9046d59b3e72aa84f6dae83a040bd1184ebd7fef4e822d38186a8158c89e3cf", - "sha256:fd7e66999060d079e9c0e8893e78d8017dad4f59721f6fe0be6307cd32127a07", - "sha256:fe988e73f2ce6d947220624f04d467faf05f1bbdbc64b0a201296bb3af92739e" - ], - "index": "pypi", - "version": "==1.0.2" + "version": "==1.4.0" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.12.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" } }, "develop": { @@ -177,170 +125,209 @@ }, "appnope": { "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, - "argh": { - "hashes": [ - "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", - "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" + "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442", + "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a" ], - "version": "==0.26.2" - }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "version": "==1.3.0" + "markers": "sys_platform == 'darwin' and platform_system == 'Darwin'", + "version": "==0.1.2" }, "attrs": { "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==18.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "babel": { "hashes": [ - "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", - "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", + "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], - "version": "==2.6.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0" }, "backcall": { "hashes": [ - "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", - "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" ], - "version": "==0.1.0" + "version": "==0.2.0" + }, + "bump2version": { + "hashes": [ + "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", + "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.1" }, "bumpversion": { "hashes": [ - "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", - "sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57" + "sha256:4ba55e4080d373f80177b4dabef146c07ce73c7d1377aabf9d3c3ae1f94584a6", + "sha256:4eb3267a38194d09f048a2179980bb4803701969bff2c85fa8f6d1ce050be15e" ], "index": "pypi", - "version": "==0.5.3" + "version": "==0.6.0" }, "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2018.11.29" + "version": "==2020.12.5" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, - "click": { + "codecov": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:6cde272454009d27355f9434f4e49f238c0273b216beda8472a65dc4957f473b", + "sha256:ba8553a82942ce37d4da92b70ffd6d54cf635fc1793ab0a7dc3fecd6ebfb3df8", + "sha256:e95901d4350e99fc39c8353efa450050d2446c55bac91d90fcfd2354e19a6aef" ], - "version": "==7.0" + "index": "pypi", + "version": "==2.1.11" }, - "codecov": { + "colorama": { "hashes": [ - "sha256:8ed8b7c6791010d359baed66f84f061bba5bd41174bf324c31311e8737602788", - "sha256:ae00d68e18d8a20e9c3288ba3875ae03db3a8e892115bf9b83ef20507732bed4" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "index": "pypi", - "version": "==2.0.15" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.4" }, "coverage": { "hashes": [ - "sha256:029c69deaeeeae1b15bc6c59f0ffa28aa8473721c614a23f2c2976dec245cd12", - "sha256:02abbbebc6e9d5abe13cd28b5e963dedb6ffb51c146c916d17b18f141acd9947", - "sha256:1bbfe5b82a3921d285e999c6d256c1e16b31c554c29da62d326f86c173d30337", - "sha256:210c02f923df33a8d0e461c86fdcbbb17228ff4f6d92609fc06370a98d283c2d", - "sha256:2d0807ba935f540d20b49d5bf1c0237b90ce81e133402feda906e540003f2f7a", - "sha256:35d7a013874a7c927ce997350d314144ffc5465faf787bb4e46e6c4f381ef562", - "sha256:3636f9d0dcb01aed4180ef2e57a4e34bb4cac3ecd203c2a23db8526d86ab2fb4", - "sha256:42f4be770af2455a75e4640f033a82c62f3fb0d7a074123266e143269d7010ef", - "sha256:48440b25ba6cda72d4c638f3a9efa827b5b87b489c96ab5f4ff597d976413156", - "sha256:4dac8dfd1acf6a3ac657475dfdc66c621f291b1b7422a939cc33c13ac5356473", - "sha256:4e8474771c69c2991d5eab65764289a7dd450bbea050bc0ebb42b678d8222b42", - "sha256:551f10ddfeff56a1325e5a34eff304c5892aa981fd810babb98bfee77ee2fb17", - "sha256:5b104982f1809c1577912519eb249f17d9d7e66304ad026666cb60a5ef73309c", - "sha256:5c62aef73dfc87bfcca32cee149a1a7a602bc74bac72223236b0023543511c88", - "sha256:633151f8d1ad9467b9f7e90854a7f46ed8f2919e8bc7d98d737833e8938fc081", - "sha256:772207b9e2d5bf3f9d283b88915723e4e92d9a62c83f44ec92b9bd0cd685541b", - "sha256:7d5e02f647cd727afc2659ec14d4d1cc0508c47e6cfb07aea33d7aa9ca94d288", - "sha256:a9798a4111abb0f94584000ba2a2c74841f2cfe5f9254709756367aabbae0541", - "sha256:b38ea741ab9e35bfa7015c93c93bbd6a1623428f97a67083fc8ebd366238b91f", - "sha256:b6a5478c904236543c0347db8a05fac6fc0bd574c870e7970faa88e1d9890044", - "sha256:c6248bfc1de36a3844685a2e10ba17c18119ba6252547f921062a323fb31bff1", - "sha256:c705ab445936457359b1424ef25ccc0098b0491b26064677c39f1d14a539f056", - "sha256:d95a363d663ceee647291131dbd213af258df24f41350246842481ec3709bd33", - "sha256:e27265eb80cdc5dab55a40ef6f890e04ecc618649ad3da5265f128b141f93f78", - "sha256:ebc276c9cb5d917bd2ae959f84ffc279acafa9c9b50b0fa436ebb70bbe2166ea", - "sha256:f4d229866d030863d0fe3bf297d6d11e6133ca15bbb41ed2534a8b9a3d6bd061", - "sha256:f95675bd88b51474d4fe5165f3266f419ce754ffadfb97f10323931fa9ac95e5", - "sha256:f95bc54fb6d61b9f9ff09c4ae8ff6a3f5edc937cda3ca36fc937302a7c152bf1", - "sha256:fd0f6be53de40683584e5331c341e65a679dbe5ec489a0697cec7c2ef1a48cda" + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], "index": "pypi", - "version": "==5.0a4" + "version": "==5.5" }, "decorator": { "hashes": [ - "sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e", - "sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b" + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" ], - "version": "==4.3.2" + "version": "==4.4.2" }, "docutils": { "hashes": [ - "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", - "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", - "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.16" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" ], - "version": "==0.14" + "version": "==3.0.12" }, "hypothesis": { "hashes": [ - "sha256:7f97f984d45f22b8895ee4d98c7f48e7ebc2c2fbf9d5ce07dad77989997821f6", - "sha256:cb8c89295c36db0014409c8d7c897094f538cb880a25e3dc3e86fb768787a03d", - "sha256:e83442d2f74ceb1d166e351308810b394802fd9758c3b89ae36ca8daadac9fd4" + "sha256:2dd38676402d1c218225210cde0cf19f286352279f32631ac5c801f5d767bc94", + "sha256:3b7d9f7e40e406b550d4fd26fef0ce3fad216f163a3400ab701329b865e25876" ], "index": "pypi", - "version": "==4.6.1" + "version": "==6.8.1" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "imagesize": { "hashes": [ - "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", - "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" + "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", + "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "version": "==1.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" }, "ipykernel": { "hashes": [ - "sha256:0aeb7ec277ac42cc2b59ae3d08b10909b2ec161dc6908096210527162b53675d", - "sha256:0fc0bf97920d454102168ec2008620066878848fcfca06c22b669696212e292f" + "sha256:98321abefdf0505fb3dc7601f60fc4087364d394bd8fad53107eb1adee9ff475", + "sha256:efd07253b54d84d26e0878d268c8c3a41582a18750da633c2febfd2ece0d467d" ], - "version": "==5.1.0" + "markers": "python_version >= '3.5'", + "version": "==5.5.0" }, "ipython": { "hashes": [ - "sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39", - "sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82" + "sha256:04323f72d5b85b606330b6d7e2dc8d2683ad46c3905e955aa96ecc7a99388e70", + "sha256:34207ffb2f653bced2bc8e3756c1db86e7d93e44ed049daae9814fed66d408ec" ], - "version": "==7.3.0" + "markers": "python_version >= '3.7'", + "version": "==7.21.0" }, "ipython-genutils": { "hashes": [ @@ -351,167 +338,205 @@ }, "jedi": { "hashes": [ - "sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd", - "sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191" + "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", + "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707" ], - "version": "==0.13.2" + "markers": "python_version >= '3.6'", + "version": "==0.18.0" }, "jinja2": { "hashes": [ - "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", - "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], - "version": "==2.10" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.11.3" }, "jsonschema": { "hashes": [ - "sha256:683fe7ed58763ea0be572de5aad47cd3cc1297640916f9a8ccd222b287da7d2f", - "sha256:b42d7a292addb57370e6260bcbadb77e00a899fe6ec998c453f45893c41c658b" + "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", + "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" ], - "version": "==3.0.0b3" + "version": "==3.2.0" }, "jupyter-client": { "hashes": [ - "sha256:b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551", - "sha256:c44411eb1463ed77548bc2d5ec0d744c9b81c4a542d9637c7a52824e2121b987" + "sha256:c4bca1d0846186ca8be97f4d2fa6d2bae889cce4892a167ffa1ba6bd1f73e782", + "sha256:e053a2c44b6fa597feebe2b3ecb5eea3e03d1d91cc94351a52931ee1426aecfc" ], - "version": "==5.2.4" + "markers": "python_version >= '3.5'", + "version": "==6.1.12" }, "jupyter-core": { "hashes": [ - "sha256:927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970", - "sha256:ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7" + "sha256:79025cb3225efcd36847d0840f3fc672c0abd7afd0de83ba8a1d3837619122b4", + "sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e" + ], + "markers": "python_version >= '3.6'", + "version": "==4.7.1" + }, + "libcst": { + "hashes": [ + "sha256:2766671c107263daa3fc34e39d55134a6fe253701564d7670586f30eee2c201c", + "sha256:4638e4e8f166f4c74df399222d347ce3e1d316e206b550d8a6254d51b4cf7275" ], - "version": "==4.4.0" + "markers": "python_version >= '3.6'", + "version": "==0.3.17" }, "livereload": { "hashes": [ - "sha256:29cadfabcedd12eed792e0131991235b9d4764d4474bed75cf525f57109ec0a2", - "sha256:e632a6cd1d349155c1d7f13a65be873b38f43ef02961804a1bba8d817fa649a7" + "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869" ], - "version": "==2.6.0" + "version": "==2.6.3" }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" - ], - "version": "==1.1.0" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.1" }, "mock": { "hashes": [ - "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", - "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" + "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", + "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" ], "index": "pypi", - "version": "==2.0.0" + "version": "==4.0.3" }, "monkeytype": { "hashes": [ - "sha256:baeeee422c17202038ccf17ca73eb97eddb65a4178a215c1ff212cfb7373eb65", - "sha256:ecee4162a153c8a0d2151dfc66f06ebb82e4582b0d46281798d908888bb0c9b9" + "sha256:b8ed88485d2ffb05fb1597a6e5eacb05ba5420de682054403c06fac84fdc4038", + "sha256:fe596bebc5e1b6a64eae71a40b880688de433e4f70507a31ada48510195251dd" ], "index": "pypi", - "version": "==19.1.1" - }, - "more-itertools": { - "hashes": [ - "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", - "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" - ], - "markers": "python_version > '2.7'", - "version": "==6.0.0" + "version": "==20.5.0" }, "mypy": { "hashes": [ - "sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7", - "sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d" + "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", + "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", + "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", + "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", + "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", + "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", + "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", + "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", + "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", + "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", + "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", + "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", + "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", + "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", + "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", + "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", + "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", + "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", + "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", + "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", + "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", + "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" ], "index": "pypi", - "version": "==0.670" + "version": "==0.812" }, "mypy-extensions": { "hashes": [ - "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", - "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" ], - "version": "==0.4.1" + "version": "==0.4.3" }, "nbformat": { "hashes": [ - "sha256:b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b", - "sha256:f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402" + "sha256:1d223e64a18bfa7cdf2db2e9ba8a818312fc2a0701d2e910b58df66809385a56", + "sha256:3949fdc8f5fa0b1afca16fb307546e78494fa7a7bceff880df8168eafda0e7ac" ], - "version": "==4.4.0" + "markers": "python_version >= '3.5'", + "version": "==5.1.2" }, "nbval": { "hashes": [ - "sha256:3f18b87af4e94ccd073263dd58cd3eebabe9f5e4d6ab535b39d3af64811c7eda", - "sha256:74ff5e2c90a50b1ddf7edd02978c4e43221b1ee252dc14fcaa4230aae4492eda" + "sha256:4f9b780997d8942408853513f2c5ee6c1863de193559fc3f95e1c1cde8110439", + "sha256:cfefcd2ef66ee2d337d0b252c6bcec4023384eb32e8b9e5fcc3ac80ab8cd7d40" ], "index": "pypi", - "version": "==0.9.1" + "version": "==0.9.6" }, "packaging": { "hashes": [ - "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", - "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "version": "==19.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" }, "parso": { "hashes": [ - "sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9", - "sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db" + "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410", + "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e" ], - "version": "==0.3.4" - }, - "pathtools": { - "hashes": [ - "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0" - ], - "version": "==0.1.2" - }, - "pbr": { - "hashes": [ - "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2", - "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b" - ], - "version": "==5.1.2" + "markers": "python_version >= '3.6'", + "version": "==0.8.1" }, "pexpect": { "hashes": [ - "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", - "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" ], "markers": "sys_platform != 'win32'", - "version": "==4.6.0" + "version": "==4.8.0" }, "pickleshare": { "hashes": [ @@ -522,44 +547,40 @@ }, "pluggy": { "hashes": [ - "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", - "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.8.1" - }, - "port-for": { - "hashes": [ - "sha256:b16a84bb29c2954db44c29be38b17c659c9c27e33918dec16b90d375cc596f1c" - ], - "version": "==0.3.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" }, "prompt-toolkit": { "hashes": [ - "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", - "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", - "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" + "sha256:4cea7d09e46723885cb8bc54678175453e5071e9449821dce6f017b1d1fbfc1a", + "sha256:9397a7162cf45449147ad6042fa37983a081b8a73363a5253dd4072666333137" ], - "version": "==2.0.9" + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.17" }, "ptyprocess": { "hashes": [ - "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", - "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" ], - "version": "==0.6.0" + "version": "==0.7.0" }, "py": { "hashes": [ - "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", - "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "version": "==1.7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" }, "py-cpuinfo": { "hashes": [ - "sha256:6615d4527118d4ea1db4d86dac4340725b3906aa04bf36b7902f7af4425fb25f" + "sha256:9aa2e49675114959697d25cf57fec41c29b55887bff3bc4809b44ac6f5730097" ], - "version": "==4.0.0" + "version": "==7.0.0" }, "pygal": { "hashes": [ @@ -570,279 +591,395 @@ }, "pygaljs": { "hashes": [ - "sha256:bd778749bc387a71520dde99f36a75ea776b3dcde5f3b59b4bd079c5723b1eb8", - "sha256:be757bc75f52b46669472f6cc7eac95935092495bef153b8810a46c0c59f88a1" + "sha256:0b71ee32495dcba5fbb4a0476ddbba07658ad65f5675e4ad409baf154dec5111", + "sha256:d75e18cb21cc2cda40c45c3ee690771e5e3d4652bf57206f20137cf475c0dbe8" ], - "version": "==1.0.1" + "version": "==1.0.2" }, "pygments": { "hashes": [ - "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", - "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", + "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" ], - "version": "==2.3.1" + "markers": "python_version >= '3.5'", + "version": "==2.8.1" }, "pyparsing": { "hashes": [ - "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", - "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==2.3.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" }, "pyrsistent": { "hashes": [ - "sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf" + "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" ], - "version": "==0.14.10" + "markers": "python_version >= '3.5'", + "version": "==0.17.3" }, "pytest": { "hashes": [ - "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", - "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" + "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", + "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" ], "index": "pypi", - "version": "==4.3.0" + "version": "==6.2.2" }, "pytest-benchmark": { "extras": [ "histogram" ], "hashes": [ - "sha256:4512c6805318d07926efcb3b39f7b98a10d035305a93edfd5329c86cbf9cfbf7", - "sha256:ab851115ce022639173b9497d4a4183a1d8fe9cdcf8fab9d8a57607008aedd3d" + "sha256:01f79d38d506f5a3a0a9ada22ded714537bbdfc8147a881a35c1655db07289d9", + "sha256:ad4314d093a3089701b24c80a05121994c7765ce373478c8f4ba8d23c9ba9528" ], "index": "pypi", - "version": "==3.2.2" + "version": "==3.2.3" }, "pytest-cov": { "hashes": [ - "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", - "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" + "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", + "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" ], "index": "pypi", - "version": "==2.6.1" + "version": "==2.11.1" }, "pytest-mock": { "hashes": [ - "sha256:4d0d06d173eecf172703219a71dbd4ade0e13904e6bbce1ce660e2e0dc78b5c4", - "sha256:bfdf02789e3d197bd682a758cae0a4a18706566395fbe2803badcd1335e0173e" + "sha256:379b391cfad22422ea2e252bdfc008edd08509029bcde3c25b2c0bd741e0424e", + "sha256:a1e2aba6af9560d313c642dae7e00a2a12b022b80301d9d7fc8ec6858e1dd9fc" ], "index": "pypi", - "version": "==1.10.1" + "version": "==3.5.1" }, "pytest-mypy": { "hashes": [ - "sha256:8f6436eed8118afd6c10a82b3b60fb537336736b0fd7a29262a656ac42ce01ac", - "sha256:acc653210e7d8d5c72845a5248f00fd33f4f3379ca13fe56cfc7b749b5655c3e" + "sha256:63d418a4fea7d598ac40b659723c00804d16a251d90a5cfbca213eeba5aaf01c", + "sha256:8d2112972c1debf087943f48963a0daf04f3424840aea0cf437cc97053b1b0ef" ], "index": "pypi", - "version": "==0.3.2" + "version": "==0.8.0" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "version": "==2.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.1" }, "pytz": { "hashes": [ - "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", - "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2018.9" + "version": "==2021.1" }, "pyyaml": { "hashes": [ - "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", - "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", - "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", - "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", - "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" - ], - "version": "==4.2b4" + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==5.4.1" }, "pyzmq": { "hashes": [ - "sha256:07a03450418694fb07e76a0191b6bc9f411afc8e364ca2062edcf28bb0e51c63", - "sha256:15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b", - "sha256:1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6", - "sha256:1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4", - "sha256:228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc", - "sha256:264ac9dcee6a7af2bce4b61f2d19e5926118a5caa629b50f107ef6318670a364", - "sha256:2b5a43da65f5dec857184d5c2ce13b80071019e96358f146bdecff7238765bc9", - "sha256:3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba", - "sha256:3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a", - "sha256:411def5b4cbe6111856040a55c8048df113882e90c57ce88de4a48f0189441ac", - "sha256:4b77e96a7ffc1c5e08eaf274db554f227b31717d086adca1bb42b12ef35a7194", - "sha256:4c87fa3e449e1f4ab9170cdfe8213dc0ba34a11b160e6adecafa892e451a29b6", - "sha256:4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda", - "sha256:54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba", - "sha256:60acabd86808a16a895a247fd2bf7a127284a33562d79687bb5df163cff068b2", - "sha256:618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac", - "sha256:69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae", - "sha256:6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1", - "sha256:6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9", - "sha256:75c87f1dc1e65cea4b709f2ebc78fa18d4b475e41463502aec9cd26208b88e0f", - "sha256:97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c", - "sha256:abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69", - "sha256:ae3d2627d74195ddc95675f2f814aca998381b73dc4341b9e10e3e191e1bdb0b", - "sha256:b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b", - "sha256:cbabdced5b137cd56aa22633f13ac5690029a0ad43ab6c05f53206e489178362" - ], - "version": "==18.0.0" + "sha256:13465c1ff969cab328bc92f7015ce3843f6e35f8871ad79d236e4fbc85dbe4cb", + "sha256:23a74de4b43c05c3044aeba0d1f3970def8f916151a712a3ac1e5cd9c0bc2902", + "sha256:26380487eae4034d6c2a3fb8d0f2dff6dd0d9dd711894e8d25aa2d1938950a33", + "sha256:279cc9b51db48bec2db146f38e336049ac5a59e5f12fb3a8ad864e238c1c62e3", + "sha256:2f971431aaebe0a8b54ac018e041c2f0b949a43745444e4dadcc80d0f0ef8457", + "sha256:30df70f81fe210506aa354d7fd486a39b87d9f7f24c3d3f4f698ec5d96b8c084", + "sha256:33acd2b9790818b9d00526135acf12790649d8d34b2b04d64558b469c9d86820", + "sha256:38e3dca75d81bec4f2defa14b0a65b74545812bb519a8e89c8df96bbf4639356", + "sha256:3e29f9cf85a40d521d048b55c63f59d6c772ac1c4bf51cdfc23b62a62e377c33", + "sha256:3ef50d74469b03725d781a2a03c57537d86847ccde587130fe35caafea8f75c6", + "sha256:4231943514812dfb74f44eadcf85e8dd8cf302b4d0bce450ce1357cac88dbfdc", + "sha256:4f34a173f813b38b83f058e267e30465ed64b22cd0cf6bad21148d3fa718f9bb", + "sha256:532af3e6dddea62d9c49062ece5add998c9823c2419da943cf95589f56737de0", + "sha256:581787c62eaa0e0db6c5413cedc393ebbadac6ddfd22e1cf9a60da23c4f1a4b2", + "sha256:60e63577b85055e4cc43892fecd877b86695ee3ef12d5d10a3c5d6e77a7cc1a3", + "sha256:61e4bb6cd60caf1abcd796c3f48395e22c5b486eeca6f3a8797975c57d94b03e", + "sha256:6d4163704201fff0f3ab0cd5d7a0ea1514ecfffd3926d62ec7e740a04d2012c7", + "sha256:7026f0353977431fc884abd4ac28268894bd1a780ba84bb266d470b0ec26d2ed", + "sha256:763c175294d861869f18eb42901d500eda7d3fa4565f160b3b2fd2678ea0ebab", + "sha256:81e7df0da456206201e226491aa1fc449da85328bf33bbeec2c03bb3a9f18324", + "sha256:9221783dacb419604d5345d0e097bddef4459a9a95322de6c306bf1d9896559f", + "sha256:a558c5bc89d56d7253187dccc4e81b5bb0eac5ae9511eb4951910a1245d04622", + "sha256:b25e5d339550a850f7e919fe8cb4c8eabe4c917613db48dab3df19bfb9a28969", + "sha256:b62ea18c0458a65ccd5be90f276f7a5a3f26a6dea0066d948ce2fa896051420f", + "sha256:c0cde362075ee8f3d2b0353b283e203c2200243b5a15d5c5c03b78112a17e7d4", + "sha256:c5e29fe4678f97ce429f076a2a049a3d0b2660ada8f2c621e5dc9939426056dd", + "sha256:d18ddc6741b51f3985978f2fda57ddcdae359662d7a6b395bc8ff2292fca14bd", + "sha256:da7d4d4c778c86b60949d17531e60c54ed3726878de8a7f8a6d6e7f8cc8c3205", + "sha256:f52070871a0fd90a99130babf21f8af192304ec1e995bec2a9533efc21ea4452", + "sha256:f5831eff6b125992ec65d973f5151c48003b6754030094723ac4c6e80a97c8c4", + "sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d", + "sha256:ff1ea14075bbddd6f29bf6beb8a46d0db779bcec6b9820909584081ec119f8fd" + ], + "markers": "python_version >= '3.6'", + "version": "==22.0.3" }, "requests": { "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], "index": "pypi", - "version": "==2.21.0" + "version": "==2.25.1" }, - "retype": { + "six": { "hashes": [ - "sha256:33cfb36601bfeb355924731d8db78fa82f3f12eb37e87236e9179d81aba97740", - "sha256:b64b767befbe6f5fd918603ab7d6bbff07fc4c431bae2f471e195677a0c9b327" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==17.12.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, - "six": { + "snowballstemmer": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==1.12.0" + "version": "==2.1.0" }, - "snowballstemmer": { + "sortedcontainers": { "hashes": [ - "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", - "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f", + "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1" ], - "version": "==1.2.1" + "version": "==2.3.0" }, "sphinx": { "hashes": [ - "sha256:230af939a2f678ab4f2a0a948c3b24a822a0d280821859caaefb750ef7413003", - "sha256:835c701420102a0a71ba2ed54a5bada2da6fd01263bf6dc8c5c80c798e27709c" + "sha256:672cfcc24b6b69235c97c750cb190a44ecd72696b4452acaf75c2d9cc78ca5ff", + "sha256:ef64a814576f46ec7de06adf11b433a0d6049be007fefe7fd0d183d28b581fac" ], "index": "pypi", - "version": "==2.0.0b1" + "version": "==3.5.2" }, "sphinx-autobuild": { "hashes": [ - "sha256:66388f81884666e3821edbe05dd53a0cfb68093873d17320d0610de8db28c74e", - "sha256:e60aea0789cab02fa32ee63c7acae5ef41c06f1434d9fd0a74250a61f5994692" + "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", + "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05" ], "index": "pypi", - "version": "==0.7.1" + "version": "==2021.3.14" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", - "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a" + "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5", + "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113" ], "index": "pypi", - "version": "==0.4.3" + "version": "==0.5.1" }, "sphinxcontrib-applehelp": { "hashes": [ - "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", - "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" + "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", + "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "version": "==1.0.1" + "markers": "python_version >= '3.5'", + "version": "==1.0.2" }, "sphinxcontrib-devhelp": { "hashes": [ - "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", - "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" + "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", + "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "version": "==1.0.1" + "markers": "python_version >= '3.5'", + "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:0d691ca8edf5995fbacfe69b191914256071a94cbad03c3688dca47385c9206c", - "sha256:e31c8271f5a8f04b620a500c0442a7d5cfc1a732fa5c10ec363f90fe72af0cb8" + "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", + "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "version": "==1.0.1" + "markers": "python_version >= '3.5'", + "version": "==1.0.3" }, "sphinxcontrib-jsmath": { "hashes": [ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { "hashes": [ - "sha256:18ec9f74ea2c92fd512d5f3b532d6ab4ac2be76b12cc2490f7729842ba2a60c9", - "sha256:f39159b45de6d3d86c30874a3220be4f8e75ed12c71aff50cb8b2cac46e240f0" + "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", + "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "version": "==1.0.1" + "markers": "python_version >= '3.5'", + "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:01d9b2617d7e8ddf7a00cae091f08f9fa4db587cc160b493141ee56710810932", - "sha256:392187ac558863b8aff0d76dc78e0731fed58f3b06e2b00e22995dcdb630f213" + "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", + "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "version": "==1.1.1" + "markers": "python_version >= '3.5'", + "version": "==1.1.4" }, - "tornado": { + "toml": { "hashes": [ - "sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==6.0b1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tornado": { + "hashes": [ + "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", + "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", + "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", + "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", + "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", + "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", + "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", + "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", + "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", + "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", + "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", + "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", + "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", + "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", + "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", + "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", + "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", + "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", + "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", + "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", + "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", + "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", + "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", + "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", + "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", + "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", + "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", + "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", + "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", + "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", + "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", + "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", + "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", + "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", + "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", + "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", + "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", + "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", + "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", + "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", + "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" + ], + "markers": "python_version >= '3.5'", + "version": "==6.1" }, "traitlets": { "hashes": [ - "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", - "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", + "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" ], - "version": "==4.3.2" + "markers": "python_version >= '3.7'", + "version": "==5.0.5" }, "typed-ast": { "hashes": [ - "sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23", - "sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15", - "sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3", - "sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d", - "sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6", - "sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60", - "sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773", - "sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424", - "sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287", - "sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99", - "sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23", - "sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8", - "sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699", - "sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1", - "sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463", - "sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6", - "sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0", - "sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0", - "sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6" - ], - "version": "==1.3.1" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" + ], + "version": "==1.4.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "version": "==3.7.4.3" + }, + "typing-inspect": { + "hashes": [ + "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f", + "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7", + "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0" ], - "version": "==1.24.1" + "version": "==0.6.0" }, - "watchdog": { + "urllib3": { "hashes": [ - "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "version": "==0.9.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.4" }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" ], - "version": "==0.1.7" + "version": "==0.2.5" } } } diff --git a/setup.py b/setup.py index dd6a57c4..ca53b41e 100644 --- a/setup.py +++ b/setup.py @@ -60,9 +60,6 @@ def run(self): 'setuptools', 'cryptography>=2.3', 'pynacl', - 'pysha3', - 'constant-sorrow>=0.1.0a7', - 'bytestring-splitter', ] DEV_INSTALL_REQUIRES = [ From 54ba9bb38feb95d4f7b6618ad70dad911c45a1d0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 20 Mar 2021 16:29:49 -0700 Subject: [PATCH 04/25] Exclude abstract methods during coverage tests --- .coveragerc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.coveragerc b/.coveragerc index 7ea406ea..cb912888 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,11 @@ omit = setup.py, *__init__.py parallel=True + +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Exclude abstract methods + @abstractmethod From f030fd335335a1a316a7bf149610279a2556f9c6 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 16 Mar 2021 16:39:52 -0700 Subject: [PATCH 05/25] Use a composable approach for serialization --- umbral/serializable.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 umbral/serializable.py diff --git a/umbral/serializable.py b/umbral/serializable.py new file mode 100644 index 00000000..196e0f5a --- /dev/null +++ b/umbral/serializable.py @@ -0,0 +1,37 @@ +from abc import abstractmethod, ABC +from typing import Tuple, Type, List, Any, TypeVar + + +class Serializable(ABC): + + _T = TypeVar('_T', bound='Serializable') + + @classmethod + def from_bytes(cls: Type[_T], data: bytes) -> _T: + obj, remainder = cls.__take__(data) + if len(remainder) != 0: + raise ValueError(f"{len(remainder)} bytes remaining after deserializing {cls}") + return obj + + @classmethod + def __take_bytes__(cls, data: bytes, size: int) -> Tuple[bytes, bytes]: + if len(data) < size: + raise ValueError(f"{cls} cannot take {size} bytes from a bytestring of size {len(data)}") + return data[:size], data[size:] + + @classmethod + def __take_types__(cls, data: bytes, *types: Type) -> Tuple[List[Any], bytes]: + objs = [] + for tp in types: + obj, data = tp.__take__(data) + objs.append(obj) + return objs, data + + @classmethod + @abstractmethod + def __take__(cls: Type[_T], data: bytes) -> Tuple[_T, bytes]: + raise NotImplementedError + + @abstractmethod + def __bytes__(self): + raise NotImplementedError From f33431d92a96cf1df077b4ce86807273dad05fe0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 15 Mar 2021 22:07:09 -0700 Subject: [PATCH 06/25] Working secret & public keys --- umbral/__init__.py | 14 ++- umbral/curve.py | 120 ++++++++++++++++++++++++ umbral/curve_point.py | 148 ++++++++++++++++++++++++++++++ umbral/curve_scalar.py | 156 ++++++++++++++++++++++++++++++++ umbral/dem.py | 61 +++++++++++++ umbral/hashing.py | 62 +++++++++++++ umbral/keys.py | 123 +++++++++++++++++++++++++ umbral/openssl.py | 201 +++++++++++++++++++++++++++++++++++++++++ umbral/params.py | 10 ++ 9 files changed, 893 insertions(+), 2 deletions(-) create mode 100644 umbral/curve.py create mode 100644 umbral/curve_point.py create mode 100644 umbral/curve_scalar.py create mode 100644 umbral/dem.py create mode 100644 umbral/hashing.py create mode 100644 umbral/keys.py create mode 100644 umbral/openssl.py create mode 100644 umbral/params.py diff --git a/umbral/__init__.py b/umbral/__init__.py index 313ba250..47459240 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -1,8 +1,18 @@ -from umbral.__about__ import ( +from .__about__ import ( __author__, __license__, __summary__, __title__, __version__, __copyright__, __email__, __url__ ) +from .keys import SecretKey, PublicKey __all__ = [ - "__title__", "__summary__", "__version__", "__author__", "__license__", "__copyright__", "__email__", "__url__" + "__title__", + "__summary__", + "__version__", + "__author__", + "__license__", + "__copyright__", + "__email__", + "__url__", + "SecretKey", + "PublicKey", ] diff --git a/umbral/curve.py b/umbral/curve.py new file mode 100644 index 00000000..35f27f4e --- /dev/null +++ b/umbral/curve.py @@ -0,0 +1,120 @@ +from cryptography.hazmat.backends import default_backend + +from . import openssl + + +class Curve: + """ + Acts as a container to store constant variables such as the OpenSSL + curve_nid, the EC_GROUP struct, and the order of the curve. + + Contains a whitelist of supported elliptic curves used in pyUmbral. + + """ + + _supported_curves = { + 415: 'secp256r1', + 714: 'secp256k1', + 715: 'secp384r1' + } + + def __init__(self, nid: int) -> None: + """ + Instantiates an OpenSSL curve with the provided curve_nid and derives + the proper EC_GROUP struct and order. You can _only_ instantiate curves + with supported nids (see `Curve.supported_curves`). + """ + + try: + self.__curve_name = self._supported_curves[nid] + except KeyError: + raise NotImplementedError("Curve NID {} is not supported.".format(nid)) + + # set only once + self.__curve_nid = nid + self.__ec_group = openssl._get_ec_group_by_curve_nid(self.__curve_nid) + self.__order = openssl._get_ec_order_by_group(self.ec_group) + self.__generator = openssl._get_ec_generator_by_group(self.ec_group) + + # Init cache + self.__field_order_size_in_bytes = 0 + self.__group_order_size_in_bytes = 0 + + @classmethod + def from_name(cls, name: str) -> 'Curve': + """ + Alternate constructor to generate a curve instance by its name. + + Raises NotImplementedError if the name cannot be mapped to a known + supported curve NID. + + """ + + name = name.casefold() # normalize + + for supported_nid, supported_name in cls._supported_curves.items(): + if name == supported_name: + instance = cls(nid=supported_nid) + break + else: + message = "{} is not supported curve name.".format(name) + raise NotImplementedError(message) + + return instance + + def __eq__(self, other): + return self.__curve_nid == other.curve_nid + + def __str__(self): + return "".format(self.__curve_nid, self.__curve_name) + + # + # Immutable Curve Data + # + + @property + def field_order_size_in_bytes(self) -> int: + if not self.__field_order_size_in_bytes: + size_in_bits = openssl._get_ec_group_degree(self.__ec_group) + self.__field_order_size_in_bytes = (size_in_bits + 7) // 8 + return self.__field_order_size_in_bytes + + @property + def group_order_size_in_bytes(self) -> int: + if not self.__group_order_size_in_bytes: + BN_num_bytes = default_backend()._lib.BN_num_bytes + self.__group_order_size_in_bytes = BN_num_bytes(self.order) + return self.__group_order_size_in_bytes + + @property + def curve_nid(self) -> int: + return self.__curve_nid + + @property + def name(self) -> str: + return self.__curve_name + + @property + def ec_group(self): + return self.__ec_group + + @property + def order(self): + return self.__order + + @property + def generator(self): + return self.__generator + + +# +# Global Curve Instances +# + +SECP256R1 = Curve.from_name('secp256r1') +SECP256K1 = Curve.from_name('secp256k1') +SECP384R1 = Curve.from_name('secp384r1') + +CURVES = (SECP256K1, SECP256R1, SECP384R1) + +CURVE = SECP256K1 diff --git a/umbral/curve_point.py b/umbral/curve_point.py new file mode 100644 index 00000000..5ca39d01 --- /dev/null +++ b/umbral/curve_point.py @@ -0,0 +1,148 @@ +from typing import Optional, Tuple + +from cryptography.hazmat.backends.openssl import backend + +from . import openssl +from .curve import CURVE +from .curve_scalar import CurveScalar +from .serializable import Serializable + + +class CurvePoint(Serializable): + """ + Represents an OpenSSL EC_POINT except more Pythonic. + """ + + def __init__(self, backend_point) -> None: + self._backend_point = backend_point + + @classmethod + def generator(cls) -> 'CurvePoint': + return cls(CURVE.generator) + + @classmethod + def random(cls) -> 'CurvePoint': + """ + Returns a CurvePoint object with a cryptographically secure EC_POINT based + on the provided curve. + """ + return cls.generator() * CurveScalar.random_nonzero() + + @classmethod + def from_affine(cls, coords: Tuple[int, int]) -> 'CurvePoint': + """ + Returns a CurvePoint object from the given affine coordinates in a tuple in + the format of (x, y) and a given curve. + """ + affine_x, affine_y = coords + if type(affine_x) == int: + affine_x = openssl._int_to_bn(affine_x, curve=None) + + if type(affine_y) == int: + affine_y = openssl._int_to_bn(affine_y, curve=None) + + backend_point = openssl._get_EC_POINT_via_affine(affine_x, affine_y, CURVE) + return cls(backend_point) + + def to_affine(self): + """ + Returns a tuple of Python ints in the format of (x, y) that represents + the point in the curve. + """ + affine_x, affine_y = openssl._get_affine_coords_via_EC_POINT(self._backend_point, CURVE) + return (backend._bn_to_int(affine_x), backend._bn_to_int(affine_y)) + + @classmethod + def __take__(cls, data: bytes) -> Tuple['CurvePoint', bytes]: + """ + Returns a CurvePoint object from the given byte data on the curve provided. + """ + size = CURVE.field_order_size_in_bytes + 1 # compressed point size + point_data, data = cls.__take_bytes__(data, size) + + point = openssl._get_new_EC_POINT(CURVE) + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_oct2point( + CURVE.ec_group, point, point_data, len(point_data), bn_ctx); + backend.openssl_assert(res == 1) + + return cls(point), data + + def __bytes__(self) -> bytes: + """ + Returns the CurvePoint serialized as bytes in the compressed form. + """ + point_conversion_form = backend._lib.POINT_CONVERSION_COMPRESSED + size = CURVE.field_order_size_in_bytes + 1 # compressed point size + + bin_ptr = backend._ffi.new("unsigned char[]", size) + with backend._tmp_bn_ctx() as bn_ctx: + bin_len = backend._lib.EC_POINT_point2oct( + CURVE.ec_group, self._backend_point, point_conversion_form, + bin_ptr, size, bn_ctx + ) + backend.openssl_assert(bin_len != 0) + + return bytes(backend._ffi.buffer(bin_ptr, bin_len)[:]) + + def __eq__(self, other): + """ + Compares two EC_POINTS for equality. + """ + with backend._tmp_bn_ctx() as bn_ctx: + is_equal = backend._lib.EC_POINT_cmp( + CURVE.ec_group, self._backend_point, other._backend_point, bn_ctx + ) + backend.openssl_assert(is_equal != -1) + + # 1 is not-equal, 0 is equal, -1 is error + return not bool(is_equal) + + def __mul__(self, other: CurveScalar) -> 'CurvePoint': + """ + Performs an EC_POINT_mul on an EC_POINT and a BIGNUM. + """ + # TODO: Check that both points use the same curve. + prod = openssl._get_new_EC_POINT(CURVE) + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_mul( + CURVE.ec_group, prod, backend._ffi.NULL, + self._backend_point, other._backend_bignum, bn_ctx + ) + backend.openssl_assert(res == 1) + + return CurvePoint(prod) + + def __add__(self, other: 'CurvePoint') -> 'CurvePoint': + """ + Performs an EC_POINT_add on two EC_POINTS. + """ + op_sum = openssl._get_new_EC_POINT(CURVE) + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_add( + CURVE.ec_group, op_sum, self._backend_point, other._backend_point, bn_ctx + ) + backend.openssl_assert(res == 1) + return CurvePoint(op_sum) + + def __sub__(self, other: 'CurvePoint') -> 'CurvePoint': + """ + Performs subtraction by adding the inverse of the `other` to the point. + """ + return (self + (-other)) + + def __neg__(self) -> 'CurvePoint': + """ + Computes the additive inverse of a CurvePoint, by performing an + EC_POINT_invert on itself. + """ + inv = backend._lib.EC_POINT_dup(self._backend_point, CURVE.ec_group) + backend.openssl_assert(inv != backend._ffi.NULL) + inv = backend._ffi.gc(inv, backend._lib.EC_POINT_clear_free) + + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_invert( + CURVE.ec_group, inv, bn_ctx + ) + backend.openssl_assert(res == 1) + return CurvePoint(inv) diff --git a/umbral/curve_scalar.py b/umbral/curve_scalar.py new file mode 100644 index 00000000..c14463dc --- /dev/null +++ b/umbral/curve_scalar.py @@ -0,0 +1,156 @@ +from typing import Optional, Union, Tuple + +from cryptography.hazmat.backends.openssl import backend + +from . import openssl +from .curve import CURVE +from .serializable import Serializable + + +class CurveScalar(Serializable): + """ + Represents an OpenSSL Bignum modulo the order of a curve. Some of these + operations will only work with prime numbers. + By default, the underlying OpenSSL BIGNUM has BN_FLG_CONSTTIME set for + constant time operations. + """ + + def __init__(self, backend_bignum): + on_curve = openssl._bn_is_on_curve(backend_bignum, CURVE) + if not on_curve: + raise ValueError("The provided BIGNUM is not on the provided curve.") + + self._backend_bignum = backend_bignum + + @classmethod + def random_nonzero(cls) -> 'CurveScalar': + """ + Returns a CurveScalar object with a cryptographically secure OpenSSL BIGNUM. + """ + one = backend._lib.BN_value_one() + + # TODO: in most cases, we want this number to be secret. + # OpenSSL 1.1.1 has `BN_priv_rand_range()`, but it is not + # currently exported by `cryptography`. + # Use when available. + + # Calculate `order - 1` + order_minus_1 = openssl._get_new_BN() + res = backend._lib.BN_sub(order_minus_1, CURVE.order, one) + backend.openssl_assert(res == 1) + + # Get a random in range `[0, order - 1)` + new_rand_bn = openssl._get_new_BN() + res = backend._lib.BN_rand_range(new_rand_bn, order_minus_1) + backend.openssl_assert(res == 1) + + # Turn it into a random in range `[1, order)` + op_sum = openssl._get_new_BN() + res = backend._lib.BN_add(op_sum, new_rand_bn, one) + backend.openssl_assert(res == 1) + + return cls(op_sum) + + @classmethod + def from_int(cls, num: int) -> 'CurveScalar': + """ + Returns a CurveScalar object from a given integer on a curve. + """ + conv_bn = openssl._int_to_bn(num, CURVE) + return cls(conv_bn) + + @classmethod + def __take__(cls, data: bytes) -> Tuple['CurveScalar', bytes]: + size = backend._lib.BN_num_bytes(CURVE.order) + scalar_data, data = cls.__take_bytes__(data, size) + bignum = openssl._bytes_to_bn(scalar_data) + return cls(bignum), data + + def __bytes__(self) -> bytes: + """ + Returns the CurveScalar as bytes. + """ + size = backend._lib.BN_num_bytes(CURVE.order) + return openssl._bn_to_bytes(self._backend_bignum, size) + + def __int__(self) -> int: + """ + Converts the CurveScalar to a Python int. + """ + return backend._bn_to_int(self._backend_bignum) + + def __eq__(self, other) -> bool: + """ + Compares the two BIGNUMS or int. + """ + if type(other) == int: + other = CurveScalar.from_int(other) + + # -1 less than, 0 is equal to, 1 is greater than + return not bool(backend._lib.BN_cmp(self._backend_bignum, other._backend_bignum)) + + def is_zero(self): + # BN_is_zero() is not exported, so this will have to do + return self == 0 + + def __mul__(self, other: Union[int, 'CurveScalar']) -> 'CurveScalar': + """ + Performs a BN_mod_mul between two BIGNUMS. + """ + if isinstance(other, int): + other = CurveScalar.from_int(other) + + product = openssl._get_new_BN() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod_mul( + product, self._backend_bignum, other._backend_bignum, CURVE.order, bn_ctx + ) + backend.openssl_assert(res == 1) + + return CurveScalar(product) + + def __add__(self, other : Union[int, 'CurveScalar']) -> 'CurveScalar': + """ + Performs a BN_mod_add on two BIGNUMs. + """ + if isinstance(other, int): + other = CurveScalar.from_int(other) + + op_sum = openssl._get_new_BN() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod_add( + op_sum, self._backend_bignum, other._backend_bignum, CURVE.order, bn_ctx + ) + backend.openssl_assert(res == 1) + + return CurveScalar(op_sum) + + def __sub__(self, other : Union[int, 'CurveScalar']) -> 'CurveScalar': + """ + Performs a BN_mod_sub on two BIGNUMS. + """ + if isinstance(other, int): + other = CurveScalar.from_int(other) + + diff = openssl._get_new_BN() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod_sub( + diff, self._backend_bignum, other._backend_bignum, CURVE.order, bn_ctx + ) + backend.openssl_assert(res == 1) + + return CurveScalar(diff) + + def invert(self) -> 'CurveScalar': + """ + Performs a BN_mod_inverse. + WARNING: Only in constant time if BN_FLG_CONSTTIME is set on the BN. + """ + with backend._tmp_bn_ctx() as bn_ctx: + inv = backend._lib.BN_mod_inverse( + backend._ffi.NULL, self._backend_bignum, CURVE.order, bn_ctx + ) + backend.openssl_assert(inv != backend._ffi.NULL) + inv = backend._ffi.gc(inv, backend._lib.BN_clear_free) + + return CurveScalar(inv) diff --git a/umbral/dem.py b/umbral/dem.py new file mode 100644 index 00000000..4ba16bbe --- /dev/null +++ b/umbral/dem.py @@ -0,0 +1,61 @@ +import os +from typing import Optional + +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.backends.openssl import backend +from cryptography.hazmat.primitives import hashes + +from nacl.bindings.crypto_aead import ( + crypto_aead_xchacha20poly1305_ietf_encrypt as xchacha_encrypt, + crypto_aead_xchacha20poly1305_ietf_decrypt as xchacha_decrypt, + crypto_aead_xchacha20poly1305_ietf_KEYBYTES as XCHACHA_KEY_SIZE, + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES as XCHACHA_NONCE_SIZE, + ) + + +def kdf(data: bytes, + key_length: int, + salt: Optional[bytes] = None, + info: Optional[bytes] = None, + ) -> bytes: + + hkdf = HKDF(algorithm=hashes.SHA256(), + length=key_length, + salt=salt, + info=info, + backend=backend) + return hkdf.derive(data) + + +class DEM: + + KEY_SIZE = XCHACHA_KEY_SIZE + NONCE_SIZE = XCHACHA_NONCE_SIZE + + def __init__(self, + key_material: bytes, + salt: Optional[bytes] = None, + info: Optional[bytes] = None, + ): + self._key = kdf(key_material, self.KEY_SIZE, salt, info) + + def encrypt(self, plaintext: bytes, nonce: Optional[bytes] = None) -> bytes: + if nonce is None: + nonce = os.urandom(self.NONCE_SIZE) + + if len(nonce) != self.NONCE_SIZE: + raise ValueError(f"The nonce must be exactly {self.NONCE_SIZE} bytes long") + + ciphertext = xchacha_encrypt(plaintext, b"", nonce, self._key) + return nonce + ciphertext + + def decrypt(self, nonce_and_ciphertext: bytes) -> bytes: + + if len(nonce_and_ciphertext) < self.NONCE_SIZE: + raise ValueError(f"The ciphertext must include the nonce") + + nonce = nonce_and_ciphertext[:self.NONCE_SIZE] + ciphertext = nonce_and_ciphertext[self.NONCE_SIZE:] + + # TODO: replace `nacl.exceptions.CryptoError` with our error? + return xchacha_decrypt(ciphertext, b"", nonce, self._key) diff --git a/umbral/hashing.py b/umbral/hashing.py new file mode 100644 index 00000000..a0cd5f04 --- /dev/null +++ b/umbral/hashing.py @@ -0,0 +1,62 @@ +from typing import Optional, Type + +from cryptography.hazmat.backends.openssl import backend +from cryptography.hazmat.primitives import hashes +from cryptography.exceptions import InternalError + +from . import openssl +from .curve import CURVE +from .curve_scalar import CurveScalar +from .curve_point import CurvePoint + + +class Hash: + def __init__(self, dst: bytes): + self._sha256 = hashes.Hash(hashes.SHA256(), backend=backend) + len_dst = len(dst).to_bytes(4, byteorder='big') + self.update(len_dst + dst) + + def update(self, data: bytes) -> None: + self._sha256.update(data) + + def finalize(self) -> bytes: + return self._sha256.finalize() + + +def unsafe_hash_to_point(dst: bytes, data: bytes) -> 'Point': + """ + Hashes arbitrary data into a valid EC point of the specified curve, + using the try-and-increment method. + + WARNING: Do not use when the input data is secret, as this implementation is not + in constant time, and hence, it is not safe with respect to timing attacks. + """ + + len_data = len(data).to_bytes(4, byteorder='big') + data_with_len = len_data + data + sign = b'\x02' + + # We use an internal 32-bit counter as additional input + for i in range(2**32): + ibytes = i.to_bytes(4, byteorder='big') + digest = Hash(dst) + digest.update(data_with_len + ibytes) + point_data = digest.finalize()[:CURVE.field_order_size_in_bytes] + + compressed_point = sign + point_data + + try: + return CurvePoint.from_bytes(compressed_point) + except InternalError as e: + # We want to catch specific InternalExceptions: + # - Point not in the curve (code 107) + # - Invalid compressed point (code 110) + # https://github.com/openssl/openssl/blob/master/include/openssl/ecerr.h#L228 + if e.err_code[0].reason in (107, 110): + pass + else: + # Any other exception, we raise it + raise e + + # Only happens with probability 2^(-32) + raise ValueError('Could not hash input into the curve') # pragma: no cover diff --git a/umbral/keys.py b/umbral/keys.py new file mode 100644 index 00000000..6fbb6896 --- /dev/null +++ b/umbral/keys.py @@ -0,0 +1,123 @@ +from typing import Tuple + +from cryptography.hazmat.backends.openssl import backend +from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey, _EllipticCurvePublicKey + +from . import openssl +from .curve import CURVE +from .curve_scalar import CurveScalar +from .curve_point import CurvePoint +from .dem import DEM +from .serializable import Serializable + + +class SecretKey(Serializable): + + __SERIALIZATION_INFO = b"SECRET_KEY" + + def __init__(self, scalar_key: CurveScalar): + self._scalar_key = scalar_key + + @classmethod + def random(cls) -> 'SecretKey': + """ + Generates a secret key and returns it. + """ + return cls(CurveScalar.random_nonzero()) + + def __eq__(self, other): + return self._scalar_key == other._scalar_key + + def __str__(self): + return f"{self.__class__.__name__}:..." + + def __hash__(self): + raise NotImplementedError("Hashing secret objects is insecure") + + @classmethod + def __take__(cls, data: bytes) -> Tuple['SecretKey', bytes]: + (scalar_key,), data = cls.__take_types__(data, CurveScalar) + return cls(scalar_key), data + + def __bytes__(self) -> bytes: + return bytes(self._scalar_key) + + def to_cryptography_privkey(self) -> _EllipticCurvePrivateKey: + """ + Returns a cryptography.io EllipticCurvePrivateKey from the Umbral key. + """ + ec_key = backend._lib.EC_KEY_new() + backend.openssl_assert(ec_key != backend._ffi.NULL) + ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) + + set_group_result = backend._lib.EC_KEY_set_group(ec_key, CURVE.ec_group) + backend.openssl_assert(set_group_result == 1) + + set_privkey_result = backend._lib.EC_KEY_set_private_key( + ec_key, self._scalar_key._backend_bignum + ) + backend.openssl_assert(set_privkey_result == 1) + + # Get public key + point = openssl._get_new_EC_POINT(CURVE) + with backend._tmp_bn_ctx() as bn_ctx: + mult_result = backend._lib.EC_POINT_mul( + CURVE.ec_group, point, self._scalar_key._backend_bignum, + backend._ffi.NULL, backend._ffi.NULL, bn_ctx + ) + backend.openssl_assert(mult_result == 1) + + set_pubkey_result = backend._lib.EC_KEY_set_public_key(ec_key, point) + backend.openssl_assert(set_pubkey_result == 1) + + evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) + return _EllipticCurvePrivateKey(backend, ec_key, evp_pkey) + + +class PublicKey(Serializable): + + def __init__(self, point_key: CurvePoint): + self._point_key = point_key + + def point(self): + return self._point_key + + @classmethod + def from_secret_key(cls, sk: SecretKey) -> 'PublicKey': + return cls(CurvePoint.generator() * sk.secret_scalar()) + + @classmethod + def __take__(cls, data: bytes) -> Tuple['PublicKey', bytes]: + (point_key,), data = cls.__take_types__(data, CurvePoint) + return cls(point_key), data + + def __bytes__(self) -> bytes: + return bytes(self._point_key) + + def to_cryptography_pubkey(self) -> _EllipticCurvePublicKey: + """ + Returns a cryptography.io EllipticCurvePublicKey from the Umbral key. + """ + ec_key = backend._lib.EC_KEY_new() + backend.openssl_assert(ec_key != backend._ffi.NULL) + ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) + + set_group_result = backend._lib.EC_KEY_set_group(ec_key, CURVE.ec_group) + backend.openssl_assert(set_group_result == 1) + + set_pubkey_result = backend._lib.EC_KEY_set_public_key( + ec_key, self._point_key._backend_point + ) + backend.openssl_assert(set_pubkey_result == 1) + + evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) + return _EllipticCurvePublicKey(backend, ec_key, evp_pkey) + + def __str__(self): + return f"{self.__class__.__name__}:{bytes(self).hex()[:16]}" + + def __eq__(self, other): + return self._point_key == other._point_key + + def __hash__(self) -> int: + return hash((self.__class__, bytes(self))) diff --git a/umbral/openssl.py b/umbral/openssl.py new file mode 100644 index 00000000..2b2e7f76 --- /dev/null +++ b/umbral/openssl.py @@ -0,0 +1,201 @@ +from contextlib import contextmanager +import typing + +from cryptography.hazmat.backends.openssl import backend + + +@typing.no_type_check +def _get_new_BN(set_consttime_flag=True): + """ + Returns a new and initialized OpenSSL BIGNUM. + The set_consttime_flag is set to True by default. When this instance of a + CurveBN object has BN_FLG_CONSTTIME set, OpenSSL will use constant time + operations whenever this CurveBN is passed. + """ + new_bn = backend._lib.BN_new() + backend.openssl_assert(new_bn != backend._ffi.NULL) + new_bn = backend._ffi.gc(new_bn, backend._lib.BN_clear_free) + + if set_consttime_flag: + backend._lib.BN_set_flags(new_bn, backend._lib.BN_FLG_CONSTTIME) + return new_bn + + +@typing.no_type_check +def _get_ec_group_by_curve_nid(curve_nid: int): + """ + Returns the group of a given curve via its OpenSSL nid. This must be freed + after each use otherwise it leaks memory. + """ + group = backend._lib.EC_GROUP_new_by_curve_name(curve_nid) + backend.openssl_assert(group != backend._ffi.NULL) + + return group + + +@typing.no_type_check +def _get_ec_order_by_group(ec_group): + """ + Returns the order of a given curve via its OpenSSL EC_GROUP. + """ + ec_order = _get_new_BN() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_GROUP_get_order(ec_group, ec_order, bn_ctx) + backend.openssl_assert(res == 1) + return ec_order + + +@typing.no_type_check +def _get_ec_generator_by_group(ec_group): + """ + Returns the generator point of a given curve via its OpenSSL EC_GROUP. + """ + generator = backend._lib.EC_GROUP_get0_generator(ec_group) + backend.openssl_assert(generator != backend._ffi.NULL) + generator = backend._ffi.gc(generator, backend._lib.EC_POINT_clear_free) + + return generator + + +@typing.no_type_check +def _get_ec_group_degree(ec_group): + """ + Returns the number of bits needed to represent the order of the finite + field upon the curve is based. + """ + return backend._lib.EC_GROUP_get_degree(ec_group) + + +@typing.no_type_check +def _bn_is_on_curve(check_bn, curve: 'Curve'): + """ + Checks if a given OpenSSL BIGNUM is within the provided curve's order. + Returns True if the provided BN is on the curve, that is in the range `[0, curve_order)`. + """ + zero = backend._int_to_bn(0) + zero = backend._ffi.gc(zero, backend._lib.BN_clear_free) + + check_sign = backend._lib.BN_cmp(check_bn, zero) + range_check = backend._lib.BN_cmp(check_bn, curve.order) + return (check_sign == 1 or check_sign == 0) and range_check == -1 + + +@typing.no_type_check +def _int_to_bn(py_int: int, curve: 'Curve'=None, set_consttime_flag=True): + """ + Converts the given Python int to an OpenSSL BIGNUM. If a curve is + provided, it will check if the Python integer is within the order of that + curve. If it's not within the order, it will raise a ValueError. + + If set_consttime_flag is set to True, OpenSSL will use constant time + operations when using this CurveBN. + """ + conv_bn = backend._int_to_bn(py_int) + conv_bn = backend._ffi.gc(conv_bn, backend._lib.BN_clear_free) + + if curve: + on_curve = _bn_is_on_curve(conv_bn, curve) + if not on_curve: + raise ValueError("The Python integer given is not on the provided curve.") + + if set_consttime_flag: + backend._lib.BN_set_flags(conv_bn, backend._lib.BN_FLG_CONSTTIME) + return conv_bn + +@typing.no_type_check +def _bytes_to_bn(bytes_seq: bytes, set_consttime_flag=True): + """ + Converts the given byte sequence to an OpenSSL BIGNUM. + If set_consttime_flag is set to True, OpenSSL will use constant time + operations when using this BIGNUM. + """ + bn = _get_new_BN(set_consttime_flag) + backend._lib.BN_bin2bn(bytes_seq, len(bytes_seq), bn) + backend.openssl_assert(bn != backend._ffi.NULL) + return bn + +@typing.no_type_check +def _bn_to_bytes(bignum, length : int = None): + """ + Converts the given OpenSSL BIGNUM into a Python bytes sequence. + If length is given, the return bytes will have such length. + If the BIGNUM doesn't fit, it raises a ValueError. + """ + + if bignum is None or bignum == backend._ffi.NULL: + raise ValueError("Input BIGNUM must have a value") + + bn_num_bytes = backend._lib.BN_num_bytes(bignum) + if length is None: + length = bn_num_bytes + elif bn_num_bytes > length: + raise ValueError("Input BIGNUM doesn't fit in {} B".format(length)) + + bin_ptr = backend._ffi.new("unsigned char []", length) + bin_len = backend._lib.BN_bn2bin(bignum, bin_ptr) + return bytes.rjust(backend._ffi.buffer(bin_ptr, bin_len)[:], length, b'\0') + + +@typing.no_type_check +def _get_new_EC_POINT(curve: 'Curve'): + """ + Returns a new and initialized OpenSSL EC_POINT given the group of a curve. + If __curve_nid is provided, it retrieves the group from the curve provided. + """ + new_point = backend._lib.EC_POINT_new(curve.ec_group) + backend.openssl_assert(new_point != backend._ffi.NULL) + new_point = backend._ffi.gc(new_point, backend._lib.EC_POINT_clear_free) + + return new_point + + +@typing.no_type_check +def _get_EC_POINT_via_affine(affine_x, affine_y, curve: 'Curve'): + """ + Returns an EC_POINT given the group of a curve and the affine coordinates + provided. + """ + new_point = _get_new_EC_POINT(curve) + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_set_affine_coordinates_GFp( + curve.ec_group, new_point, affine_x, affine_y, bn_ctx + ) + backend.openssl_assert(res == 1) + return new_point + + +@typing.no_type_check +def _get_affine_coords_via_EC_POINT(ec_point, curve: 'Curve'): + """ + Returns the affine coordinates of a given point on the provided ec_group. + """ + affine_x = _get_new_BN() + affine_y = _get_new_BN() + + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_get_affine_coordinates_GFp( + curve.ec_group, ec_point, affine_x, affine_y, bn_ctx + ) + backend.openssl_assert(res == 1) + return (affine_x, affine_y) + + +@typing.no_type_check +@contextmanager +def _tmp_bn_mont_ctx(modulus): + """ + Initializes and returns a BN_MONT_CTX for Montgomery ops. + Requires a modulus to place in the Montgomery structure. + """ + bn_mont_ctx = backend._lib.BN_MONT_CTX_new() + backend.openssl_assert(bn_mont_ctx != backend._ffi.NULL) + # Don't set the garbage collector. Only free it when the context is done + # or else you'll get a null pointer error. + + try: + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_MONT_CTX_set(bn_mont_ctx, modulus, bn_ctx) + backend.openssl_assert(res == 1) + yield bn_mont_ctx + finally: + backend._lib.BN_MONT_CTX_free(bn_mont_ctx) diff --git a/umbral/params.py b/umbral/params.py new file mode 100644 index 00000000..88c53344 --- /dev/null +++ b/umbral/params.py @@ -0,0 +1,10 @@ +from .hashing import unsafe_hash_to_point + + +class Parameters: + + def __init__(self): + self.u = unsafe_hash_to_point(b'PARAMETERS', b'POINT_U') + + +PARAMETERS = Parameters() From 2c28ae8bc241c0dddef1ac0c4a15789cc1a44fd8 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 16 Mar 2021 16:41:05 -0700 Subject: [PATCH 07/25] Add Capsule class and encrypt()/decrypt_original() --- umbral/__init__.py | 5 ++++ umbral/capsule.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++ umbral/dem.py | 15 ++++------ umbral/hashing.py | 22 ++++++++++++++ umbral/keys.py | 3 ++ umbral/pre.py | 29 ++++++++++++++++++ 6 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 umbral/capsule.py create mode 100644 umbral/pre.py diff --git a/umbral/__init__.py b/umbral/__init__.py index 47459240..2317a754 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -2,7 +2,9 @@ __author__, __license__, __summary__, __title__, __version__, __copyright__, __email__, __url__ ) +from .capsule import Capsule from .keys import SecretKey, PublicKey +from .pre import encrypt, decrypt_original __all__ = [ "__title__", @@ -15,4 +17,7 @@ "__url__", "SecretKey", "PublicKey", + "Capsule", + "encrypt", + "decrypt_original", ] diff --git a/umbral/capsule.py b/umbral/capsule.py new file mode 100644 index 00000000..cac39e51 --- /dev/null +++ b/umbral/capsule.py @@ -0,0 +1,73 @@ +from typing import Tuple + +from .curve_point import CurvePoint +from .curve_scalar import CurveScalar +from .dem import kdf +from .hashing import hash_capsule_points +from .keys import PublicKey, SecretKey +from .params import PARAMETERS +from .serializable import Serializable + + +class Capsule(Serializable): + + class NotValid(ValueError): + """ + raised if the capsule does not pass verification. + """ + + def __init__(self, point_e: CurvePoint, point_v: CurvePoint, signature: CurveScalar): + self.point_e = point_e + self.point_v = point_v + self.signature = signature + + @classmethod + def __take__(cls, data: bytes) -> Tuple['Capsule', bytes]: + (e, v, sig), data = cls.__take_types__(data, CurvePoint, CurvePoint, CurveScalar) + + capsule = cls(e, v, sig) + if not capsule._verify(): + raise cls.NotValid("Capsule verification failed.") + + return capsule, data + + def __bytes__(self) -> bytes: + return bytes(self.point_e) + bytes(self.point_v) + bytes(self.signature) + + @classmethod + def from_public_key(cls, pk: PublicKey) -> Tuple['Capsule', CurvePoint]: + g = CurvePoint.generator() + + priv_r = CurveScalar.random_nonzero() + pub_r = g * priv_r + + priv_u = CurveScalar.random_nonzero() + pub_u = g * priv_u + + h = hash_capsule_points(pub_r, pub_u) + s = priv_u + (priv_r * h) + + shared_key = pk._point_key * (priv_r + priv_u) + + return cls(point_e=pub_r, point_v=pub_u, signature=s), shared_key + + def open_original(self, sk: SecretKey) -> CurvePoint: + return (self.point_e + self.point_v) * sk.secret_scalar() + + def _components(self): + return (self.point_e, self.point_v, self.signature) + + def _verify(self) -> bool: + g = CurvePoint.generator() + e, v, s = self._components() + h = hash_capsule_points(e, v) + return g * s == v + (e * h) + + def __eq__(self, other): + return self._components() == other._components() + + def __hash__(self): + return hash((self.__class__, bytes(self))) + + def __str__(self): + return f"{self.__class__.__name__}:{bytes(self).hex()[:16]}" diff --git a/umbral/dem.py b/umbral/dem.py index 4ba16bbe..635973f4 100644 --- a/umbral/dem.py +++ b/umbral/dem.py @@ -39,17 +39,12 @@ def __init__(self, ): self._key = kdf(key_material, self.KEY_SIZE, salt, info) - def encrypt(self, plaintext: bytes, nonce: Optional[bytes] = None) -> bytes: - if nonce is None: - nonce = os.urandom(self.NONCE_SIZE) - - if len(nonce) != self.NONCE_SIZE: - raise ValueError(f"The nonce must be exactly {self.NONCE_SIZE} bytes long") - - ciphertext = xchacha_encrypt(plaintext, b"", nonce, self._key) + def encrypt(self, plaintext: bytes, authenticated_data: bytes = b"") -> bytes: + nonce = os.urandom(self.NONCE_SIZE) + ciphertext = xchacha_encrypt(plaintext, authenticated_data, nonce, self._key) return nonce + ciphertext - def decrypt(self, nonce_and_ciphertext: bytes) -> bytes: + def decrypt(self, nonce_and_ciphertext: bytes, authenticated_data: bytes = b"") -> bytes: if len(nonce_and_ciphertext) < self.NONCE_SIZE: raise ValueError(f"The ciphertext must include the nonce") @@ -58,4 +53,4 @@ def decrypt(self, nonce_and_ciphertext: bytes) -> bytes: ciphertext = nonce_and_ciphertext[self.NONCE_SIZE:] # TODO: replace `nacl.exceptions.CryptoError` with our error? - return xchacha_decrypt(ciphertext, b"", nonce, self._key) + return xchacha_decrypt(ciphertext, authenticated_data, nonce, self._key) diff --git a/umbral/hashing.py b/umbral/hashing.py index a0cd5f04..ac5776fb 100644 --- a/umbral/hashing.py +++ b/umbral/hashing.py @@ -23,6 +23,28 @@ def finalize(self) -> bytes: return self._sha256.finalize() +def digest_to_scalar(digest: Hash) -> CurveScalar: + # TODO: to be replaced by the standard algroithm. + # Currently just matching what we have in RustCrypto stack. + # Can produce zeros! + + hash_digest = openssl._bytes_to_bn(digest.finalize()) + + bignum = openssl._get_new_BN() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod(bignum, hash_digest, CURVE.order, bn_ctx) + backend.openssl_assert(res == 1) + + return CurveScalar(bignum) + + +def hash_capsule_points(e: CurvePoint, v: CurvePoint) -> CurveScalar: + digest = Hash(b"CAPSULE_POINTS") + digest.update(bytes(e)) + digest.update(bytes(v)) + return digest_to_scalar(digest) + + def unsafe_hash_to_point(dst: bytes, data: bytes) -> 'Point': """ Hashes arbitrary data into a valid EC point of the specified curve, diff --git a/umbral/keys.py b/umbral/keys.py index 6fbb6896..8c1d3361 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -34,6 +34,9 @@ def __str__(self): def __hash__(self): raise NotImplementedError("Hashing secret objects is insecure") + def secret_scalar(self): + return self._scalar_key + @classmethod def __take__(cls, data: bytes) -> Tuple['SecretKey', bytes]: (scalar_key,), data = cls.__take_types__(data, CurveScalar) diff --git a/umbral/pre.py b/umbral/pre.py new file mode 100644 index 00000000..d7b51ad6 --- /dev/null +++ b/umbral/pre.py @@ -0,0 +1,29 @@ +from typing import Tuple + +from .capsule import Capsule +from .dem import DEM +from .keys import PublicKey, SecretKey + + +def encrypt(pk: PublicKey, plaintext: bytes) -> Tuple[Capsule, bytes]: + """ + Performs an encryption using the UmbralDEM object and encapsulates a key + for the sender using the public key provided. + + Returns the KEM Capsule and the ciphertext. + """ + capsule, key_seed = Capsule.from_public_key(pk) + dem = DEM(bytes(key_seed)) + ciphertext = dem.encrypt(plaintext, authenticated_data=bytes(capsule)) + return capsule, ciphertext + + +def decrypt_original(sk: SecretKey, capsule: Capsule, ciphertext: bytes) -> bytes: + """ + Opens the capsule using the original (Alice's) key used for encryption and gets what's inside. + We hope that's a symmetric key, which we use to decrypt the ciphertext + and return the resulting cleartext. + """ + key_seed = capsule.open_original(sk) + dem = DEM(bytes(key_seed)) + return dem.decrypt(ciphertext, authenticated_data=bytes(capsule)) From d6626ba1a6b605608dae8cd780f0970318e72e20 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 17 Mar 2021 16:24:41 -0700 Subject: [PATCH 08/25] Add generate_kfrags() --- umbral/__init__.py | 3 + umbral/hashing.py | 74 ++++++++++- umbral/key_frag.py | 290 +++++++++++++++++++++++++++++++++++++++++ umbral/keys.py | 66 +++++++++- umbral/serializable.py | 15 +++ 5 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 umbral/key_frag.py diff --git a/umbral/__init__.py b/umbral/__init__.py index 2317a754..b5e1e043 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -3,6 +3,7 @@ ) from .capsule import Capsule +from .key_frag import KeyFrag, generate_kfrags from .keys import SecretKey, PublicKey from .pre import encrypt, decrypt_original @@ -18,6 +19,8 @@ "SecretKey", "PublicKey", "Capsule", + "KeyFrag", "encrypt", "decrypt_original", + "generate_kfrags", ] diff --git a/umbral/hashing.py b/umbral/hashing.py index ac5776fb..96c292e9 100644 --- a/umbral/hashing.py +++ b/umbral/hashing.py @@ -1,4 +1,4 @@ -from typing import Optional, Type +from typing import TYPE_CHECKING, Optional, Type from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.primitives import hashes @@ -8,6 +8,10 @@ from .curve import CURVE from .curve_scalar import CurveScalar from .curve_point import CurvePoint +from .keys import PublicKey, SecretKey, Signature +from .serializable import serialize_bool +if TYPE_CHECKING: # pragma: no cover + from .key_frag import KeyFragID class Hash: @@ -38,6 +42,19 @@ def digest_to_scalar(digest: Hash) -> CurveScalar: return CurveScalar(bignum) +def hash_to_polynomial_arg(precursor: CurvePoint, + pubkey: CurvePoint, + dh_point: CurvePoint, + kfrag_id: 'KeyFragID', + ) -> CurveScalar: + digest = Hash(b"POLYNOMIAL_ARG") + digest.update(bytes(precursor)) + digest.update(bytes(pubkey)) + digest.update(bytes(dh_point)) + digest.update(bytes(kfrag_id)) + return digest_to_scalar(digest) + + def hash_capsule_points(e: CurvePoint, v: CurvePoint) -> CurveScalar: digest = Hash(b"CAPSULE_POINTS") digest.update(bytes(e)) @@ -45,7 +62,60 @@ def hash_capsule_points(e: CurvePoint, v: CurvePoint) -> CurveScalar: return digest_to_scalar(digest) -def unsafe_hash_to_point(dst: bytes, data: bytes) -> 'Point': +def hash_to_shared_secret(precursor: CurvePoint, + pubkey: CurvePoint, + dh_point: CurvePoint + ) -> CurveScalar: + digest = Hash(b"SHARED_SECRET") + digest.update(bytes(precursor)) + digest.update(bytes(pubkey)) + digest.update(bytes(dh_point)) + return digest_to_scalar(digest) + + +def hash_to_cfrag_signature(kfrag_id: 'KeyFragID', + commitment: CurvePoint, + precursor: CurvePoint, + maybe_delegating_pk: Optional[PublicKey], + maybe_receiving_pk: Optional[PublicKey], + ) -> 'SignatureDigest': + + digest = SignatureDigest(b"CFRAG_SIGNATURE") + digest.update(bytes(kfrag_id)) + digest.update(bytes(commitment)) + digest.update(bytes(precursor)) + + if maybe_delegating_pk: + digest.update(serialize_bool(True)) + digest.update(bytes(maybe_delegating_pk)) + else: + digest.update(serialize_bool(False)) + + if maybe_receiving_pk: + digest.update(serialize_bool(True)) + digest.update(bytes(maybe_receiving_pk)) + else: + digest.update(serialize_bool(False)) + + return digest + + +class SignatureDigest: + + def __init__(self, dst: bytes): + self._digest = Hash(dst) + + def update(self, value): + self._digest.update(value) + + def sign(self, sk: SecretKey) -> Signature: + return sk.sign_digest(self._digest, hashes.SHA256) + + def verify(self, pk: PublicKey, sig: Signature): + return sig.verify_digest(pk, self._digest, hashes.SHA256) + + +def unsafe_hash_to_point(dst: bytes, data: bytes) -> CurvePoint: """ Hashes arbitrary data into a valid EC point of the specified curve, using the try-and-increment method. diff --git a/umbral/key_frag.py b/umbral/key_frag.py new file mode 100644 index 00000000..6d0d8cf7 --- /dev/null +++ b/umbral/key_frag.py @@ -0,0 +1,290 @@ +import os +from typing import Tuple, List, Optional + +from .curve_point import CurvePoint +from .curve_scalar import CurveScalar +from .hashing import hash_to_shared_secret, hash_to_cfrag_signature, hash_to_polynomial_arg +from .keys import PublicKey, SecretKey, Signature +from .params import PARAMETERS +from .serializable import Serializable, serialize_bool, take_bool + + +class KeyFragID(Serializable): + + __SIZE = 32 + + def __init__(self, id_: bytes): + self._id = id_ + + def __eq__(self, other): + return self._id == other._id + + @classmethod + def random(cls) -> 'KeyFragID': + return cls(os.urandom(cls.__SIZE)) + + @classmethod + def __take__(cls, data): + id_, data = cls.__take_bytes__(data, cls.__SIZE) + return cls(id_), data + + def __bytes__(self): + return self._id + + +class KeyFragProof(Serializable): + + @classmethod + def from_base(cls, + base: 'KeyFragBase', + kfrag_id: KeyFragID, + kfrag_key: CurveScalar, + sign_delegating_key: bool, + sign_receiving_key: bool, + ) -> 'KeyFragProof': + + params = PARAMETERS + + kfrag_precursor = base.precursor + signing_sk = base.signing_sk + delegating_pk = base.delegating_pk + receiving_pk = base.receiving_pk + + commitment = params.u * kfrag_key + + signature_for_receiver = hash_to_cfrag_signature(kfrag_id, + commitment, + kfrag_precursor, + delegating_pk, + receiving_pk, + ).sign(signing_sk) + + maybe_delegating_pk = delegating_pk if sign_delegating_key else None + maybe_receiving_pk = receiving_pk if sign_receiving_key else None + signature_for_proxy = hash_to_cfrag_signature(kfrag_id, + commitment, + kfrag_precursor, + maybe_delegating_pk, + maybe_receiving_pk + ).sign(signing_sk) + + return cls(commitment, + signature_for_proxy, + signature_for_receiver, + sign_delegating_key, + sign_receiving_key) + + def __init__(self, + commitment: CurvePoint, + signature_for_proxy: Signature, + signature_for_receiver: Signature, + delegating_key_signed: bool, + receiving_key_signed: bool + ): + + self.commitment = commitment + self.signature_for_proxy = signature_for_proxy + self.signature_for_receiver = signature_for_receiver + self.delegating_key_signed = delegating_key_signed + self.receiving_key_signed = receiving_key_signed + + def _components(self): + return (self.commitment, + self.signature_for_proxy, + self.signature_for_receiver, + self.delegating_key_signed, + self.receiving_key_signed) + + def __eq__(self, other): + return self._components() == other._components() + + @classmethod + def __take__(cls, data): + types = [CurvePoint, Signature, Signature] + (commitment, sig_proxy, sig_bob), data = cls.__take_types__(data, *types) + delegating_key_signed, data = take_bool(data) + receiving_key_signed, data = take_bool(data) + + obj = cls(commitment, sig_proxy, sig_bob, delegating_key_signed, receiving_key_signed) + return obj, data + + def __bytes__(self): + return (bytes(self.commitment) + + bytes(self.signature_for_proxy) + + bytes(self.signature_for_receiver) + + serialize_bool(self.delegating_key_signed) + + serialize_bool(self.receiving_key_signed) + ) + + +# Coefficients of the generating polynomial +def poly_eval(coeffs: List[CurveScalar], x: CurveScalar) -> CurveScalar: + result = coeffs[-1] + for coeff in reversed(coeffs[:-1]): + result = (result * x) + coeff + return result + + +class KeyFrag(Serializable): + + def __init__(self, + id_: KeyFragID, + key: CurveScalar, + precursor: CurvePoint, + proof: KeyFragProof): + self.id = id_ + self.key = key + self.precursor = precursor + self.proof = proof + + @classmethod + def __take__(cls, data): + types = [KeyFragID, CurveScalar, CurvePoint, KeyFragProof] + components, data = cls.__take_types__(data, *types) + return cls(*components), data + + def __bytes__(self): + return bytes(self.id) + bytes(self.key) + bytes(self.precursor) + bytes(self.proof) + + def _components(self): + return self.id, self.key, self.precursor, self.proof + + def __eq__(self, other): + return self._components() == other._components() + + def __hash__(self): + return hash((self.__class__, bytes(self))) + + def __str__(self): + return f"{self.__class__.__name__}:{bytes(self).hex()[:16]}" + + @classmethod + def from_base(cls, + base: 'KeyFragBase', + sign_delegating_key: bool, + sign_receiving_key: bool, + ) -> 'KeyFrag': + + kfrag_id = KeyFragID.random() + + # The index of the re-encryption key share (which in Shamir's Secret + # Sharing corresponds to x in the tuple (x, f(x)), with f being the + # generating polynomial), is used to prevent reconstruction of the + # re-encryption key without Bob's intervention + share_index = hash_to_polynomial_arg(base.precursor, + base.receiving_pk.point(), + base.dh_point, + kfrag_id, + ) + + # The re-encryption key share is the result of evaluating the generating + # polynomial for the index value + rk = poly_eval(base.coefficients, share_index) + + proof = KeyFragProof.from_base(base, + kfrag_id, + rk, + sign_delegating_key, + sign_receiving_key, + ) + + return cls(kfrag_id, rk, base.precursor, proof) + + def verify(self, + signing_pk: PublicKey, + delegating_pk: Optional[PublicKey] = None, + receiving_pk: Optional[PublicKey] = None, + ) -> bool: + + u = PARAMETERS.u + + kfrag_id = self.id + key = self.key + commitment = self.proof.commitment + precursor = self.precursor + + # We check that the commitment is well-formed + if commitment != u * key: + return False + + # A shortcut, perhaps not necessary + delegating_key_missing = self.proof.delegating_key_signed and not bool(delegating_pk) + receiving_key_missing = self.proof.receiving_key_signed and not bool(receiving_pk) + + if delegating_key_missing or receiving_key_missing: + return False + + delegating_pk = delegating_pk if self.proof.delegating_key_signed else None + receiving_pk = receiving_pk if self.proof.receiving_key_signed else None + sig = hash_to_cfrag_signature(kfrag_id, + commitment, + precursor, + delegating_pk, + receiving_pk) + return sig.verify(signing_pk, self.proof.signature_for_proxy) + + +class KeyFragBase: + + def __init__(self, + delegating_sk: SecretKey, + receiving_pk: PublicKey, + signing_sk: SecretKey, + threshold: int, + ): + + if threshold <= 0: + raise ValueError(f"`threshold` must be larger than 0 (given: {threshold})") + + g = CurvePoint.generator() + + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + receiving_pk_point = receiving_pk.point() + + while True: + # The precursor point is used as an ephemeral public key in a DH key exchange, + # and the resulting shared secret 'dh_point' is used to derive other secret values + private_precursor = CurveScalar.random_nonzero() + precursor = g * private_precursor + + dh_point = receiving_pk_point * private_precursor + + # Secret value 'd' allows to make Umbral non-interactive + d = hash_to_shared_secret(precursor, receiving_pk_point, dh_point) + + # At the moment we cannot statically ensure `d` is not zero, + # but we need it to be non-zero for the algorithm to work. + if not d.is_zero(): + break + + # Coefficients of the generating polynomial + # `invert()` is guaranteed to work because `d` is nonzero. + coefficients = [ + delegating_sk.secret_scalar() * d.invert(), + *[CurveScalar.random_nonzero() for _ in range(threshold-1)]] + + self.signing_sk = signing_sk + self.precursor = precursor + self.dh_point = dh_point + self.delegating_pk = delegating_pk + self.receiving_pk = receiving_pk + self.coefficients = coefficients + + +def generate_kfrags(delegating_sk: SecretKey, + receiving_pk: PublicKey, + signing_sk: SecretKey, + threshold: int, + num_kfrags: int, + sign_delegating_key: bool = True, + sign_receiving_key: bool = True, + ) -> List[KeyFrag]: + + base = KeyFragBase(delegating_sk, receiving_pk, signing_sk, threshold) + + # Technically we could allow it, but what would be the use of these kfrags? + if num_kfrags < threshold: + raise ValueError(f"Creating less kfrags ({num_kfrags}) than threshold ({threshold}) makes them useless") + + return [KeyFrag.from_base(base, sign_delegating_key, sign_receiving_key) for _ in range(num_kfrags)] diff --git a/umbral/keys.py b/umbral/keys.py index 8c1d3361..46d5023a 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -1,7 +1,10 @@ -from typing import Tuple +from typing import TYPE_CHECKING, Tuple +from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey, _EllipticCurvePublicKey +from cryptography.hazmat.primitives.asymmetric import utils +from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from . import openssl from .curve import CURVE @@ -10,6 +13,9 @@ from .dem import DEM from .serializable import Serializable +if TYPE_CHECKING: # pragma: no cover + from .hashing import Hash + class SecretKey(Serializable): @@ -76,6 +82,64 @@ def to_cryptography_privkey(self) -> _EllipticCurvePrivateKey: evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) return _EllipticCurvePrivateKey(backend, ec_key, evp_pkey) + def sign_digest(self, digest: 'Hash', backend_hash_algorithm) -> 'Signature': + + signature_algorithm = ECDSA(utils.Prehashed(backend_hash_algorithm())) + message = digest.finalize() + + cpk = self.to_cryptography_privkey() + signature_der_bytes = cpk.sign(message, signature_algorithm) + r, s = utils.decode_dss_signature(signature_der_bytes) + + # Normalize s + # s is public, so no constant-timeness required here + order = backend._bn_to_int(CURVE.order) + if s > (order >> 1): + s = order - s + + return Signature(CurveScalar.from_int(r), CurveScalar.from_int(s)) + + +class Signature(Serializable): + """ + Wrapper for ECDSA signatures. + We store signatures as r and s; this class allows interoperation + between (r, s) and DER formatting. + """ + + def __init__(self, r: CurveScalar, s: CurveScalar): + self.r = r + self.s = s + + def __repr__(self): + return f"ECDSA Signature: {bytes(self).hex()[:15]}" + + def verify_digest(self, verifying_key: 'PublicKey', digest: 'Hash', backend_hash_algorithm) -> bool: + cryptography_pub_key = verifying_key.to_cryptography_pubkey() + signature_algorithm = ECDSA(utils.Prehashed(backend_hash_algorithm())) + message = digest.finalize() + signature_der_bytes = utils.encode_dss_signature(int(self.r), int(self.s)) + + # TODO: Raise error instead of returning boolean + try: + cryptography_pub_key.verify(signature=signature_der_bytes, + data=message, + signature_algorithm=signature_algorithm) + except InvalidSignature: + return False + return True + + @classmethod + def __take__(cls, data): + (r, s), data = cls.__take_types__(data, CurveScalar, CurveScalar) + return cls(r, s), data + + def __bytes__(self): + return bytes(self.r) + bytes(self.s) + + def __eq__(self, other): + return self.r == other.r and self.s == other.s + class PublicKey(Serializable): diff --git a/umbral/serializable.py b/umbral/serializable.py index 196e0f5a..dde3d14a 100644 --- a/umbral/serializable.py +++ b/umbral/serializable.py @@ -35,3 +35,18 @@ def __take__(cls: Type[_T], data: bytes) -> Tuple[_T, bytes]: @abstractmethod def __bytes__(self): raise NotImplementedError + + +def serialize_bool(b: bool) -> bytes: + return b'\x01' if b else b'\x00' + + +def take_bool(data: bytes) -> Tuple[bool, bytes]: + bool_bytes, data = Serializable.__take_bytes__(data, 1) + if bool_bytes == b'\x01': + b = True + elif bool_bytes == b'\x00': + b = False + else: + raise ValueError(f"Incorrectly serialized boolean; expected b'\\x00' or b'\\x01', got {repr(bool_bytes)}") + return b, data From b96888cafbb5c0d27b7d49d53d57064c51f7048f Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 17 Mar 2021 21:34:12 -0700 Subject: [PATCH 09/25] Add reencryption functionality --- umbral/__init__.py | 6 +- umbral/capsule.py | 64 ++++++++++++- umbral/capsule_frag.py | 200 +++++++++++++++++++++++++++++++++++++++++ umbral/curve_scalar.py | 6 +- umbral/hashing.py | 15 +++- umbral/pre.py | 22 ++++- 6 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 umbral/capsule_frag.py diff --git a/umbral/__init__.py b/umbral/__init__.py index b5e1e043..7021af4a 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -3,9 +3,10 @@ ) from .capsule import Capsule +from .capsule_frag import CapsuleFrag from .key_frag import KeyFrag, generate_kfrags from .keys import SecretKey, PublicKey -from .pre import encrypt, decrypt_original +from .pre import encrypt, decrypt_original, decrypt_reencrypted, reencrypt __all__ = [ "__title__", @@ -20,7 +21,10 @@ "PublicKey", "Capsule", "KeyFrag", + "CapsuleFrag", "encrypt", "decrypt_original", "generate_kfrags", + "reencrypt", + "decrypt_reencrypted", ] diff --git a/umbral/capsule.py b/umbral/capsule.py index cac39e51..80b00e35 100644 --- a/umbral/capsule.py +++ b/umbral/capsule.py @@ -1,12 +1,22 @@ -from typing import Tuple +from typing import TYPE_CHECKING, Tuple, Sequence from .curve_point import CurvePoint from .curve_scalar import CurveScalar -from .dem import kdf -from .hashing import hash_capsule_points +from .hashing import hash_capsule_points, hash_to_polynomial_arg, hash_to_shared_secret from .keys import PublicKey, SecretKey from .params import PARAMETERS from .serializable import Serializable +if TYPE_CHECKING: # pragma: no cover + from .capsule_frag import CapsuleFrag + + +def lambda_coeff(xs: Sequence[CurveScalar], i: int) -> CurveScalar: + res = CurveScalar.one() + for j in range(len(xs)): + if j != i: + inv_diff = (xs[j] - xs[i]).invert() + res = (res * xs[j]) * inv_diff + return res class Capsule(Serializable): @@ -54,6 +64,54 @@ def from_public_key(cls, pk: PublicKey) -> Tuple['Capsule', CurvePoint]: def open_original(self, sk: SecretKey) -> CurvePoint: return (self.point_e + self.point_v) * sk.secret_scalar() + def open_reencrypted(self, + receiving_sk: SecretKey, + delegating_pk: PublicKey, + cfrags: Sequence['CapsuleFrag'], + ) -> CurvePoint: + + if len(cfrags) == 0: + raise ValueError("Empty CapsuleFrag sequence") + + precursor = cfrags[0].precursor + + if len(set(cfrags)) != len(cfrags): + raise ValueError("Some of the CapsuleFrags are repeated") + + if not all(cfrag.precursor == precursor for cfrag in cfrags[1:]): + raise ValueError("CapsuleFrags are not pairwise consistent") + + pub_key = PublicKey.from_secret_key(receiving_sk).point() + dh_point = precursor * receiving_sk.secret_scalar() + + # Combination of CFrags via Shamir's Secret Sharing reconstruction + lc = [hash_to_polynomial_arg(precursor, pub_key, dh_point, cfrag.kfrag_id) + for cfrag in cfrags] + + e_primes = [] + v_primes = [] + for i, cfrag in enumerate(cfrags): + lambda_i = lambda_coeff(lc, i) + e_primes.append(cfrag.point_e1 * lambda_i) + v_primes.append(cfrag.point_v1 * lambda_i) + e_prime = sum(e_primes[1:], e_primes[0]) + v_prime = sum(v_primes[1:], v_primes[0]) + + # Secret value 'd' allows to make Umbral non-interactive + d = hash_to_shared_secret(precursor, pub_key, dh_point) + + s = self.signature + h = hash_capsule_points(self.point_e, self.point_v) + + orig_pub_key = delegating_pk.point() + + # TODO: check for d == 0? Or just let if fail? + inv_d = d.invert() + if orig_pub_key * (s * inv_d) != (e_prime * h) + v_prime: + raise ValueError("Internal validation failed") + + return (e_prime + v_prime) * d + def _components(self): return (self.point_e, self.point_v, self.signature) diff --git a/umbral/capsule_frag.py b/umbral/capsule_frag.py new file mode 100644 index 00000000..e8fc8168 --- /dev/null +++ b/umbral/capsule_frag.py @@ -0,0 +1,200 @@ +from typing import Sequence, Optional + +from .capsule import Capsule +from .curve_point import CurvePoint +from .curve_scalar import CurveScalar +from .hashing import Hash, hash_to_cfrag_verification, hash_to_cfrag_signature +from .keys import PublicKey, SecretKey, Signature +from .key_frag import KeyFrag, KeyFragID +from .params import PARAMETERS +from .serializable import Serializable + + +class CapsuleFragProof(Serializable): + + def __init__(self, + point_e2: CurvePoint, + point_v2: CurvePoint, + kfrag_commitment: CurvePoint, + kfrag_pok: CurvePoint, + signature: CurveScalar, + kfrag_signature: Signature, + ): + + self.point_e2 = point_e2 + self.point_v2 = point_v2 + self.kfrag_commitment = kfrag_commitment + self.kfrag_pok = kfrag_pok + self.signature = signature + self.kfrag_signature = kfrag_signature + + def _components(self): + return (self.point_e2, self.point_v2, self.kfrag_commitment, + self.kfrag_pok, self.signature, self.kfrag_signature) + + def __eq__(self, other): + return self._components() == other._components() + + @classmethod + def __take__(cls, data): + types = [CurvePoint, CurvePoint, CurvePoint, CurvePoint, CurveScalar, Signature] + components, data = cls.__take_types__(data, *types) + return cls(*components), data + + def __bytes__(self): + return (bytes(self.point_e2) + + bytes(self.point_v2) + + bytes(self.kfrag_commitment) + + bytes(self.kfrag_pok) + + bytes(self.signature) + + bytes(self.kfrag_signature) + ) + + @classmethod + def from_kfrag_and_cfrag(cls, + capsule: Capsule, + kfrag: KeyFrag, + cfrag_e1: CurvePoint, + cfrag_v1: CurvePoint, + metadata: Optional[bytes], + ) -> 'CapsuleFragProof': + + params = PARAMETERS + + rk = kfrag.key + t = CurveScalar.random_nonzero() + + # Here are the formulaic constituents shared with `CapsuleFrag.verify()`. + + e = capsule.point_e + v = capsule.point_v + + e1 = cfrag_e1 + v1 = cfrag_v1 + + u = params.u + u1 = kfrag.proof.commitment + + e2 = e * t + v2 = v * t + u2 = u * t + + h = hash_to_cfrag_verification([e, e1, e2, v, v1, v2, u, u1, u2], metadata) + + ### + + z3 = t + rk * h + + return cls(point_e2=e2, + point_v2=v2, + kfrag_commitment=u1, + kfrag_pok=u2, + signature=z3, + kfrag_signature=kfrag.proof.signature_for_receiver, + ) + + +class CapsuleFrag(Serializable): + + def __init__(self, + point_e1: CurvePoint, + point_v1: CurvePoint, + kfrag_id: KeyFragID, + precursor: CurvePoint, + proof: CapsuleFragProof, + ): + + self.point_e1 = point_e1 + self.point_v1 = point_v1 + self.kfrag_id = kfrag_id + self.precursor = precursor + self.proof = proof + + def _components(self): + return (self.point_e1, self.point_v1, self.kfrag_id, self.precursor, self.proof) + + def __eq__(self, other): + return self._components() == other._components() + + def __hash__(self): + return hash((self.__class__, bytes(self))) + + def __str__(self): + return f"{self.__class__.__name__}:{bytes(self).hex()[:16]}" + + @classmethod + def __take__(cls, data): + types = CurvePoint, CurvePoint, KeyFragID, CurvePoint, CapsuleFragProof + components, data = cls.__take_types__(data, *types) + return cls(*components), data + + def __bytes__(self): + return (bytes(self.point_e1) + + bytes(self.point_v1) + + bytes(self.kfrag_id) + + bytes(self.precursor) + + bytes(self.proof)) + + @classmethod + def reencrypted(cls, + capsule: Capsule, + kfrag: KeyFrag, + metadata: Optional[bytes] = None, + ) -> 'CapsuleFrag': + rk = kfrag.key + e1 = capsule.point_e * rk + v1 = capsule.point_v * rk + proof = CapsuleFragProof.from_kfrag_and_cfrag(capsule, kfrag, e1, v1, metadata) + + return cls(point_e1=e1, + point_v1=v1, + kfrag_id=kfrag.id, + precursor=kfrag.precursor, + proof=proof, + ) + + def verify(self, + capsule: Capsule, + delegating_pk: PublicKey, + receiving_pk: PublicKey, + signing_pk: PublicKey, + metadata: Optional[bytes] = None, + ) -> bool: + + params = PARAMETERS + + # Here are the formulaic constituents shared with + # `CapsuleFragProof.from_kfrag_and_cfrag`. + + e = capsule.point_e + v = capsule.point_v + + e1 = self.point_e1 + v1 = self.point_v1 + + u = params.u + u1 = self.proof.kfrag_commitment + + e2 = self.proof.point_e2 + v2 = self.proof.point_v2 + u2 = self.proof.kfrag_pok + + h = hash_to_cfrag_verification([e, e1, e2, v, v1, v2, u, u1, u2], metadata) + + ### + + precursor = self.precursor + kfrag_id = self.kfrag_id + + kfrag_signature = hash_to_cfrag_signature(kfrag_id, u1, precursor, delegating_pk, receiving_pk) + valid_kfrag_signature = kfrag_signature.verify(signing_pk, self.proof.kfrag_signature) + + z3 = self.proof.signature + correct_reencryption_of_e = e * z3 == e2 + e1 * h + correct_reencryption_of_v = v * z3 == v2 + v1 * h + correct_rk_commitment = u * z3 == u2 + u1 * h + + return (valid_kfrag_signature + and correct_reencryption_of_e + and correct_reencryption_of_v + and correct_rk_commitment) diff --git a/umbral/curve_scalar.py b/umbral/curve_scalar.py index c14463dc..05375e17 100644 --- a/umbral/curve_scalar.py +++ b/umbral/curve_scalar.py @@ -27,7 +27,7 @@ def random_nonzero(cls) -> 'CurveScalar': """ Returns a CurveScalar object with a cryptographically secure OpenSSL BIGNUM. """ - one = backend._lib.BN_value_one() + one = cls.one()._backend_bignum # TODO: in most cases, we want this number to be secret. # OpenSSL 1.1.1 has `BN_priv_rand_range()`, but it is not @@ -89,6 +89,10 @@ def __eq__(self, other) -> bool: # -1 less than, 0 is equal to, 1 is greater than return not bool(backend._lib.BN_cmp(self._backend_bignum, other._backend_bignum)) + @classmethod + def one(cls): + return cls(backend._lib.BN_value_one()) + def is_zero(self): # BN_is_zero() is not exported, so this will have to do return self == 0 diff --git a/umbral/hashing.py b/umbral/hashing.py index 96c292e9..a72bb6f0 100644 --- a/umbral/hashing.py +++ b/umbral/hashing.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional, Type, Iterable from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.primitives import hashes @@ -15,6 +15,9 @@ class Hash: + + OUTPUT_SIZE = 32 + def __init__(self, dst: bytes): self._sha256 = hashes.Hash(hashes.SHA256(), backend=backend) len_dst = len(dst).to_bytes(4, byteorder='big') @@ -73,6 +76,16 @@ def hash_to_shared_secret(precursor: CurvePoint, return digest_to_scalar(digest) + +def hash_to_cfrag_verification(points: Iterable[CurvePoint], metadata: Optional[bytes] = None) -> CurveScalar: + digest = Hash(b"CFRAG_VERIFICATION") + for point in points: + digest.update(bytes(point)) + if metadata is not None: + digest.update(metadata) + return digest_to_scalar(digest) + + def hash_to_cfrag_signature(kfrag_id: 'KeyFragID', commitment: CurvePoint, precursor: CurvePoint, diff --git a/umbral/pre.py b/umbral/pre.py index d7b51ad6..20eb4dc4 100644 --- a/umbral/pre.py +++ b/umbral/pre.py @@ -1,8 +1,10 @@ -from typing import Tuple +from typing import Tuple, Optional, Sequence from .capsule import Capsule +from .capsule_frag import CapsuleFrag from .dem import DEM from .keys import PublicKey, SecretKey +from .key_frag import KeyFrag def encrypt(pk: PublicKey, plaintext: bytes) -> Tuple[Capsule, bytes]: @@ -27,3 +29,21 @@ def decrypt_original(sk: SecretKey, capsule: Capsule, ciphertext: bytes) -> byte key_seed = capsule.open_original(sk) dem = DEM(bytes(key_seed)) return dem.decrypt(ciphertext, authenticated_data=bytes(capsule)) + + +def reencrypt(capsule: Capsule, kfrag: KeyFrag, metadata: Optional[bytes] = None) -> CapsuleFrag: + return CapsuleFrag.reencrypted(capsule, kfrag, metadata) + + +def decrypt_reencrypted(decrypting_sk: SecretKey, + delegating_pk: PublicKey, + capsule: Capsule, + cfrags: Sequence[CapsuleFrag], + ciphertext: bytes, + ) -> bytes: + + key_seed = capsule.open_reencrypted(decrypting_sk, delegating_pk, cfrags) + # TODO: add salt and info here? + dem = DEM(bytes(key_seed)) + return dem.decrypt(ciphertext, authenticated_data=bytes(capsule)) + From d532ef1383187f7190a65cb769d79fd60e091383 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Thu, 18 Mar 2021 19:01:56 -0700 Subject: [PATCH 10/25] Move all OpenSSL stuff into one module, move around some low-level details --- umbral/curve.py | 115 +---------- umbral/curve_point.py | 82 ++------ umbral/curve_scalar.py | 103 +++------- umbral/dem.py | 5 +- umbral/hashing.py | 54 ++--- umbral/keys.py | 78 ++----- umbral/openssl.py | 447 +++++++++++++++++++++++++++++++---------- 7 files changed, 419 insertions(+), 465 deletions(-) diff --git a/umbral/curve.py b/umbral/curve.py index 35f27f4e..dee0cc23 100644 --- a/umbral/curve.py +++ b/umbral/curve.py @@ -1,119 +1,10 @@ -from cryptography.hazmat.backends import default_backend - from . import openssl - -class Curve: - """ - Acts as a container to store constant variables such as the OpenSSL - curve_nid, the EC_GROUP struct, and the order of the curve. - - Contains a whitelist of supported elliptic curves used in pyUmbral. - - """ - - _supported_curves = { - 415: 'secp256r1', - 714: 'secp256k1', - 715: 'secp384r1' - } - - def __init__(self, nid: int) -> None: - """ - Instantiates an OpenSSL curve with the provided curve_nid and derives - the proper EC_GROUP struct and order. You can _only_ instantiate curves - with supported nids (see `Curve.supported_curves`). - """ - - try: - self.__curve_name = self._supported_curves[nid] - except KeyError: - raise NotImplementedError("Curve NID {} is not supported.".format(nid)) - - # set only once - self.__curve_nid = nid - self.__ec_group = openssl._get_ec_group_by_curve_nid(self.__curve_nid) - self.__order = openssl._get_ec_order_by_group(self.ec_group) - self.__generator = openssl._get_ec_generator_by_group(self.ec_group) - - # Init cache - self.__field_order_size_in_bytes = 0 - self.__group_order_size_in_bytes = 0 - - @classmethod - def from_name(cls, name: str) -> 'Curve': - """ - Alternate constructor to generate a curve instance by its name. - - Raises NotImplementedError if the name cannot be mapped to a known - supported curve NID. - - """ - - name = name.casefold() # normalize - - for supported_nid, supported_name in cls._supported_curves.items(): - if name == supported_name: - instance = cls(nid=supported_nid) - break - else: - message = "{} is not supported curve name.".format(name) - raise NotImplementedError(message) - - return instance - - def __eq__(self, other): - return self.__curve_nid == other.curve_nid - - def __str__(self): - return "".format(self.__curve_nid, self.__curve_name) - - # - # Immutable Curve Data - # - - @property - def field_order_size_in_bytes(self) -> int: - if not self.__field_order_size_in_bytes: - size_in_bits = openssl._get_ec_group_degree(self.__ec_group) - self.__field_order_size_in_bytes = (size_in_bits + 7) // 8 - return self.__field_order_size_in_bytes - - @property - def group_order_size_in_bytes(self) -> int: - if not self.__group_order_size_in_bytes: - BN_num_bytes = default_backend()._lib.BN_num_bytes - self.__group_order_size_in_bytes = BN_num_bytes(self.order) - return self.__group_order_size_in_bytes - - @property - def curve_nid(self) -> int: - return self.__curve_nid - - @property - def name(self) -> str: - return self.__curve_name - - @property - def ec_group(self): - return self.__ec_group - - @property - def order(self): - return self.__order - - @property - def generator(self): - return self.__generator - - -# # Global Curve Instances -# -SECP256R1 = Curve.from_name('secp256r1') -SECP256K1 = Curve.from_name('secp256k1') -SECP384R1 = Curve.from_name('secp384r1') +SECP256R1 = openssl.Curve.from_name('secp256r1') +SECP256K1 = openssl.Curve.from_name('secp256k1') +SECP384R1 = openssl.Curve.from_name('secp384r1') CURVES = (SECP256K1, SECP256R1, SECP384R1) diff --git a/umbral/curve_point.py b/umbral/curve_point.py index 5ca39d01..0067fba8 100644 --- a/umbral/curve_point.py +++ b/umbral/curve_point.py @@ -1,7 +1,5 @@ from typing import Optional, Tuple -from cryptography.hazmat.backends.openssl import backend - from . import openssl from .curve import CURVE from .curve_scalar import CurveScalar @@ -18,7 +16,7 @@ def __init__(self, backend_point) -> None: @classmethod def generator(cls) -> 'CurvePoint': - return cls(CURVE.generator) + return cls(CURVE.point_generator) @classmethod def random(cls) -> 'CurvePoint': @@ -29,101 +27,54 @@ def random(cls) -> 'CurvePoint': return cls.generator() * CurveScalar.random_nonzero() @classmethod - def from_affine(cls, coords: Tuple[int, int]) -> 'CurvePoint': + def from_affine(cls, affine_x: int, affine_y: int) -> 'CurvePoint': """ Returns a CurvePoint object from the given affine coordinates in a tuple in the format of (x, y) and a given curve. """ - affine_x, affine_y = coords - if type(affine_x) == int: - affine_x = openssl._int_to_bn(affine_x, curve=None) - - if type(affine_y) == int: - affine_y = openssl._int_to_bn(affine_y, curve=None) - - backend_point = openssl._get_EC_POINT_via_affine(affine_x, affine_y, CURVE) + backend_point = openssl.point_from_affine_coords(CURVE, affine_x, affine_y) return cls(backend_point) - def to_affine(self): + def to_affine(self) -> Tuple[int, int]: """ Returns a tuple of Python ints in the format of (x, y) that represents the point in the curve. """ - affine_x, affine_y = openssl._get_affine_coords_via_EC_POINT(self._backend_point, CURVE) - return (backend._bn_to_int(affine_x), backend._bn_to_int(affine_y)) + return openssl.point_to_affine_coords(CURVE, self._backend_point) @classmethod def __take__(cls, data: bytes) -> Tuple['CurvePoint', bytes]: """ Returns a CurvePoint object from the given byte data on the curve provided. """ - size = CURVE.field_order_size_in_bytes + 1 # compressed point size + size = CURVE.field_element_size + 1 # compressed point size point_data, data = cls.__take_bytes__(data, size) - - point = openssl._get_new_EC_POINT(CURVE) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_oct2point( - CURVE.ec_group, point, point_data, len(point_data), bn_ctx); - backend.openssl_assert(res == 1) - + point = openssl.point_from_bytes(CURVE, point_data) return cls(point), data def __bytes__(self) -> bytes: """ Returns the CurvePoint serialized as bytes in the compressed form. """ - point_conversion_form = backend._lib.POINT_CONVERSION_COMPRESSED - size = CURVE.field_order_size_in_bytes + 1 # compressed point size - - bin_ptr = backend._ffi.new("unsigned char[]", size) - with backend._tmp_bn_ctx() as bn_ctx: - bin_len = backend._lib.EC_POINT_point2oct( - CURVE.ec_group, self._backend_point, point_conversion_form, - bin_ptr, size, bn_ctx - ) - backend.openssl_assert(bin_len != 0) - - return bytes(backend._ffi.buffer(bin_ptr, bin_len)[:]) + return openssl.point_to_bytes_compressed(CURVE, self._backend_point) def __eq__(self, other): """ Compares two EC_POINTS for equality. """ - with backend._tmp_bn_ctx() as bn_ctx: - is_equal = backend._lib.EC_POINT_cmp( - CURVE.ec_group, self._backend_point, other._backend_point, bn_ctx - ) - backend.openssl_assert(is_equal != -1) - - # 1 is not-equal, 0 is equal, -1 is error - return not bool(is_equal) + return openssl.point_eq(CURVE, self._backend_point, other._backend_point) def __mul__(self, other: CurveScalar) -> 'CurvePoint': """ Performs an EC_POINT_mul on an EC_POINT and a BIGNUM. """ - # TODO: Check that both points use the same curve. - prod = openssl._get_new_EC_POINT(CURVE) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_mul( - CURVE.ec_group, prod, backend._ffi.NULL, - self._backend_point, other._backend_bignum, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurvePoint(prod) + return CurvePoint(openssl.point_mul_bn(CURVE, self._backend_point, other._backend_bignum)) def __add__(self, other: 'CurvePoint') -> 'CurvePoint': """ Performs an EC_POINT_add on two EC_POINTS. """ - op_sum = openssl._get_new_EC_POINT(CURVE) - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_add( - CURVE.ec_group, op_sum, self._backend_point, other._backend_point, bn_ctx - ) - backend.openssl_assert(res == 1) - return CurvePoint(op_sum) + return CurvePoint(openssl.point_add(CURVE, self._backend_point, other._backend_point)) def __sub__(self, other: 'CurvePoint') -> 'CurvePoint': """ @@ -136,13 +87,4 @@ def __neg__(self) -> 'CurvePoint': Computes the additive inverse of a CurvePoint, by performing an EC_POINT_invert on itself. """ - inv = backend._lib.EC_POINT_dup(self._backend_point, CURVE.ec_group) - backend.openssl_assert(inv != backend._ffi.NULL) - inv = backend._ffi.gc(inv, backend._lib.EC_POINT_clear_free) - - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_POINT_invert( - CURVE.ec_group, inv, bn_ctx - ) - backend.openssl_assert(res == 1) - return CurvePoint(inv) + return CurvePoint(openssl.point_neg(CURVE, self._backend_point)) diff --git a/umbral/curve_scalar.py b/umbral/curve_scalar.py index 05375e17..da5f2859 100644 --- a/umbral/curve_scalar.py +++ b/umbral/curve_scalar.py @@ -1,23 +1,23 @@ -from typing import Optional, Union, Tuple - -from cryptography.hazmat.backends.openssl import backend +from typing import TYPE_CHECKING, Optional, Union, Tuple from . import openssl from .curve import CURVE from .serializable import Serializable +if TYPE_CHECKING: # pragma: no cover + from .hashing import Hash class CurveScalar(Serializable): """ Represents an OpenSSL Bignum modulo the order of a curve. Some of these operations will only work with prime numbers. + By default, the underlying OpenSSL BIGNUM has BN_FLG_CONSTTIME set for constant time operations. """ def __init__(self, backend_bignum): - on_curve = openssl._bn_is_on_curve(backend_bignum, CURVE) - if not on_curve: + if not openssl.bn_is_normalized(backend_bignum, CURVE.bn_order): raise ValueError("The provided BIGNUM is not on the provided curve.") self._backend_bignum = backend_bignum @@ -27,57 +27,42 @@ def random_nonzero(cls) -> 'CurveScalar': """ Returns a CurveScalar object with a cryptographically secure OpenSSL BIGNUM. """ - one = cls.one()._backend_bignum - - # TODO: in most cases, we want this number to be secret. - # OpenSSL 1.1.1 has `BN_priv_rand_range()`, but it is not - # currently exported by `cryptography`. - # Use when available. - - # Calculate `order - 1` - order_minus_1 = openssl._get_new_BN() - res = backend._lib.BN_sub(order_minus_1, CURVE.order, one) - backend.openssl_assert(res == 1) - - # Get a random in range `[0, order - 1)` - new_rand_bn = openssl._get_new_BN() - res = backend._lib.BN_rand_range(new_rand_bn, order_minus_1) - backend.openssl_assert(res == 1) - - # Turn it into a random in range `[1, order)` - op_sum = openssl._get_new_BN() - res = backend._lib.BN_add(op_sum, new_rand_bn, one) - backend.openssl_assert(res == 1) - - return cls(op_sum) + return cls(openssl.bn_random_nonzero(CURVE.bn_order)) @classmethod def from_int(cls, num: int) -> 'CurveScalar': """ Returns a CurveScalar object from a given integer on a curve. """ - conv_bn = openssl._int_to_bn(num, CURVE) + conv_bn = openssl.bn_from_int(num, modulus=CURVE.bn_order) return cls(conv_bn) + @classmethod + def from_digest(cls, digest: 'Hash') -> 'CurveScalar': + # TODO (#39): to be replaced by the standard algroithm. + # Currently just matching what we have in RustCrypto stack + # (taking bytes modulo curve order). + # Can produce zeros! + bn = openssl.bn_from_bytes(digest.finalize(), modulus=CURVE.bn_order) + return cls(bn) + @classmethod def __take__(cls, data: bytes) -> Tuple['CurveScalar', bytes]: - size = backend._lib.BN_num_bytes(CURVE.order) - scalar_data, data = cls.__take_bytes__(data, size) - bignum = openssl._bytes_to_bn(scalar_data) + scalar_data, data = cls.__take_bytes__(data, CURVE.scalar_size) + bignum = openssl.bn_from_bytes(scalar_data) return cls(bignum), data def __bytes__(self) -> bytes: """ Returns the CurveScalar as bytes. """ - size = backend._lib.BN_num_bytes(CURVE.order) - return openssl._bn_to_bytes(self._backend_bignum, size) + return openssl.bn_to_bytes(self._backend_bignum, CURVE.scalar_size) def __int__(self) -> int: """ Converts the CurveScalar to a Python int. """ - return backend._bn_to_int(self._backend_bignum) + return openssl.bn_to_int(self._backend_bignum) def __eq__(self, other) -> bool: """ @@ -85,17 +70,14 @@ def __eq__(self, other) -> bool: """ if type(other) == int: other = CurveScalar.from_int(other) - - # -1 less than, 0 is equal to, 1 is greater than - return not bool(backend._lib.BN_cmp(self._backend_bignum, other._backend_bignum)) + return openssl.bn_cmp(self._backend_bignum, other._backend_bignum) == 0 @classmethod def one(cls): - return cls(backend._lib.BN_value_one()) + return cls(openssl.bn_one()) def is_zero(self): - # BN_is_zero() is not exported, so this will have to do - return self == 0 + return openssl.bn_is_zero(self._backend_bignum) def __mul__(self, other: Union[int, 'CurveScalar']) -> 'CurveScalar': """ @@ -103,15 +85,7 @@ def __mul__(self, other: Union[int, 'CurveScalar']) -> 'CurveScalar': """ if isinstance(other, int): other = CurveScalar.from_int(other) - - product = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_mul( - product, self._backend_bignum, other._backend_bignum, CURVE.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveScalar(product) + return CurveScalar(openssl.bn_mul(self._backend_bignum, other._backend_bignum, CURVE.bn_order)) def __add__(self, other : Union[int, 'CurveScalar']) -> 'CurveScalar': """ @@ -119,15 +93,7 @@ def __add__(self, other : Union[int, 'CurveScalar']) -> 'CurveScalar': """ if isinstance(other, int): other = CurveScalar.from_int(other) - - op_sum = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_add( - op_sum, self._backend_bignum, other._backend_bignum, CURVE.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveScalar(op_sum) + return CurveScalar(openssl.bn_add(self._backend_bignum, other._backend_bignum, CURVE.bn_order)) def __sub__(self, other : Union[int, 'CurveScalar']) -> 'CurveScalar': """ @@ -135,26 +101,11 @@ def __sub__(self, other : Union[int, 'CurveScalar']) -> 'CurveScalar': """ if isinstance(other, int): other = CurveScalar.from_int(other) - - diff = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod_sub( - diff, self._backend_bignum, other._backend_bignum, CURVE.order, bn_ctx - ) - backend.openssl_assert(res == 1) - - return CurveScalar(diff) + return CurveScalar(openssl.bn_sub(self._backend_bignum, other._backend_bignum, CURVE.bn_order)) def invert(self) -> 'CurveScalar': """ Performs a BN_mod_inverse. WARNING: Only in constant time if BN_FLG_CONSTTIME is set on the BN. """ - with backend._tmp_bn_ctx() as bn_ctx: - inv = backend._lib.BN_mod_inverse( - backend._ffi.NULL, self._backend_bignum, CURVE.order, bn_ctx - ) - backend.openssl_assert(inv != backend._ffi.NULL) - inv = backend._ffi.gc(inv, backend._lib.BN_clear_free) - - return CurveScalar(inv) + return CurveScalar(openssl.bn_invert(self._backend_bignum, CURVE.bn_order)) diff --git a/umbral/dem.py b/umbral/dem.py index 635973f4..bd705bac 100644 --- a/umbral/dem.py +++ b/umbral/dem.py @@ -2,7 +2,6 @@ from typing import Optional from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.primitives import hashes from nacl.bindings.crypto_aead import ( @@ -12,6 +11,8 @@ crypto_aead_xchacha20poly1305_ietf_NPUBBYTES as XCHACHA_NONCE_SIZE, ) +from . import openssl + def kdf(data: bytes, key_length: int, @@ -23,7 +24,7 @@ def kdf(data: bytes, length=key_length, salt=salt, info=info, - backend=backend) + backend=openssl.backend) return hkdf.derive(data) diff --git a/umbral/hashing.py b/umbral/hashing.py index a72bb6f0..303c1ce0 100644 --- a/umbral/hashing.py +++ b/umbral/hashing.py @@ -1,10 +1,8 @@ from typing import TYPE_CHECKING, Optional, Type, Iterable -from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.primitives import hashes -from cryptography.exceptions import InternalError -from . import openssl +from .openssl import backend, ErrorInvalidCompressedPoint from .curve import CURVE from .curve_scalar import CurveScalar from .curve_point import CurvePoint @@ -19,30 +17,17 @@ class Hash: OUTPUT_SIZE = 32 def __init__(self, dst: bytes): - self._sha256 = hashes.Hash(hashes.SHA256(), backend=backend) + self._backend_hash_algorithm = hashes.SHA256() + self._hash = hashes.Hash(self._backend_hash_algorithm, backend=backend) + len_dst = len(dst).to_bytes(4, byteorder='big') self.update(len_dst + dst) def update(self, data: bytes) -> None: - self._sha256.update(data) + self._hash.update(data) def finalize(self) -> bytes: - return self._sha256.finalize() - - -def digest_to_scalar(digest: Hash) -> CurveScalar: - # TODO: to be replaced by the standard algroithm. - # Currently just matching what we have in RustCrypto stack. - # Can produce zeros! - - hash_digest = openssl._bytes_to_bn(digest.finalize()) - - bignum = openssl._get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod(bignum, hash_digest, CURVE.order, bn_ctx) - backend.openssl_assert(res == 1) - - return CurveScalar(bignum) + return self._hash.finalize() def hash_to_polynomial_arg(precursor: CurvePoint, @@ -55,14 +40,14 @@ def hash_to_polynomial_arg(precursor: CurvePoint, digest.update(bytes(pubkey)) digest.update(bytes(dh_point)) digest.update(bytes(kfrag_id)) - return digest_to_scalar(digest) + return CurveScalar.from_digest(digest) def hash_capsule_points(e: CurvePoint, v: CurvePoint) -> CurveScalar: digest = Hash(b"CAPSULE_POINTS") digest.update(bytes(e)) digest.update(bytes(v)) - return digest_to_scalar(digest) + return CurveScalar.from_digest(digest) def hash_to_shared_secret(precursor: CurvePoint, @@ -73,7 +58,7 @@ def hash_to_shared_secret(precursor: CurvePoint, digest.update(bytes(precursor)) digest.update(bytes(pubkey)) digest.update(bytes(dh_point)) - return digest_to_scalar(digest) + return CurveScalar.from_digest(digest) @@ -83,7 +68,7 @@ def hash_to_cfrag_verification(points: Iterable[CurvePoint], metadata: Optional[ digest.update(bytes(point)) if metadata is not None: digest.update(metadata) - return digest_to_scalar(digest) + return CurveScalar.from_digest(digest) def hash_to_cfrag_signature(kfrag_id: 'KeyFragID', @@ -122,10 +107,10 @@ def update(self, value): self._digest.update(value) def sign(self, sk: SecretKey) -> Signature: - return sk.sign_digest(self._digest, hashes.SHA256) + return sk.sign_digest(self._digest) def verify(self, pk: PublicKey, sig: Signature): - return sig.verify_digest(pk, self._digest, hashes.SHA256) + return sig.verify_digest(pk, self._digest) def unsafe_hash_to_point(dst: bytes, data: bytes) -> CurvePoint: @@ -146,22 +131,15 @@ def unsafe_hash_to_point(dst: bytes, data: bytes) -> CurvePoint: ibytes = i.to_bytes(4, byteorder='big') digest = Hash(dst) digest.update(data_with_len + ibytes) - point_data = digest.finalize()[:CURVE.field_order_size_in_bytes] + point_data = digest.finalize()[:CURVE.field_element_size] compressed_point = sign + point_data try: return CurvePoint.from_bytes(compressed_point) - except InternalError as e: - # We want to catch specific InternalExceptions: - # - Point not in the curve (code 107) - # - Invalid compressed point (code 110) - # https://github.com/openssl/openssl/blob/master/include/openssl/ecerr.h#L228 - if e.err_code[0].reason in (107, 110): - pass - else: - # Any other exception, we raise it - raise e + except ErrorInvalidCompressedPoint: + # If it is not a valid point, continue on + pass # Only happens with probability 2^(-32) raise ValueError('Could not hash input into the curve') # pragma: no cover diff --git a/umbral/keys.py b/umbral/keys.py index 46d5023a..9333dcdf 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -1,8 +1,6 @@ from typing import TYPE_CHECKING, Tuple from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends.openssl import backend -from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey, _EllipticCurvePublicKey from cryptography.hazmat.primitives.asymmetric import utils from cryptography.hazmat.primitives.asymmetric.ec import ECDSA @@ -51,51 +49,19 @@ def __take__(cls, data: bytes) -> Tuple['SecretKey', bytes]: def __bytes__(self) -> bytes: return bytes(self._scalar_key) - def to_cryptography_privkey(self) -> _EllipticCurvePrivateKey: - """ - Returns a cryptography.io EllipticCurvePrivateKey from the Umbral key. - """ - ec_key = backend._lib.EC_KEY_new() - backend.openssl_assert(ec_key != backend._ffi.NULL) - ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) - - set_group_result = backend._lib.EC_KEY_set_group(ec_key, CURVE.ec_group) - backend.openssl_assert(set_group_result == 1) - - set_privkey_result = backend._lib.EC_KEY_set_private_key( - ec_key, self._scalar_key._backend_bignum - ) - backend.openssl_assert(set_privkey_result == 1) - - # Get public key - point = openssl._get_new_EC_POINT(CURVE) - with backend._tmp_bn_ctx() as bn_ctx: - mult_result = backend._lib.EC_POINT_mul( - CURVE.ec_group, point, self._scalar_key._backend_bignum, - backend._ffi.NULL, backend._ffi.NULL, bn_ctx - ) - backend.openssl_assert(mult_result == 1) - - set_pubkey_result = backend._lib.EC_KEY_set_public_key(ec_key, point) - backend.openssl_assert(set_pubkey_result == 1) - - evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) - return _EllipticCurvePrivateKey(backend, ec_key, evp_pkey) - - def sign_digest(self, digest: 'Hash', backend_hash_algorithm) -> 'Signature': + def sign_digest(self, digest: 'Hash') -> 'Signature': - signature_algorithm = ECDSA(utils.Prehashed(backend_hash_algorithm())) + signature_algorithm = ECDSA(utils.Prehashed(digest._backend_hash_algorithm)) message = digest.finalize() - cpk = self.to_cryptography_privkey() - signature_der_bytes = cpk.sign(message, signature_algorithm) + backend_sk = openssl.bn_to_privkey(CURVE, self._scalar_key._backend_bignum) + signature_der_bytes = backend_sk.sign(message, signature_algorithm) r, s = utils.decode_dss_signature(signature_der_bytes) # Normalize s # s is public, so no constant-timeness required here - order = backend._bn_to_int(CURVE.order) - if s > (order >> 1): - s = order - s + if s > (CURVE.order >> 1): + s = CURVE.order - s return Signature(CurveScalar.from_int(r), CurveScalar.from_int(s)) @@ -114,17 +80,18 @@ def __init__(self, r: CurveScalar, s: CurveScalar): def __repr__(self): return f"ECDSA Signature: {bytes(self).hex()[:15]}" - def verify_digest(self, verifying_key: 'PublicKey', digest: 'Hash', backend_hash_algorithm) -> bool: - cryptography_pub_key = verifying_key.to_cryptography_pubkey() - signature_algorithm = ECDSA(utils.Prehashed(backend_hash_algorithm())) + def verify_digest(self, verifying_key: 'PublicKey', digest: 'Hash') -> bool: + backend_pk = openssl.point_to_pubkey(CURVE, verifying_key.point()._backend_point) + signature_algorithm = ECDSA(utils.Prehashed(digest._backend_hash_algorithm)) + message = digest.finalize() signature_der_bytes = utils.encode_dss_signature(int(self.r), int(self.s)) # TODO: Raise error instead of returning boolean try: - cryptography_pub_key.verify(signature=signature_der_bytes, - data=message, - signature_algorithm=signature_algorithm) + backend_pk.verify(signature=signature_der_bytes, + data=message, + signature_algorithm=signature_algorithm) except InvalidSignature: return False return True @@ -161,25 +128,6 @@ def __take__(cls, data: bytes) -> Tuple['PublicKey', bytes]: def __bytes__(self) -> bytes: return bytes(self._point_key) - def to_cryptography_pubkey(self) -> _EllipticCurvePublicKey: - """ - Returns a cryptography.io EllipticCurvePublicKey from the Umbral key. - """ - ec_key = backend._lib.EC_KEY_new() - backend.openssl_assert(ec_key != backend._ffi.NULL) - ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) - - set_group_result = backend._lib.EC_KEY_set_group(ec_key, CURVE.ec_group) - backend.openssl_assert(set_group_result == 1) - - set_pubkey_result = backend._lib.EC_KEY_set_public_key( - ec_key, self._point_key._backend_point - ) - backend.openssl_assert(set_pubkey_result == 1) - - evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) - return _EllipticCurvePublicKey(backend, ec_key, evp_pkey) - def __str__(self): return f"{self.__class__.__name__}:{bytes(self).hex()[:16]}" diff --git a/umbral/openssl.py b/umbral/openssl.py index 2b2e7f76..5801afb4 100644 --- a/umbral/openssl.py +++ b/umbral/openssl.py @@ -1,91 +1,153 @@ from contextlib import contextmanager -import typing +from typing import Tuple +from cryptography.exceptions import InternalError from cryptography.hazmat.backends.openssl import backend +from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey, _EllipticCurvePublicKey -@typing.no_type_check -def _get_new_BN(set_consttime_flag=True): +class Curve: """ - Returns a new and initialized OpenSSL BIGNUM. - The set_consttime_flag is set to True by default. When this instance of a - CurveBN object has BN_FLG_CONSTTIME set, OpenSSL will use constant time - operations whenever this CurveBN is passed. + Acts as a container to store constant variables such as the OpenSSL + curve_nid, the EC_GROUP struct, and the order of the curve. + + Contains a whitelist of supported elliptic curves used in pyUmbral. """ - new_bn = backend._lib.BN_new() - backend.openssl_assert(new_bn != backend._ffi.NULL) - new_bn = backend._ffi.gc(new_bn, backend._lib.BN_clear_free) - if set_consttime_flag: - backend._lib.BN_set_flags(new_bn, backend._lib.BN_FLG_CONSTTIME) - return new_bn + _supported_curves = { + 415: 'secp256r1', + 714: 'secp256k1', + 715: 'secp384r1' + } + + @staticmethod + def _get_ec_group_by_curve_nid(nid: int): + """ + Returns the group of a given curve via its OpenSSL nid. This must be freed + after each use otherwise it leaks memory. + """ + group = backend._lib.EC_GROUP_new_by_curve_name(nid) + backend.openssl_assert(group != backend._ffi.NULL) + return group + + @staticmethod + def _get_ec_order_by_group(ec_group): + """ + Returns the order of a given curve via its OpenSSL EC_GROUP. + """ + ec_order = _bn_new() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_GROUP_get_order(ec_group, ec_order, bn_ctx) + backend.openssl_assert(res == 1) + return ec_order + @staticmethod + def _get_ec_generator_by_group(ec_group): + """ + Returns the generator point of a given curve via its OpenSSL EC_GROUP. + """ + generator = backend._lib.EC_GROUP_get0_generator(ec_group) + backend.openssl_assert(generator != backend._ffi.NULL) + generator = backend._ffi.gc(generator, backend._lib.EC_POINT_clear_free) -@typing.no_type_check -def _get_ec_group_by_curve_nid(curve_nid: int): - """ - Returns the group of a given curve via its OpenSSL nid. This must be freed - after each use otherwise it leaks memory. - """ - group = backend._lib.EC_GROUP_new_by_curve_name(curve_nid) - backend.openssl_assert(group != backend._ffi.NULL) + return generator - return group + @staticmethod + def _get_ec_group_degree(ec_group): + """ + Returns the number of bits needed to represent the order of the finite + field upon the curve is based. + """ + return backend._lib.EC_GROUP_get_degree(ec_group) + def __init__(self, nid: int): + """ + Instantiates an OpenSSL curve with the provided curve_nid and derives + the proper EC_GROUP struct and order. You can _only_ instantiate curves + with supported nids (see `Curve.supported_curves`). + """ -@typing.no_type_check -def _get_ec_order_by_group(ec_group): - """ - Returns the order of a given curve via its OpenSSL EC_GROUP. - """ - ec_order = _get_new_BN() - with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.EC_GROUP_get_order(ec_group, ec_order, bn_ctx) - backend.openssl_assert(res == 1) - return ec_order + try: + self.name = self._supported_curves[nid] + except KeyError: + raise NotImplementedError("Curve NID {} is not supported.".format(nid)) + self.nid = nid + + self.ec_group = self._get_ec_group_by_curve_nid(self.nid) + self.bn_order = self._get_ec_order_by_group(self.ec_group) + self.point_generator = self._get_ec_generator_by_group(self.ec_group) + + size_in_bits = self._get_ec_group_degree(self.ec_group) + self.field_element_size = (size_in_bits + 7) // 8 + + self.scalar_size = _bn_size(self.bn_order) + self.order = bn_to_int(self.bn_order) + + @classmethod + def from_name(cls, name: str) -> 'Curve': + """ + Alternate constructor to generate a curve instance by its name. + + Raises NotImplementedError if the name cannot be mapped to a known + supported curve NID. + """ + + name = name.casefold() # normalize + + for supported_nid, supported_name in cls._supported_curves.items(): + if name == supported_name: + instance = cls(nid=supported_nid) + break + else: + raise NotImplementedError(f"{name} is not supported curve name.") + + return instance + + def __eq__(self, other): + return self.nid == other.nid + + def __str__(self): + return "".format(self.nid, self.name) -@typing.no_type_check -def _get_ec_generator_by_group(ec_group): - """ - Returns the generator point of a given curve via its OpenSSL EC_GROUP. - """ - generator = backend._lib.EC_GROUP_get0_generator(ec_group) - backend.openssl_assert(generator != backend._ffi.NULL) - generator = backend._ffi.gc(generator, backend._lib.EC_POINT_clear_free) - return generator +# +# OpenSSL bignums +# -@typing.no_type_check -def _get_ec_group_degree(ec_group): +def _bn_new(set_consttime_flag=True): """ - Returns the number of bits needed to represent the order of the finite - field upon the curve is based. + Returns a new and initialized OpenSSL BIGNUM. + The set_consttime_flag is set to True by default. When this instance of a + CurveBN object has BN_FLG_CONSTTIME set, OpenSSL will use constant time + operations whenever this CurveBN is passed. """ - return backend._lib.EC_GROUP_get_degree(ec_group) + new_bn = backend._lib.BN_new() + backend.openssl_assert(new_bn != backend._ffi.NULL) + new_bn = backend._ffi.gc(new_bn, backend._lib.BN_clear_free) + + if set_consttime_flag: + backend._lib.BN_set_flags(new_bn, backend._lib.BN_FLG_CONSTTIME) + return new_bn -@typing.no_type_check -def _bn_is_on_curve(check_bn, curve: 'Curve'): +def bn_is_normalized(check_bn, modulus): """ - Checks if a given OpenSSL BIGNUM is within the provided curve's order. - Returns True if the provided BN is on the curve, that is in the range `[0, curve_order)`. + Returns ``True`` if ``check_bn`` is in ``[0, modulus)``, ``False`` otherwise. """ zero = backend._int_to_bn(0) zero = backend._ffi.gc(zero, backend._lib.BN_clear_free) check_sign = backend._lib.BN_cmp(check_bn, zero) - range_check = backend._lib.BN_cmp(check_bn, curve.order) + range_check = backend._lib.BN_cmp(check_bn, modulus) return (check_sign == 1 or check_sign == 0) and range_check == -1 -@typing.no_type_check -def _int_to_bn(py_int: int, curve: 'Curve'=None, set_consttime_flag=True): +def bn_from_int(py_int: int, modulus=None, set_consttime_flag=True): """ - Converts the given Python int to an OpenSSL BIGNUM. If a curve is - provided, it will check if the Python integer is within the order of that - curve. If it's not within the order, it will raise a ValueError. + Converts the given Python int to an OpenSSL BIGNUM. If ``modulus`` is + provided, it will check if the Python integer is within ``[0, modulus)``. If set_consttime_flag is set to True, OpenSSL will use constant time operations when using this CurveBN. @@ -93,109 +155,290 @@ def _int_to_bn(py_int: int, curve: 'Curve'=None, set_consttime_flag=True): conv_bn = backend._int_to_bn(py_int) conv_bn = backend._ffi.gc(conv_bn, backend._lib.BN_clear_free) - if curve: - on_curve = _bn_is_on_curve(conv_bn, curve) - if not on_curve: - raise ValueError("The Python integer given is not on the provided curve.") + if modulus and not bn_is_normalized(conv_bn, modulus): + raise ValueError("The Python integer given is not under the provided modulus.") if set_consttime_flag: backend._lib.BN_set_flags(conv_bn, backend._lib.BN_FLG_CONSTTIME) return conv_bn -@typing.no_type_check -def _bytes_to_bn(bytes_seq: bytes, set_consttime_flag=True): + +def bn_from_bytes(bytes_seq: bytes, set_consttime_flag=True, modulus=None): """ Converts the given byte sequence to an OpenSSL BIGNUM. If set_consttime_flag is set to True, OpenSSL will use constant time operations when using this BIGNUM. """ - bn = _get_new_BN(set_consttime_flag) + bn = _bn_new(set_consttime_flag) backend._lib.BN_bin2bn(bytes_seq, len(bytes_seq), bn) backend.openssl_assert(bn != backend._ffi.NULL) + + if modulus: + bignum =_bn_new() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod(bignum, bn, modulus, bn_ctx) + backend.openssl_assert(res == 1) + return bn -@typing.no_type_check -def _bn_to_bytes(bignum, length : int = None): + +def bn_to_bytes(bn, length: int): """ Converts the given OpenSSL BIGNUM into a Python bytes sequence. If length is given, the return bytes will have such length. If the BIGNUM doesn't fit, it raises a ValueError. """ - if bignum is None or bignum == backend._ffi.NULL: - raise ValueError("Input BIGNUM must have a value") - - bn_num_bytes = backend._lib.BN_num_bytes(bignum) - if length is None: - length = bn_num_bytes - elif bn_num_bytes > length: - raise ValueError("Input BIGNUM doesn't fit in {} B".format(length)) + # Sanity check, CurveScalar ensures it won't happen. + bn_num_bytes = backend._lib.BN_num_bytes(bn) + assert bn_num_bytes <= length, f"Input BIGNUM doesn't fit in {length} B" bin_ptr = backend._ffi.new("unsigned char []", length) - bin_len = backend._lib.BN_bn2bin(bignum, bin_ptr) + bin_len = backend._lib.BN_bn2bin(bn, bin_ptr) return bytes.rjust(backend._ffi.buffer(bin_ptr, bin_len)[:], length, b'\0') -@typing.no_type_check -def _get_new_EC_POINT(curve: 'Curve'): +def bn_random_nonzero(modulus): + + one = backend._lib.BN_value_one() + + # TODO: in most cases, we want this number to be secret. + # OpenSSL 1.1.1 has `BN_priv_rand_range()`, but it is not + # currently exported by `cryptography`. + # Use when available. + + # Calculate `modulus - 1` + modulus_minus_1 = _bn_new() + res = backend._lib.BN_sub(modulus_minus_1, modulus, one) + backend.openssl_assert(res == 1) + + # Get a random in range `[0, modulus - 1)` + new_rand_bn = _bn_new() + res = backend._lib.BN_rand_range(new_rand_bn, modulus_minus_1) + backend.openssl_assert(res == 1) + + # Turn it into a random in range `[1, modulus)` + op_sum = _bn_new() + res = backend._lib.BN_add(op_sum, new_rand_bn, one) + backend.openssl_assert(res == 1) + + return op_sum + + +def _bn_size(bn): + return backend._lib.BN_num_bytes(bn) + + +def bn_to_int(bn): + return backend._bn_to_int(bn) + + +def bn_cmp(bn1, bn2): + # -1 less than, 0 is equal to, 1 is greater than + return backend._lib.BN_cmp(bn1, bn2) + + +def bn_one(): + return backend._lib.BN_value_one() + + +def bn_is_zero(bn): + # No special function exported in the current backend, so this will have to do + return bn_cmp(bn, bn_from_int(0)) == 0 + + +def bn_invert(bn, modulus): + with backend._tmp_bn_ctx() as bn_ctx: + inv = backend._lib.BN_mod_inverse(backend._ffi.NULL, bn, modulus, bn_ctx) + backend.openssl_assert(inv != backend._ffi.NULL) + inv = backend._ffi.gc(inv, backend._lib.BN_clear_free) + return inv + + +def bn_sub(bn1, bn2, modulus): + diff = _bn_new() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod_sub(diff, bn1, bn2, modulus, bn_ctx) + backend.openssl_assert(res == 1) + return diff + + +def bn_add(bn1, bn2, modulus): + op_sum = _bn_new() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod_add(op_sum, bn1, bn2, modulus, bn_ctx) + backend.openssl_assert(res == 1) + return op_sum + + +def bn_mul(bn1, bn2, modulus): + product = _bn_new() + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.BN_mod_mul(product, bn1, bn2, modulus, bn_ctx) + backend.openssl_assert(res == 1) + return product + + +def bn_to_privkey(curve: Curve, bn): + + ec_key = backend._lib.EC_KEY_new() + backend.openssl_assert(ec_key != backend._ffi.NULL) + ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) + + set_group_result = backend._lib.EC_KEY_set_group(ec_key, curve.ec_group) + backend.openssl_assert(set_group_result == 1) + + set_privkey_result = backend._lib.EC_KEY_set_private_key(ec_key, bn) + backend.openssl_assert(set_privkey_result == 1) + + evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) + return _EllipticCurvePrivateKey(backend, ec_key, evp_pkey) + + +# +# OpenSSL EC points +# + + +def _point_new(ec_group): """ Returns a new and initialized OpenSSL EC_POINT given the group of a curve. If __curve_nid is provided, it retrieves the group from the curve provided. """ - new_point = backend._lib.EC_POINT_new(curve.ec_group) + new_point = backend._lib.EC_POINT_new(ec_group) backend.openssl_assert(new_point != backend._ffi.NULL) new_point = backend._ffi.gc(new_point, backend._lib.EC_POINT_clear_free) return new_point -@typing.no_type_check -def _get_EC_POINT_via_affine(affine_x, affine_y, curve: 'Curve'): +def point_from_affine_coords(curve: Curve, affine_x: int, affine_y: int): """ Returns an EC_POINT given the group of a curve and the affine coordinates provided. """ - new_point = _get_new_EC_POINT(curve) + bn_affine_x = bn_from_int(affine_x) + bn_affine_y = bn_from_int(affine_y) + + new_point = _point_new(curve.ec_group) with backend._tmp_bn_ctx() as bn_ctx: res = backend._lib.EC_POINT_set_affine_coordinates_GFp( - curve.ec_group, new_point, affine_x, affine_y, bn_ctx + curve.ec_group, new_point, bn_affine_x, bn_affine_y, bn_ctx ) backend.openssl_assert(res == 1) return new_point -@typing.no_type_check -def _get_affine_coords_via_EC_POINT(ec_point, curve: 'Curve'): +def point_to_affine_coords(curve: Curve, point) -> Tuple[int, int]: """ Returns the affine coordinates of a given point on the provided ec_group. """ - affine_x = _get_new_BN() - affine_y = _get_new_BN() + affine_x = _bn_new() + affine_y = _bn_new() with backend._tmp_bn_ctx() as bn_ctx: res = backend._lib.EC_POINT_get_affine_coordinates_GFp( - curve.ec_group, ec_point, affine_x, affine_y, bn_ctx + curve.ec_group, point, affine_x, affine_y, bn_ctx ) backend.openssl_assert(res == 1) - return (affine_x, affine_y) + return bn_to_int(affine_x), bn_to_int(affine_y) -@typing.no_type_check -@contextmanager -def _tmp_bn_mont_ctx(modulus): - """ - Initializes and returns a BN_MONT_CTX for Montgomery ops. - Requires a modulus to place in the Montgomery structure. - """ - bn_mont_ctx = backend._lib.BN_MONT_CTX_new() - backend.openssl_assert(bn_mont_ctx != backend._ffi.NULL) - # Don't set the garbage collector. Only free it when the context is done - # or else you'll get a null pointer error. +class ErrorInvalidCompressedPoint(Exception): + pass + + +class ErrorInvalidPointEncoding(Exception): + pass + + +def point_from_bytes(curve: Curve, data): + point = _point_new(curve.ec_group) try: with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_MONT_CTX_set(bn_mont_ctx, modulus, bn_ctx) + res = backend._lib.EC_POINT_oct2point(curve.ec_group, point, data, len(data), bn_ctx); backend.openssl_assert(res == 1) - yield bn_mont_ctx - finally: - backend._lib.BN_MONT_CTX_free(bn_mont_ctx) + except InternalError as e: + # We want to catch specific InternalExceptions. + # https://github.com/openssl/openssl/blob/master/include/openssl/ecerr.h + # There is also EC_R_POINT_IS_NOT_ON_CURVE (code 107), + # but somehow it is never triggered during deserialization. + if e.err_code[0].reason == 110: # EC_R_INVALID_COMPRESSED_POINT + raise ErrorInvalidCompressedPoint + elif e.err_code[0].reason == 102: # EC_R_INVALID_ENCODING + raise ErrorInvalidPointEncoding + else: + # Any other exception, we raise it. + # (although at the moment I'm not sure what should one do to cause it) + raise e # pragma: no cover + return point + + +def point_to_bytes_compressed(curve: Curve, point): + point_conversion_form = backend._lib.POINT_CONVERSION_COMPRESSED + + size = curve.field_element_size + 1 # compressed point size + + bin_ptr = backend._ffi.new("unsigned char[]", size) + with backend._tmp_bn_ctx() as bn_ctx: + bin_len = backend._lib.EC_POINT_point2oct( + curve.ec_group, point, point_conversion_form, + bin_ptr, size, bn_ctx + ) + backend.openssl_assert(bin_len != 0) + + return bytes(backend._ffi.buffer(bin_ptr, bin_len)[:]) + + +def point_eq(curve: Curve, point1, point2): + with backend._tmp_bn_ctx() as bn_ctx: + is_equal = backend._lib.EC_POINT_cmp(curve.ec_group, point1, point2, bn_ctx) + backend.openssl_assert(is_equal != -1) + + # 1 is not-equal, 0 is equal, -1 is error + return is_equal == 0 + + +def point_mul_bn(curve: Curve, point, bn): + prod = _point_new(curve.ec_group) + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_mul(curve.ec_group, prod, backend._ffi.NULL, point, bn, bn_ctx) + backend.openssl_assert(res == 1) + return prod + + +def point_add(curve: Curve, point1, point2): + op_sum = _point_new(curve.ec_group) + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_add(curve.ec_group, op_sum, point1, point2, bn_ctx) + backend.openssl_assert(res == 1) + return op_sum + + +def point_neg(curve: Curve, point): + inv = backend._lib.EC_POINT_dup(point, curve.ec_group) + backend.openssl_assert(inv != backend._ffi.NULL) + inv = backend._ffi.gc(inv, backend._lib.EC_POINT_clear_free) + + with backend._tmp_bn_ctx() as bn_ctx: + res = backend._lib.EC_POINT_invert(curve.ec_group, inv, bn_ctx) + backend.openssl_assert(res == 1) + + return inv + + +def point_to_pubkey(curve: Curve, point): + + ec_key = backend._lib.EC_KEY_new() + backend.openssl_assert(ec_key != backend._ffi.NULL) + ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free) + + set_group_result = backend._lib.EC_KEY_set_group(ec_key, curve.ec_group) + backend.openssl_assert(set_group_result == 1) + + set_pubkey_result = backend._lib.EC_KEY_set_public_key(ec_key, point) + backend.openssl_assert(set_pubkey_result == 1) + + evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key) + return _EllipticCurvePublicKey(backend, ec_key, evp_pkey) From c419705245836e3aba12300366062bc918f0a94a Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 21:34:44 -0700 Subject: [PATCH 11/25] Add SecretKeyFactory --- umbral/__init__.py | 3 ++- umbral/keys.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/umbral/__init__.py b/umbral/__init__.py index 7021af4a..130758f0 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -5,7 +5,7 @@ from .capsule import Capsule from .capsule_frag import CapsuleFrag from .key_frag import KeyFrag, generate_kfrags -from .keys import SecretKey, PublicKey +from .keys import SecretKey, PublicKey, SecretKeyFactory from .pre import encrypt, decrypt_original, decrypt_reencrypted, reencrypt __all__ = [ @@ -19,6 +19,7 @@ "__url__", "SecretKey", "PublicKey", + "SecretKeyFactory", "Capsule", "KeyFrag", "CapsuleFrag", diff --git a/umbral/keys.py b/umbral/keys.py index 9333dcdf..89c36355 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -1,3 +1,4 @@ +import os from typing import TYPE_CHECKING, Tuple from cryptography.exceptions import InvalidSignature @@ -8,7 +9,7 @@ from .curve import CURVE from .curve_scalar import CurveScalar from .curve_point import CurvePoint -from .dem import DEM +from .dem import kdf from .serializable import Serializable if TYPE_CHECKING: # pragma: no cover @@ -136,3 +137,46 @@ def __eq__(self, other): def __hash__(self) -> int: return hash((self.__class__, bytes(self))) + + +class SecretKeyFactory(Serializable): + """ + This class handles keying material for Umbral, by allowing deterministic + derivation of SecretKeys based on labels. + Don't use this key material directly as a key. + """ + + _KEY_SEED_SIZE = 64 + _DERIVED_KEY_SIZE = 64 + + def __init__(self, key_seed: bytes): + self.__key_seed = key_seed + + @classmethod + def random(cls) -> 'SecretKeyFactory': + return cls(os.urandom(cls._KEY_SEED_SIZE)) + + def secret_key_by_label(self, label: bytes) -> SecretKey: + tag = b"KEY_DERIVATION/" + label + key = kdf(self.__key_seed, self._DERIVED_KEY_SIZE, info=tag) + + from .hashing import Hash + digest = Hash(tag) + digest.update(key) + scalar_key = CurveScalar.from_digest(digest) + + return SecretKey(scalar_key) + + @classmethod + def __take__(cls, data: bytes) -> Tuple['SecretKeyFactory', bytes]: + key_seed, data = cls.__take_bytes__(data, cls._KEY_SEED_SIZE) + return cls(key_seed), data + + def __bytes__(self) -> bytes: + return bytes(self.__key_seed) + + def __str__(self): + return f"{self.__class__.__name__}:..." + + def __hash__(self): + raise NotImplementedError("Hashing secret objects is insecure") From 6af41b09d9c049991ad6a589e3db2dde8f48abf7 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Thu, 18 Mar 2021 19:11:21 -0700 Subject: [PATCH 12/25] Remove repeated casting to bytes from hashing calls --- umbral/hashing.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/umbral/hashing.py b/umbral/hashing.py index 303c1ce0..0eba516d 100644 --- a/umbral/hashing.py +++ b/umbral/hashing.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, Type, Iterable +from typing import TYPE_CHECKING, Optional, Type, Iterable, Union from cryptography.hazmat.primitives import hashes @@ -7,7 +7,8 @@ from .curve_scalar import CurveScalar from .curve_point import CurvePoint from .keys import PublicKey, SecretKey, Signature -from .serializable import serialize_bool +from .serializable import Serializable, serialize_bool + if TYPE_CHECKING: # pragma: no cover from .key_frag import KeyFragID @@ -23,8 +24,8 @@ def __init__(self, dst: bytes): len_dst = len(dst).to_bytes(4, byteorder='big') self.update(len_dst + dst) - def update(self, data: bytes) -> None: - self._hash.update(data) + def update(self, data: Union[bytes, Serializable]) -> None: + self._hash.update(bytes(data)) def finalize(self) -> bytes: return self._hash.finalize() @@ -36,17 +37,17 @@ def hash_to_polynomial_arg(precursor: CurvePoint, kfrag_id: 'KeyFragID', ) -> CurveScalar: digest = Hash(b"POLYNOMIAL_ARG") - digest.update(bytes(precursor)) - digest.update(bytes(pubkey)) - digest.update(bytes(dh_point)) - digest.update(bytes(kfrag_id)) + digest.update(precursor) + digest.update(pubkey) + digest.update(dh_point) + digest.update(kfrag_id) return CurveScalar.from_digest(digest) def hash_capsule_points(e: CurvePoint, v: CurvePoint) -> CurveScalar: digest = Hash(b"CAPSULE_POINTS") - digest.update(bytes(e)) - digest.update(bytes(v)) + digest.update(e) + digest.update(v) return CurveScalar.from_digest(digest) @@ -55,9 +56,9 @@ def hash_to_shared_secret(precursor: CurvePoint, dh_point: CurvePoint ) -> CurveScalar: digest = Hash(b"SHARED_SECRET") - digest.update(bytes(precursor)) - digest.update(bytes(pubkey)) - digest.update(bytes(dh_point)) + digest.update(precursor) + digest.update(pubkey) + digest.update(dh_point) return CurveScalar.from_digest(digest) @@ -65,7 +66,7 @@ def hash_to_shared_secret(precursor: CurvePoint, def hash_to_cfrag_verification(points: Iterable[CurvePoint], metadata: Optional[bytes] = None) -> CurveScalar: digest = Hash(b"CFRAG_VERIFICATION") for point in points: - digest.update(bytes(point)) + digest.update(point) if metadata is not None: digest.update(metadata) return CurveScalar.from_digest(digest) @@ -79,19 +80,19 @@ def hash_to_cfrag_signature(kfrag_id: 'KeyFragID', ) -> 'SignatureDigest': digest = SignatureDigest(b"CFRAG_SIGNATURE") - digest.update(bytes(kfrag_id)) - digest.update(bytes(commitment)) - digest.update(bytes(precursor)) + digest.update(kfrag_id) + digest.update(commitment) + digest.update(precursor) if maybe_delegating_pk: digest.update(serialize_bool(True)) - digest.update(bytes(maybe_delegating_pk)) + digest.update(maybe_delegating_pk) else: digest.update(serialize_bool(False)) if maybe_receiving_pk: digest.update(serialize_bool(True)) - digest.update(bytes(maybe_receiving_pk)) + digest.update(maybe_receiving_pk) else: digest.update(serialize_bool(False)) From 9e87006a9b8aedae8b59fd41610ffb68a52f00bb Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 15:53:35 -0700 Subject: [PATCH 13/25] Add back performance tests --- tests/metrics/reencryption_benchmark.py | 102 ++++++++++++++++++++++++ tests/metrics/reencryption_firehose.py | 54 +++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 tests/metrics/reencryption_benchmark.py create mode 100644 tests/metrics/reencryption_firehose.py diff --git a/tests/metrics/reencryption_benchmark.py b/tests/metrics/reencryption_benchmark.py new file mode 100644 index 00000000..3476ad0f --- /dev/null +++ b/tests/metrics/reencryption_benchmark.py @@ -0,0 +1,102 @@ +import os +import time + +import pytest + +import umbral as umbral_py +import umbral_pre as umbral_rs + + +# Faster +# (M, N) # | +FRAG_VALUES = ((1, 1), # | + (2, 3), # | + (5, 8), # | + (6, 10), # | + (10, 30), # | + # (20, 30), # | # FIXME: CircleCi build killed + # (10, 100) # | + # | + ) # | +# Slower + + +def __standard_encryption_api(umbral) -> tuple: + + delegating_sk = umbral.SecretKey.random() + delegating_pk = umbral.PublicKey.from_secret_key(delegating_sk) + + signing_sk = umbral.SecretKey.random() + + receiving_sk = umbral.SecretKey.random() + receiving_pk = umbral.PublicKey.from_secret_key(receiving_sk) + + plain_data = os.urandom(32) + capsule, ciphertext = umbral.encrypt(delegating_pk, plain_data) + + return delegating_sk, receiving_pk, signing_sk, ciphertext, capsule + + +# +# KFrag Generation Benchmarks +# + + +@pytest.mark.benchmark(group="Reencryption Key Generation Performance", + disable_gc=True, + warmup=True, + warmup_iterations=10) +@pytest.mark.parametrize("m, n", FRAG_VALUES) +@pytest.mark.parametrize("umbral", [umbral_py, umbral_rs], ids=["python", "rust"]) +def test_generate_kfrags_performance(benchmark, m: int, n: int, umbral) -> None: + + def __setup(): + delegating_sk, receiving_pk, signing_sk, ciphertext, capsule = __standard_encryption_api(umbral) + return (delegating_sk, receiving_pk, signing_sk, m, n, True, True), {} + + benchmark.pedantic(umbral.generate_kfrags, setup=__setup, rounds=1000) + assert True # ensure function finishes and succeeds. + + +# +# Reencryption Benchmarks +# + +@pytest.mark.benchmark(group="Reencryption Performance", + timer=time.perf_counter, + disable_gc=True, + warmup=True, + warmup_iterations=10) +@pytest.mark.parametrize("m, n", ((6, 10), )) +@pytest.mark.parametrize("umbral", [umbral_py, umbral_rs], ids=["python", "rust"]) +def test_random_frag_reencryption_performance(benchmark, m: int, n: int, umbral) -> None: + + def __setup(): + delegating_sk, receiving_pk, signing_sk, ciphertext, capsule = __standard_encryption_api(umbral) + kfrags = umbral.generate_kfrags(delegating_sk, receiving_pk, signing_sk, m, n, True, True) + one_kfrag, *remaining_kfrags = kfrags + return (capsule, one_kfrag), {} + + benchmark.pedantic(umbral.reencrypt, setup=__setup, rounds=1000) + assert True # ensure function finishes and succeeds. + + +@pytest.mark.benchmark(group="Reencryption Performance", + timer=time.perf_counter, + disable_gc=True, + min_time=0.00005, + max_time=0.005, + min_rounds=7, + warmup=True, + warmup_iterations=10) +@pytest.mark.parametrize("m, n", ((6, 10), )) +@pytest.mark.parametrize("umbral", [umbral_py, umbral_rs], ids=["python", "rust"]) +def test_single_frag_reencryption_performance(benchmark, m: int, n: int, umbral) -> None: + + delegating_sk, receiving_pk, signing_sk, ciphertext, capsule = __standard_encryption_api(umbral) + kfrags = umbral.generate_kfrags(delegating_sk, receiving_pk, signing_sk, m, n, True, True) + one_kfrag, *remaining_kfrags = kfrags + args, kwargs = (capsule, one_kfrag), {} + + benchmark.pedantic(umbral.reencrypt, args=args, kwargs=kwargs, iterations=20, rounds=100) + assert True # ensure function finishes and succeeds. diff --git a/tests/metrics/reencryption_firehose.py b/tests/metrics/reencryption_firehose.py new file mode 100644 index 00000000..defe6be6 --- /dev/null +++ b/tests/metrics/reencryption_firehose.py @@ -0,0 +1,54 @@ +import os +import sys + +sys.path.append(os.path.abspath(os.getcwd())) + +from typing import Tuple, List + +import umbral + + +REENCRYPTIONS = 1000 + + +def __produce_kfrags_and_capsule(m: int, n: int) -> Tuple[List[umbral.KeyFrag], umbral.Capsule]: + + delegating_sk = umbral.SecretKey.random() + delegating_pk = umbral.PublicKey.from_secret_key(delegating_sk) + + signing_sk = umbral.SecretKey.random() + + receiving_sk = umbral.SecretKey.random() + receiving_pk = umbral.PublicKey.from_secret_key(receiving_sk) + + plain_data = os.urandom(32) + capsule, ciphertext = umbral.encrypt(delegating_pk, plain_data) + + kfrags = umbral.generate_kfrags(delegating_sk, receiving_pk, signing_sk, m, n) + + return kfrags, capsule + + +def firehose(m: int=6, n: int=10) -> None: + + print("Making kfrags...") + kfrags, capsule = __produce_kfrags_and_capsule(m=m, n=n) + one_kfrag, *remaining_kfrags = kfrags + + print('Re-encrypting...') + successful_reencryptions = 0 + for iteration in range(int(REENCRYPTIONS)): + + _cfrag = umbral.reencrypt(capsule, one_kfrag) # <<< REENCRYPTION HAPPENS HERE + + successful_reencryptions += 1 + if iteration % 20 == 0: + print('Performed {} Re-encryptions...'.format(iteration)) + + failure_message = "A Reencryption failed. {} of {} succeeded".format(successful_reencryptions, REENCRYPTIONS) + assert successful_reencryptions == REENCRYPTIONS, failure_message + print("Successfully performed {} reencryptions".format(successful_reencryptions), end='\n') + + +if __name__ == "__main__": + firehose() # do From d65969761c2f7b349179f123cf6199cfcd247416 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 22:35:39 -0700 Subject: [PATCH 14/25] Skip rust-umbral tests if the library is not available Will help CI for the time being --- tests/metrics/reencryption_benchmark.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/metrics/reencryption_benchmark.py b/tests/metrics/reencryption_benchmark.py index 3476ad0f..ee1ca91c 100644 --- a/tests/metrics/reencryption_benchmark.py +++ b/tests/metrics/reencryption_benchmark.py @@ -4,7 +4,21 @@ import pytest import umbral as umbral_py -import umbral_pre as umbral_rs + +try: + import umbral_pre as umbral_rs +except ImportError: + umbral_rs = None + + +def pytest_generate_tests(metafunc): + if 'umbral' in metafunc.fixturenames: + implementations = [umbral_py] + ids = ['python'] + if umbral_rs is not None: + implementations.append(umbral_rs) + ids.append('rust') + metafunc.parametrize('umbral', implementations, ids=ids) # Faster @@ -47,7 +61,6 @@ def __standard_encryption_api(umbral) -> tuple: warmup=True, warmup_iterations=10) @pytest.mark.parametrize("m, n", FRAG_VALUES) -@pytest.mark.parametrize("umbral", [umbral_py, umbral_rs], ids=["python", "rust"]) def test_generate_kfrags_performance(benchmark, m: int, n: int, umbral) -> None: def __setup(): @@ -68,7 +81,6 @@ def __setup(): warmup=True, warmup_iterations=10) @pytest.mark.parametrize("m, n", ((6, 10), )) -@pytest.mark.parametrize("umbral", [umbral_py, umbral_rs], ids=["python", "rust"]) def test_random_frag_reencryption_performance(benchmark, m: int, n: int, umbral) -> None: def __setup(): @@ -90,7 +102,6 @@ def __setup(): warmup=True, warmup_iterations=10) @pytest.mark.parametrize("m, n", ((6, 10), )) -@pytest.mark.parametrize("umbral", [umbral_py, umbral_rs], ids=["python", "rust"]) def test_single_frag_reencryption_performance(benchmark, m: int, n: int, umbral) -> None: delegating_sk, receiving_pk, signing_sk, ciphertext, capsule = __standard_encryption_api(umbral) From f58a2580dcd5229bd933cdf91d328146a11053a9 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 20:18:28 -0700 Subject: [PATCH 15/25] curve_scalar: don't check range in __init__, only in publicly used constructors --- umbral/curve_scalar.py | 12 +++++------- umbral/keys.py | 12 ++++++++---- umbral/openssl.py | 16 ++++++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/umbral/curve_scalar.py b/umbral/curve_scalar.py index da5f2859..6fa9925d 100644 --- a/umbral/curve_scalar.py +++ b/umbral/curve_scalar.py @@ -17,9 +17,6 @@ class CurveScalar(Serializable): """ def __init__(self, backend_bignum): - if not openssl.bn_is_normalized(backend_bignum, CURVE.bn_order): - raise ValueError("The provided BIGNUM is not on the provided curve.") - self._backend_bignum = backend_bignum @classmethod @@ -30,11 +27,12 @@ def random_nonzero(cls) -> 'CurveScalar': return cls(openssl.bn_random_nonzero(CURVE.bn_order)) @classmethod - def from_int(cls, num: int) -> 'CurveScalar': + def from_int(cls, num: int, check_normalization: bool = True) -> 'CurveScalar': """ Returns a CurveScalar object from a given integer on a curve. """ - conv_bn = openssl.bn_from_int(num, modulus=CURVE.bn_order) + modulus = CURVE.bn_order if check_normalization else None + conv_bn = openssl.bn_from_int(num, check_modulus=modulus) return cls(conv_bn) @classmethod @@ -43,13 +41,13 @@ def from_digest(cls, digest: 'Hash') -> 'CurveScalar': # Currently just matching what we have in RustCrypto stack # (taking bytes modulo curve order). # Can produce zeros! - bn = openssl.bn_from_bytes(digest.finalize(), modulus=CURVE.bn_order) + bn = openssl.bn_from_bytes(digest.finalize(), apply_modulus=CURVE.bn_order) return cls(bn) @classmethod def __take__(cls, data: bytes) -> Tuple['CurveScalar', bytes]: scalar_data, data = cls.__take_bytes__(data, CURVE.scalar_size) - bignum = openssl.bn_from_bytes(scalar_data) + bignum = openssl.bn_from_bytes(scalar_data, check_modulus=CURVE.bn_order) return cls(bignum), data def __bytes__(self) -> bytes: diff --git a/umbral/keys.py b/umbral/keys.py index 89c36355..0badfa1f 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -57,14 +57,18 @@ def sign_digest(self, digest: 'Hash') -> 'Signature': backend_sk = openssl.bn_to_privkey(CURVE, self._scalar_key._backend_bignum) signature_der_bytes = backend_sk.sign(message, signature_algorithm) - r, s = utils.decode_dss_signature(signature_der_bytes) + r_int, s_int = utils.decode_dss_signature(signature_der_bytes) # Normalize s # s is public, so no constant-timeness required here - if s > (CURVE.order >> 1): - s = CURVE.order - s + if s_int > (CURVE.order >> 1): + s_int = CURVE.order - s_int - return Signature(CurveScalar.from_int(r), CurveScalar.from_int(s)) + # Already normalized, don't waste time + r = CurveScalar.from_int(r_int, check_normalization=False) + s = CurveScalar.from_int(s_int, check_normalization=False) + + return Signature(r, s) class Signature(Serializable): diff --git a/umbral/openssl.py b/umbral/openssl.py index 5801afb4..cede9b52 100644 --- a/umbral/openssl.py +++ b/umbral/openssl.py @@ -144,7 +144,7 @@ def bn_is_normalized(check_bn, modulus): return (check_sign == 1 or check_sign == 0) and range_check == -1 -def bn_from_int(py_int: int, modulus=None, set_consttime_flag=True): +def bn_from_int(py_int: int, check_modulus=None, set_consttime_flag=True): """ Converts the given Python int to an OpenSSL BIGNUM. If ``modulus`` is provided, it will check if the Python integer is within ``[0, modulus)``. @@ -155,15 +155,15 @@ def bn_from_int(py_int: int, modulus=None, set_consttime_flag=True): conv_bn = backend._int_to_bn(py_int) conv_bn = backend._ffi.gc(conv_bn, backend._lib.BN_clear_free) - if modulus and not bn_is_normalized(conv_bn, modulus): - raise ValueError("The Python integer given is not under the provided modulus.") + if check_modulus and not bn_is_normalized(conv_bn, check_modulus): + raise ValueError(f"The Python integer given ({py_int}) is not under the provided modulus.") if set_consttime_flag: backend._lib.BN_set_flags(conv_bn, backend._lib.BN_FLG_CONSTTIME) return conv_bn -def bn_from_bytes(bytes_seq: bytes, set_consttime_flag=True, modulus=None): +def bn_from_bytes(bytes_seq: bytes, set_consttime_flag=True, check_modulus=None, apply_modulus=None): """ Converts the given byte sequence to an OpenSSL BIGNUM. If set_consttime_flag is set to True, OpenSSL will use constant time @@ -173,10 +173,14 @@ def bn_from_bytes(bytes_seq: bytes, set_consttime_flag=True, modulus=None): backend._lib.BN_bin2bn(bytes_seq, len(bytes_seq), bn) backend.openssl_assert(bn != backend._ffi.NULL) - if modulus: + if check_modulus and not bn_is_normalized(bn, check_modulus): + raise ValueError(f"The integer encoded with given bytes ({repr(bytes_seq)}) " + "is not under the provided modulus.") + + if apply_modulus: bignum =_bn_new() with backend._tmp_bn_ctx() as bn_ctx: - res = backend._lib.BN_mod(bignum, bn, modulus, bn_ctx) + res = backend._lib.BN_mod(bignum, bn, apply_modulus, bn_ctx) backend.openssl_assert(res == 1) return bn From c401c52e92b01088407b1cd39728ec3291d68e11 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 19 Mar 2021 20:21:01 -0700 Subject: [PATCH 16/25] Add tests --- tests/conftest.py | 51 +++++++++ tests/test_capsule.py | 107 +++++++++++++++++++ tests/test_capsule_frag.py | 149 ++++++++++++++++++++++++++ tests/test_compatibility.py | 205 ++++++++++++++++++++++++++++++++++++ tests/test_curve.py | 113 ++++++++++++++++++++ tests/test_curve_point.py | 90 ++++++++++++++++ tests/test_curve_scalar.py | 127 ++++++++++++++++++++++ tests/test_dem.py | 80 ++++++++++++++ tests/test_key_frag.py | 126 ++++++++++++++++++++++ tests/test_keys.py | 203 +++++++++++++++++++++++++++++++++++ tests/test_pre.py | 95 +++++++++++++++++ tests/test_serializable.py | 92 ++++++++++++++++ umbral/dem.py | 17 ++- 13 files changed, 1453 insertions(+), 2 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_capsule.py create mode 100644 tests/test_capsule_frag.py create mode 100644 tests/test_compatibility.py create mode 100644 tests/test_curve.py create mode 100644 tests/test_curve_point.py create mode 100644 tests/test_curve_scalar.py create mode 100644 tests/test_dem.py create mode 100644 tests/test_key_frag.py create mode 100644 tests/test_keys.py create mode 100644 tests/test_pre.py create mode 100644 tests/test_serializable.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..954b727e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +import pytest + +from umbral import SecretKey, PublicKey, generate_kfrags, encrypt + + +@pytest.fixture +def alices_keys(): + delegating_sk = SecretKey.random() + signing_sk = SecretKey.random() + return delegating_sk, signing_sk + + +@pytest.fixture +def bobs_keys(): + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + return sk, pk + + +@pytest.fixture +def kfrags(alices_keys, bobs_keys): + delegating_sk, signing_sk = alices_keys + receiving_sk, receiving_pk = bobs_keys + yield generate_kfrags(delegating_sk=delegating_sk, + signing_sk=signing_sk, + receiving_pk=receiving_pk, + threshold=6, num_kfrags=10) + + +@pytest.fixture(scope='session') +def message(): + message = (b"dnunez [9:30 AM]" + b"@Tux we had this super fruitful discussion last night with @jMyles @michwill @KPrasch" + b"to sum up: the symmetric ciphertext is now called the 'Chimney'." + b"the chimney of the capsule, of course" + b"tux [9:32 AM]" + b"wat") + return message + + +@pytest.fixture +def capsule_and_ciphertext(alices_keys, message): + delegating_sk, _signing_sk = alices_keys + capsule, ciphertext = encrypt(PublicKey.from_secret_key(delegating_sk), message) + return capsule, ciphertext + + +@pytest.fixture +def capsule(capsule_and_ciphertext): + capsule, ciphertext = capsule_and_ciphertext + return capsule diff --git a/tests/test_capsule.py b/tests/test_capsule.py new file mode 100644 index 00000000..1896ca26 --- /dev/null +++ b/tests/test_capsule.py @@ -0,0 +1,107 @@ +import pytest + +from umbral import ( + Capsule, + SecretKey, + PublicKey, + encrypt, + decrypt_original, + reencrypt, + decrypt_reencrypted, + generate_kfrags + ) +from umbral.curve_point import CurvePoint + + +def test_capsule_serialization(alices_keys): + + delegating_sk, _signing_sk = alices_keys + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + capsule, _key = Capsule.from_public_key(delegating_pk) + new_capsule = Capsule.from_bytes(bytes(capsule)) + + assert capsule == new_capsule + + # Deserializing a bad capsule triggers verification error + capsule.point_e = CurvePoint.random() + capsule_bytes = bytes(capsule) + + with pytest.raises(Capsule.NotValid): + Capsule.from_bytes(capsule_bytes) + + +def test_capsule_is_hashable(alices_keys): + + delegating_sk, _signing_sk = alices_keys + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + capsule1, key1 = Capsule.from_public_key(delegating_pk) + capsule2, key2 = Capsule.from_public_key(delegating_pk) + + assert capsule1 != capsule2 + assert key1 != key2 + assert hash(capsule1) != hash(capsule2) + + new_capsule = Capsule.from_bytes(bytes(capsule1)) + assert hash(new_capsule) == hash(capsule1) + + +def test_open_original(alices_keys): + + delegating_sk, _signing_sk = alices_keys + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + capsule, key = Capsule.from_public_key(delegating_pk) + key_back = capsule.open_original(delegating_sk) + assert key == key_back + + +def test_open_reencrypted(alices_keys, bobs_keys): + + threshold = 6 + num_kfrags = 10 + + delegating_sk, signing_sk = alices_keys + receiving_sk, receiving_pk = bobs_keys + + signing_pk = PublicKey.from_secret_key(signing_sk) + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + capsule, key = Capsule.from_public_key(delegating_pk) + kfrags = generate_kfrags(delegating_sk=delegating_sk, + signing_sk=signing_sk, + receiving_pk=receiving_pk, + threshold=threshold, + num_kfrags=num_kfrags) + + cfrags = [reencrypt(capsule, kfrag) for kfrag in kfrags] + key_back = capsule.open_reencrypted(receiving_sk, delegating_pk, cfrags[:threshold]) + assert key_back == key + + # No cfrags at all + with pytest.raises(ValueError, match="Empty CapsuleFrag sequence"): + capsule.open_reencrypted(receiving_sk, delegating_pk, []) + + # Not enough cfrags + with pytest.raises(ValueError, match="Internal validation failed"): + capsule.open_reencrypted(receiving_sk, delegating_pk, cfrags[:threshold-1]) + + # Repeating cfrags + with pytest.raises(ValueError, match="Some of the CapsuleFrags are repeated"): + capsule.open_reencrypted(receiving_sk, delegating_pk, [cfrags[0]] + cfrags[:threshold-1]) + + # Mismatched cfrags + kfrags2 = generate_kfrags(delegating_sk=delegating_sk, + signing_sk=signing_sk, + receiving_pk=receiving_pk, + threshold=threshold, + num_kfrags=num_kfrags) + cfrags2 = [reencrypt(capsule, kfrag) for kfrag in kfrags2] + with pytest.raises(ValueError, match="CapsuleFrags are not pairwise consistent"): + capsule.open_reencrypted(receiving_sk, delegating_pk, [cfrags2[0]] + cfrags[:threshold-1]) + + +def test_capsule_str(capsule): + s = str(capsule) + assert 'Capsule' in s diff --git a/tests/test_capsule_frag.py b/tests/test_capsule_frag.py new file mode 100644 index 00000000..ff81e724 --- /dev/null +++ b/tests/test_capsule_frag.py @@ -0,0 +1,149 @@ +from umbral import reencrypt, CapsuleFrag, PublicKey, Capsule +from umbral.curve_point import CurvePoint + + +def test_cfrag_serialization(alices_keys, bobs_keys, capsule, kfrags): + + delegating_sk, signing_sk = alices_keys + _receiving_sk, receiving_pk = bobs_keys + + signing_pk = PublicKey.from_secret_key(signing_sk) + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + metadata = b'This is an example of metadata for re-encryption request' + for kfrag in kfrags: + cfrag = reencrypt(capsule, kfrag, metadata=metadata) + cfrag_bytes = bytes(cfrag) + + new_cfrag = CapsuleFrag.from_bytes(cfrag_bytes) + assert new_cfrag == cfrag + + assert new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=signing_pk, + metadata=metadata) + + # No metadata + assert not new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=signing_pk) + + # Wrong metadata + assert not new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=signing_pk, + metadata=b'Not the same metadata') + + # Wrong delegating key + assert not new_cfrag.verify(capsule, + delegating_pk=receiving_pk, + receiving_pk=receiving_pk, + signing_pk=signing_pk, + metadata=metadata) + + # Wrong receiving key + assert not new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=delegating_pk, + signing_pk=signing_pk, + metadata=metadata) + + # Wrong signing key + assert not new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=receiving_pk, + metadata=metadata) + + +def test_cfrag_serialization_no_metadata(alices_keys, bobs_keys, capsule, kfrags): + + delegating_sk, signing_sk = alices_keys + _receiving_sk, receiving_pk = bobs_keys + + signing_pk = PublicKey.from_secret_key(signing_sk) + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + for kfrag in kfrags: + + # Create with no metadata + cfrag = reencrypt(capsule, kfrag) + cfrag_bytes = bytes(cfrag) + new_cfrag = CapsuleFrag.from_bytes(cfrag_bytes) + + assert new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=signing_pk) + + assert not new_cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=signing_pk, + metadata=b'some metadata') + + +def test_cfrag_with_wrong_capsule(alices_keys, bobs_keys, + kfrags, capsule_and_ciphertext, message): + + capsule, ciphertext = capsule_and_ciphertext + + delegating_sk, signing_sk = alices_keys + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + _receiving_sk, receiving_pk = bobs_keys + + capsule_alice1 = capsule + capsule_alice2, _unused_key2 = Capsule.from_public_key(delegating_pk) + + metadata = b"some metadata" + cfrag = reencrypt(capsule_alice2, kfrags[0], metadata=metadata) + + assert not cfrag.verify(capsule_alice1, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=PublicKey.from_secret_key(signing_sk), + metadata=metadata) + + +def test_cfrag_with_wrong_data(kfrags, alices_keys, bobs_keys, capsule_and_ciphertext, message): + + capsule, ciphertext = capsule_and_ciphertext + + delegating_sk, signing_sk = alices_keys + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + _receiving_sk, receiving_pk = bobs_keys + + metadata = b"some metadata" + cfrag = reencrypt(capsule, kfrags[0], metadata=metadata) + + # Let's put random garbage in one of the cfrags + cfrag.point_e1 = CurvePoint.random() + cfrag.point_v1 = CurvePoint.random() + + assert not cfrag.verify(capsule, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + signing_pk=PublicKey.from_secret_key(signing_sk), + metadata=metadata) + + +def test_cfrag_is_hashable(capsule, kfrags): + + cfrag0 = reencrypt(capsule, kfrags[0], metadata=b'abcdef') + cfrag1 = reencrypt(capsule, kfrags[1], metadata=b'abcdef') + + assert hash(cfrag0) != hash(cfrag1) + + new_cfrag = CapsuleFrag.from_bytes(bytes(cfrag0)) + assert hash(new_cfrag) == hash(cfrag0) + + +def test_cfrag_str(capsule, kfrags): + cfrag0 = reencrypt(capsule, kfrags[0], metadata=b'abcdef') + s = str(cfrag0) + assert 'CapsuleFrag' in s diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py new file mode 100644 index 00000000..bba1bee4 --- /dev/null +++ b/tests/test_compatibility.py @@ -0,0 +1,205 @@ +import pytest + +try: + import umbral_pre as umbral_rs +except ImportError: + umbral_rs = None + +import umbral as umbral_py + + +def pytest_generate_tests(metafunc): + if 'implementations' in metafunc.fixturenames: + implementations = [(umbral_py, umbral_py)] + ids = ['python -> python'] + if umbral_rs is not None: + implementations.extend([(umbral_py, umbral_rs), (umbral_rs, umbral_py)]) + ids.extend(['python -> rust', 'rust -> python']) + + metafunc.parametrize('implementations', implementations, ids=ids) + + +def _create_keypair(umbral): + sk = umbral.SecretKey.random() + pk = umbral.PublicKey.from_secret_key(sk) + return bytes(sk), bytes(pk) + + +def _restore_keys(umbral, sk_bytes, pk_bytes): + sk = umbral.SecretKey.from_bytes(sk_bytes) + pk_from_sk = umbral.PublicKey.from_secret_key(sk) + pk_from_bytes = umbral.PublicKey.from_bytes(pk_bytes) + assert pk_from_sk == pk_from_bytes + + +def test_keys(implementations): + umbral1, umbral2 = implementations + + # On client 1 + sk_bytes, pk_bytes = _create_keypair(umbral1) + + # On client 2 + _restore_keys(umbral2, sk_bytes, pk_bytes) + + +def _create_sk_factory_and_sk(umbral, label): + skf = umbral.SecretKeyFactory.random() + sk = skf.secret_key_by_label(label) + return bytes(skf), bytes(sk) + + +def _check_sk_is_same(umbral, label, skf_bytes, sk_bytes): + skf = umbral.SecretKeyFactory.from_bytes(skf_bytes) + sk_restored = umbral.SecretKey.from_bytes(sk_bytes) + sk_generated = skf.secret_key_by_label(label) + assert sk_restored == sk_generated + + +def test_secret_key_factory(implementations): + umbral1, umbral2 = implementations + label = b'label' + + skf_bytes, sk_bytes = _create_sk_factory_and_sk(umbral1, label) + _check_sk_is_same(umbral2, label, skf_bytes, sk_bytes) + + +def _encrypt(umbral, plaintext, pk_bytes): + pk = umbral.PublicKey.from_bytes(pk_bytes) + capsule, ciphertext = umbral.encrypt(pk, plaintext) + return bytes(capsule), ciphertext + + +def _decrypt_original(umbral, sk_bytes, capsule_bytes, ciphertext): + capsule = umbral.Capsule.from_bytes(bytes(capsule_bytes)) + sk = umbral.SecretKey.from_bytes(sk_bytes) + return umbral.decrypt_original(sk, capsule, ciphertext) + + +def test_encrypt_decrypt(implementations): + + umbral1, umbral2 = implementations + plaintext = b'peace at dawn' + + # On client 1 + sk_bytes, pk_bytes = _create_keypair(umbral1) + + # On client 2 + capsule_bytes, ciphertext = _encrypt(umbral2, plaintext, pk_bytes) + + # On client 1 + plaintext_decrypted = _decrypt_original(umbral1, sk_bytes, capsule_bytes, ciphertext) + + assert plaintext_decrypted == plaintext + + +def _generate_kfrags(umbral, delegating_sk_bytes, receiving_pk_bytes, + signing_sk_bytes, threshold, num_frags): + + delegating_sk = umbral.SecretKey.from_bytes(delegating_sk_bytes) + receiving_pk = umbral.PublicKey.from_bytes(receiving_pk_bytes) + signing_sk = umbral.SecretKey.from_bytes(signing_sk_bytes) + + kfrags = umbral.generate_kfrags(delegating_sk, + receiving_pk, + signing_sk, + threshold, + num_frags, + True, + True, + ) + + return [bytes(kfrag) for kfrag in kfrags] + + +def _verify_kfrags(umbral, kfrags_bytes, signing_pk_bytes, delegating_pk_bytes, receiving_pk_bytes): + kfrags = [umbral.KeyFrag.from_bytes(kfrag_bytes) for kfrag_bytes in kfrags_bytes] + signing_pk = umbral.PublicKey.from_bytes(signing_pk_bytes) + delegating_pk = umbral.PublicKey.from_bytes(delegating_pk_bytes) + receiving_pk = umbral.PublicKey.from_bytes(receiving_pk_bytes) + assert all(kfrag.verify(signing_pk, delegating_pk, receiving_pk) for kfrag in kfrags) + + +def test_kfrags(implementations): + + umbral1, umbral2 = implementations + + threshold = 2 + num_frags = 3 + plaintext = b'peace at dawn' + + # On client 1 + + receiving_sk_bytes, receiving_pk_bytes = _create_keypair(umbral1) + delegating_sk_bytes, delegating_pk_bytes = _create_keypair(umbral1) + signing_sk_bytes, signing_pk_bytes = _create_keypair(umbral1) + kfrags_bytes = _generate_kfrags(umbral1, delegating_sk_bytes, receiving_pk_bytes, + signing_sk_bytes, threshold, num_frags) + + # On client 2 + + _verify_kfrags(umbral2, kfrags_bytes, signing_pk_bytes, delegating_pk_bytes, receiving_pk_bytes) + + +def _reencrypt(umbral, capsule_bytes, kfrags_bytes, threshold, metadata): + capsule = umbral.Capsule.from_bytes(bytes(capsule_bytes)) + kfrags = [umbral.KeyFrag.from_bytes(kfrag_bytes) for kfrag_bytes in kfrags_bytes] + cfrags = [umbral.reencrypt(capsule, kfrag, metadata=metadata) for kfrag in kfrags[:threshold]] + return [bytes(cfrag) for cfrag in cfrags] + + +def _decrypt_reencrypted(umbral, receiving_sk_bytes, delegating_pk_bytes, signing_pk_bytes, + capsule_bytes, cfrags_bytes, ciphertext, metadata): + + receiving_sk = umbral.SecretKey.from_bytes(receiving_sk_bytes) + receiving_pk = umbral.PublicKey.from_secret_key(receiving_sk) + delegating_pk = umbral.PublicKey.from_bytes(delegating_pk_bytes) + signing_pk = umbral.PublicKey.from_bytes(signing_pk_bytes) + + capsule = umbral.Capsule.from_bytes(bytes(capsule_bytes)) + cfrags = [umbral.CapsuleFrag.from_bytes(cfrag_bytes) for cfrag_bytes in cfrags_bytes] + + assert all(cfrag.verify(capsule, delegating_pk, receiving_pk, signing_pk, metadata=metadata) + for cfrag in cfrags) + + # Decryption by Bob + plaintext = umbral.decrypt_reencrypted(receiving_sk, + delegating_pk, + capsule, + cfrags, + ciphertext, + ) + + return plaintext + + +def test_reencrypt(implementations): + + umbral1, umbral2 = implementations + + metadata = b'metadata' + threshold = 2 + num_frags = 3 + plaintext = b'peace at dawn' + + # On client 1 + + receiving_sk_bytes, receiving_pk_bytes = _create_keypair(umbral1) + delegating_sk_bytes, delegating_pk_bytes = _create_keypair(umbral1) + signing_sk_bytes, signing_pk_bytes = _create_keypair(umbral1) + + capsule_bytes, ciphertext = _encrypt(umbral1, plaintext, delegating_pk_bytes) + + kfrags_bytes = _generate_kfrags(umbral1, delegating_sk_bytes, receiving_pk_bytes, + signing_sk_bytes, threshold, num_frags) + + # On client 2 + + cfrags_bytes = _reencrypt(umbral2, capsule_bytes, kfrags_bytes, threshold, metadata) + + # On client 1 + + plaintext_reencrypted = _decrypt_reencrypted(umbral1, + receiving_sk_bytes, delegating_pk_bytes, signing_pk_bytes, + capsule_bytes, cfrags_bytes, ciphertext, metadata) + + assert plaintext_reencrypted == plaintext diff --git a/tests/test_curve.py b/tests/test_curve.py new file mode 100644 index 00000000..a8c220b0 --- /dev/null +++ b/tests/test_curve.py @@ -0,0 +1,113 @@ +import pytest + +from umbral.openssl import Curve, bn_to_int, point_to_affine_coords +from umbral.curve import CURVE, CURVES, SECP256R1, SECP256K1, SECP384R1 + + +def test_supported_curves(): + + # Ensure we have the correct number of supported curves hardcoded + number_of_supported_curves = 3 + assert len(Curve._supported_curves) == number_of_supported_curves + + # Manually ensure the `_supported curves` dict contains only valid supported curves + assert Curve._supported_curves[415] == 'secp256r1' + assert Curve._supported_curves[714] == 'secp256k1' + assert Curve._supported_curves[715] == 'secp384r1' + + +def test_create_by_nid(): + + nid, name = 714, 'secp256k1' + + # supported + _curve_714 = Curve(nid=nid) + assert _curve_714.nid == nid + assert _curve_714.name == name + + # unsuported + with pytest.raises(NotImplementedError): + Curve(711) + + +def test_create_by_name(): + + nid, name = 714, 'secp256k1' + + # Supported + _curve_secp256k1 = Curve.from_name(name) + assert _curve_secp256k1.name == name + assert _curve_secp256k1.nid == nid + + # Unsupported + with pytest.raises(NotImplementedError): + Curve.from_name('abcd123e4') + + +def test_curve_constants(): + + test_p256 = SECP256R1 + test_secp256k1 = SECP256K1 + test_p384 = SECP384R1 + + assert CURVE == SECP256K1 + + # Test the hardcoded curve NIDs are correct: + assert test_p256.nid == 415 + assert test_secp256k1.nid == 714 + assert test_p384.nid == 715 + + # Ensure every curve constant is in the CURVES collection + number_of_supported_curves = 3 + assert len(CURVES) == number_of_supported_curves + + # Ensure all supported curves can be initialized + for nid, name in Curve._supported_curves.items(): + by_nid, by_name = Curve(nid=nid), Curve.from_name(name) + assert by_nid.name == name + assert by_name.nid == nid + + +def test_curve_str(): + for curve in CURVES: + s = str(curve) + assert str(curve.nid) in s + assert str(curve.name) in s + + +def _curve_info(curve: Curve): + assert bn_to_int(curve.bn_order) == curve.order + return dict(order=curve.order, + field_element_size=curve.field_element_size, + scalar_size=curve.scalar_size, + generator=point_to_affine_coords(curve, curve.point_generator)) + + +def test_secp256k1(): + info = _curve_info(SECP256K1) + assert info['order'] == 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141 + assert info['field_element_size'] == 32 + assert info['scalar_size'] == 32 + assert info['generator'] == ( + 0x79BE667E_F9DCBBAC_55A06295_CE870B07_029BFCDB_2DCE28D9_59F2815B_16F81798, + 0x483ADA77_26A3C465_5DA4FBFC_0E1108A8_FD17B448_A6855419_9C47D08F_FB10D4B8) + + +def test_p256(): + info = _curve_info(SECP256R1) + assert info['order'] == 0xFFFFFFFF_00000000_FFFFFFFF_FFFFFFFF_BCE6FAAD_A7179E84_F3B9CAC2_FC632551 + assert info['field_element_size'] == 32 + assert info['scalar_size'] == 32 + assert info['generator'] == ( + 0x6B17D1F2_E12C4247_F8BCE6E5_63A440F2_77037D81_2DEB33A0_F4A13945_D898C296, + 0x4FE342E2_FE1A7F9B_8EE7EB4A_7C0F9E16_2BCE3357_6B315ECE_CBB64068_37BF51F5) + + +def test_p384(): + info = _curve_info(SECP384R1) + assert info['order'] == 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_C7634D81_F4372DDF_581A0DB2_48B0A77A_ECEC196A_CCC52973 + assert info['field_element_size'] == 48 + assert info['scalar_size'] == 48 + assert info['generator'] == ( + 0xAA87CA22_BE8B0537_8EB1C71E_F320AD74_6E1D3B62_8BA79B98_59F741E0_82542A38_5502F25D_BF55296C_3A545E38_72760AB7, + 0x3617DE4A_96262C6F_5D9E98BF_9292DC29_F8F41DBD_289A147C_E9DA3113_B5F0B8C0_0A60B1CE_1D7E819D_7A431D7C_90EA0E5F) diff --git a/tests/test_curve_point.py b/tests/test_curve_point.py new file mode 100644 index 00000000..38d3866f --- /dev/null +++ b/tests/test_curve_point.py @@ -0,0 +1,90 @@ +import pytest + +from umbral.openssl import ErrorInvalidCompressedPoint, ErrorInvalidPointEncoding +from umbral.curve_point import CurvePoint +from umbral.curve import CURVE + + +def test_random(): + p1 = CurvePoint.random() + p2 = CurvePoint.random() + assert isinstance(p1, CurvePoint) + assert isinstance(p2, CurvePoint) + assert p1 != p2 + + +def test_generator_point(): + """http://www.secg.org/SEC2-Ver-1.0.pdf Section 2.7.1""" + g1 = CurvePoint.generator() + + g_compressed = 0x0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 + g_compressed_bytes = g_compressed.to_bytes(CURVE.field_element_size + 1, byteorder='big') + g2 = CurvePoint.from_bytes(g_compressed_bytes) + + assert g1 == g2 + + +def test_to_and_from_affine(): + + x = 17004608369308732328368332205668001941491834793934321461466076545247324070015 + y = 69725941631324401609944843130171147910924748427773762412028916504484868631573 + + p = CurvePoint.from_affine(x, y) + + assert p.to_affine() == (x, y) + + +def test_invalid_serialized_points(): + + field_order = 2**256 - 0x1000003D1 + + # A point on secp256k1 + x = 17004608369308732328368332205668001941491834793934321461466076545247324070015 + y = 69725941631324401609944843130171147910924748427773762412028916504484868631573 + + # Check it + assert (y**2 - x**3 - 7) % field_order == 0 + + # Should load + point_data = b'\x03' + x.to_bytes(CURVE.field_element_size, 'big') + p = CurvePoint.from_bytes(point_data) + + # Make it invalid + bad_x = x - 1 + assert (y**2 - bad_x**3 - 7) % field_order != 0 + + bad_x_data = b'\x03' + bad_x.to_bytes(CURVE.field_element_size, 'big') + with pytest.raises(ErrorInvalidCompressedPoint): + CurvePoint.from_bytes(bad_x_data) + + # Valid x, invalid prefix + bad_format = b'\xff' + x.to_bytes(CURVE.field_element_size, 'big') + with pytest.raises(ErrorInvalidPointEncoding): + CurvePoint.from_bytes(bad_format) + + +def test_serialize_point_at_infinity(): + + p = CurvePoint.random() + point_at_infinity = p - p + + bytes_point_at_infinity = bytes(point_at_infinity) + assert bytes_point_at_infinity == b'\x00' + + +def test_coords_with_special_characteristics(): + + # Testing that a point with x coordinate greater than the curve order is still valid. + # In particular, we will test the last valid point from the default curve (secp256k1) + # whose x coordinate is `field_order - 3` and is greater than the order of the curve + + field_order = 2**256 - 0x1000003D1 + compressed = b'\x02' + (field_order-3).to_bytes(32, 'big') + + last_point = CurvePoint.from_bytes(compressed) + + # The same point, but obtained through the from_affine method + x = 115792089237316195423570985008687907853269984665640564039457584007908834671660 + y = 109188863561374057667848968960504138135859662956057034999983532397866404169138 + + assert last_point == CurvePoint.from_affine(x, y) diff --git a/tests/test_curve_scalar.py b/tests/test_curve_scalar.py new file mode 100644 index 00000000..a9f0fd80 --- /dev/null +++ b/tests/test_curve_scalar.py @@ -0,0 +1,127 @@ +import pytest + +from umbral.curve import CURVE +from umbral.curve_scalar import CurveScalar +from umbral.hashing import Hash + + +def test_random(): + r1 = CurveScalar.random_nonzero() + r2 = CurveScalar.random_nonzero() + assert r1 != r2 + assert not r1.is_zero() + assert not r2.is_zero() + + +def test_from_and_to_int(): + zero = CurveScalar.from_int(0) + assert zero.is_zero() + assert int(zero) == 0 + + one = CurveScalar.one() + assert not one.is_zero() + assert int(one) == 1 + + big_int = CURVE.order - 2 + big_scalar = CurveScalar.from_int(big_int) + assert int(big_scalar) == big_int + + # normalization check + with pytest.raises(ValueError): + CurveScalar.from_int(CURVE.order) + + # disable normalization check + too_big = CurveScalar.from_int(CURVE.order, check_normalization=False) + + +def test_from_digest(): + digest = Hash(b'asdf') + digest.update(b'some info') + s1 = CurveScalar.from_digest(digest) + + digest = Hash(b'asdf') + digest.update(b'some info') + s2 = CurveScalar.from_digest(digest) + + assert s1 == s2 + assert int(s1) == int(s2) + + +def test_eq(): + random = CurveScalar.random_nonzero() + same = CurveScalar.from_int(int(random)) + different = CurveScalar.random_nonzero() + assert random == same + assert random == int(same) + assert random != different + assert random != int(different) + + +def test_serialization_rotations_of_1(): + + size_in_bytes = CURVE.scalar_size + for i in range(size_in_bytes): + lonely_one = 1 << i + bn = CurveScalar.from_int(lonely_one) + lonely_one_in_bytes = lonely_one.to_bytes(size_in_bytes, 'big') + + # Check serialization + assert bytes(bn) == lonely_one_in_bytes + + # Check deserialization + assert CurveScalar.from_bytes(lonely_one_in_bytes) == bn + + +def test_invalid_deserialization(): + size_in_bytes = CURVE.scalar_size + + # All-ones bytestring is invalid (since it's greater than the order) + lots_of_ones = b'\xFF' * size_in_bytes + with pytest.raises(ValueError): + CurveScalar.from_bytes(lots_of_ones) + + # Serialization of `order` is invalid since it's not strictly lower than + # the order of the curve + order = CURVE.order + with pytest.raises(ValueError): + CurveScalar.from_bytes(order.to_bytes(size_in_bytes, 'big')) + + # On the other hand, serialization of `order - 1` is valid + order -= 1 + CurveScalar.from_bytes(order.to_bytes(size_in_bytes, 'big')) + + +def test_add(): + r1 = CurveScalar.random_nonzero() + r2 = CurveScalar.random_nonzero() + r1i = int(r1) + r2i = int(r2) + assert r1 + r2 == (r1i + r2i) % CURVE.order + assert r1 + r2i == (r1i + r2i) % CURVE.order + + +def test_sub(): + r1 = CurveScalar.random_nonzero() + r2 = CurveScalar.random_nonzero() + r1i = int(r1) + r2i = int(r2) + assert r1 - r2 == (r1i - r2i) % CURVE.order + assert r1 - r2i == (r1i - r2i) % CURVE.order + + +def test_mul(): + r1 = CurveScalar.random_nonzero() + r2 = CurveScalar.random_nonzero() + r1i = int(r1) + r2i = int(r2) + assert r1 * r2 == (r1i * r2i) % CURVE.order + assert r1 * r2i == (r1i * r2i) % CURVE.order + + +def test_invert(): + r1 = CurveScalar.random_nonzero() + r1i = int(r1) + r1inv = r1.invert() + assert r1 * r1inv == CurveScalar.one() + assert (r1i * int(r1inv)) % CURVE.order == 1 + diff --git a/tests/test_dem.py b/tests/test_dem.py new file mode 100644 index 00000000..d0e6c9a9 --- /dev/null +++ b/tests/test_dem.py @@ -0,0 +1,80 @@ +import pytest +import os + +from umbral.dem import DEM, ErrorInvalidTag + + +def test_encrypt_decrypt(): + + key = os.urandom(DEM.KEY_SIZE) + dem = DEM(key) + + plaintext = b'peace at dawn' + + ciphertext0 = dem.encrypt(plaintext) + ciphertext1 = dem.encrypt(plaintext) + + assert ciphertext0 != plaintext + assert ciphertext1 != plaintext + + # Ciphertext should be different even with same plaintext. + assert ciphertext0 != ciphertext1 + + # Nonce should be different + assert ciphertext0[:DEM.NONCE_SIZE] != ciphertext1[:DEM.NONCE_SIZE] + + cleartext0 = dem.decrypt(ciphertext0) + cleartext1 = dem.decrypt(ciphertext1) + + assert cleartext0 == plaintext + assert cleartext1 == plaintext + + +def test_malformed_ciphertext(): + + key = os.urandom(DEM.KEY_SIZE) + dem = DEM(key) + + plaintext = b'peace at dawn' + ciphertext = dem.encrypt(plaintext) + + # So short it we can tell right away it doesn't even contain a nonce + with pytest.raises(ValueError, match="The ciphertext must include the nonce"): + dem.decrypt(ciphertext[:DEM.NONCE_SIZE-1]) + + # Too short to contain a tag + with pytest.raises(ValueError, match="The authentication tag is missing or malformed"): + dem.decrypt(ciphertext[:DEM.NONCE_SIZE + DEM.TAG_SIZE - 1]) + + # Too long + with pytest.raises(ErrorInvalidTag): + dem.decrypt(ciphertext + b'abcd') + + +def test_encrypt_decrypt_associated_data(): + key = os.urandom(32) + aad = b'secret code 1234' + + dem = DEM(key) + + plaintext = b'peace at dawn' + + ciphertext0 = dem.encrypt(plaintext, authenticated_data=aad) + ciphertext1 = dem.encrypt(plaintext, authenticated_data=aad) + + assert ciphertext0 != plaintext + assert ciphertext1 != plaintext + + assert ciphertext0 != ciphertext1 + + assert ciphertext0[:DEM.NONCE_SIZE] != ciphertext1[:DEM.NONCE_SIZE] + + cleartext0 = dem.decrypt(ciphertext0, authenticated_data=aad) + cleartext1 = dem.decrypt(ciphertext1, authenticated_data=aad) + + assert cleartext0 == plaintext + assert cleartext1 == plaintext + + # Attempt decryption with invalid associated data + with pytest.raises(ErrorInvalidTag): + cleartext2 = dem.decrypt(ciphertext0, authenticated_data=b'wrong data') diff --git a/tests/test_key_frag.py b/tests/test_key_frag.py new file mode 100644 index 00000000..72de83cf --- /dev/null +++ b/tests/test_key_frag.py @@ -0,0 +1,126 @@ +import pytest + +from umbral import KeyFrag, PublicKey, generate_kfrags +from umbral.key_frag import KeyFragID +from umbral.curve_scalar import CurveScalar + + +def test_kfrag_serialization(alices_keys, bobs_keys, kfrags): + + delegating_sk, signing_sk = alices_keys + _receiving_sk, receiving_pk = bobs_keys + + signing_pk = PublicKey.from_secret_key(signing_sk) + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + for kfrag in kfrags: + kfrag_bytes = bytes(kfrag) + new_kfrag = KeyFrag.from_bytes(kfrag_bytes) + + assert new_kfrag.verify(signing_pk=signing_pk, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk) + + assert new_kfrag == kfrag + + +def test_kfrag_verification(alices_keys, bobs_keys, kfrags): + + delegating_sk, signing_sk = alices_keys + _receiving_sk, receiving_pk = bobs_keys + + signing_pk = PublicKey.from_secret_key(signing_sk) + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + # Wrong signature + kfrag = kfrags[0] + kfrag.id = KeyFragID.random() + kfrag_bytes = bytes(kfrag) + new_kfrag = KeyFrag.from_bytes(kfrag_bytes) + assert not new_kfrag.verify(signing_pk=signing_pk, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk) + + # Wrong key + kfrag = kfrags[1] + kfrag.key = CurveScalar.random_nonzero() + kfrag_bytes = bytes(kfrag) + new_kfrag = KeyFrag.from_bytes(kfrag_bytes) + assert not new_kfrag.verify(signing_pk=signing_pk, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk) + + +@pytest.mark.parametrize('sign_delegating_key', + [False, True], + ids=['sign_delegating_key', 'dont_sign_delegating_key']) +@pytest.mark.parametrize('sign_receiving_key', + [False, True], + ids=['sign_receiving_key', 'dont_sign_receiving_key']) +def test_kfrag_signing(alices_keys, bobs_keys, sign_delegating_key, sign_receiving_key): + + delegating_sk, signing_sk = alices_keys + _receiving_sk, receiving_pk = bobs_keys + + signing_pk = PublicKey.from_secret_key(signing_sk) + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + kfrags = generate_kfrags(delegating_sk=delegating_sk, + signing_sk=signing_sk, + receiving_pk=receiving_pk, + threshold=6, + num_kfrags=10, + sign_delegating_key=sign_delegating_key, + sign_receiving_key=sign_receiving_key) + + kfrag = kfrags[0] + + # serialize/deserialize to make sure sign_* fields are serialized correctly + kfrag = KeyFrag.from_bytes(bytes(kfrag)) + + for pass_delegating_key, pass_receiving_key in zip([False, True], [False, True]): + + delegating_key_ok = (not sign_delegating_key) or pass_delegating_key + receiving_key_ok = (not sign_receiving_key) or pass_receiving_key + should_verify = delegating_key_ok and receiving_key_ok + + result = kfrag.verify(signing_pk=signing_pk, + delegating_pk=delegating_pk if pass_delegating_key else None, + receiving_pk=receiving_pk if pass_receiving_key else None) + + assert result == should_verify + + +def test_kfrag_is_hashable(kfrags): + + assert hash(kfrags[0]) != hash(kfrags[1]) + + new_kfrag = KeyFrag.from_bytes(bytes(kfrags[0])) + assert hash(new_kfrag) == hash(kfrags[0]) + + +def test_kfrag_str(kfrags): + s = str(kfrags[0]) + assert "KeyFrag" in s + + +WRONG_PARAMETERS = ( + # (num_kfrags, threshold) + (-1, -1), (-1, 0), (-1, 5), + (0, -1), (0, 0), (0, 5), + (1, -1), (1, 0), (1, 5), + (5, -1), (5, 0), (5, 10) +) + +@pytest.mark.parametrize("num_kfrags, threshold", WRONG_PARAMETERS) +def test_wrong_threshold_and_num_kfrags(num_kfrags, threshold, alices_keys, bobs_keys): + + delegating_sk, signing_sk = alices_keys + _receiving_sk, receiving_pk = bobs_keys + + with pytest.raises(ValueError): + generate_kfrags(delegating_sk=delegating_sk, + signing_sk=signing_sk, + receiving_pk=receiving_pk, + threshold=threshold, + num_kfrags=num_kfrags) diff --git a/tests/test_keys.py b/tests/test_keys.py new file mode 100644 index 00000000..2b0f698f --- /dev/null +++ b/tests/test_keys.py @@ -0,0 +1,203 @@ +import os +import string + +import pytest + +from umbral.keys import PublicKey, SecretKey, SecretKeyFactory, Signature +from umbral.hashing import Hash + + +def test_gen_key(): + sk = SecretKey.random() + assert type(sk) == SecretKey + + pk = PublicKey.from_secret_key(sk) + assert type(pk) == PublicKey + + pk2 = PublicKey.from_secret_key(sk) + assert pk == pk2 + + +def test_derive_key_from_label(): + factory = SecretKeyFactory.random() + + label = b"my_healthcare_information" + + sk1 = factory.secret_key_by_label(label) + assert type(sk1) == SecretKey + + pk1 = PublicKey.from_secret_key(sk1) + assert type(pk1) == PublicKey + + # Check that key derivation is reproducible + sk2 = factory.secret_key_by_label(label) + pk2 = PublicKey.from_secret_key(sk2) + assert sk1 == sk2 + assert pk1 == pk2 + + # Different labels on the same master secret create different keys + label = b"my_tax_information" + sk3 = factory.secret_key_by_label(label) + pk3 = PublicKey.from_secret_key(sk3) + assert sk1 != sk3 + + +def test_secret_key_serialization(): + sk = SecretKey.random() + encoded_key = bytes(sk) + decoded_key = SecretKey.from_bytes(encoded_key) + assert sk == decoded_key + + +def test_secret_key_str(): + sk = SecretKey.random() + s = str(sk) + assert s == "SecretKey:..." + + +def test_secret_key_hash(): + sk = SecretKey.random() + # Insecure Python hash, shouldn't be available. + with pytest.raises(NotImplementedError): + hash(sk) + + +def test_secret_key_factory_str(): + skf = SecretKeyFactory.random() + s = str(skf) + assert s == "SecretKeyFactory:..." + + +def test_secret_key_factory_hash(): + skf = SecretKeyFactory.random() + # Insecure Python hash, shouldn't be available. + with pytest.raises(NotImplementedError): + hash(skf) + + +def test_public_key_serialization(): + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + + encoded_key = bytes(pk) + decoded_key = PublicKey.from_bytes(encoded_key) + assert pk == decoded_key + + +def test_public_key_point(): + pk = PublicKey.from_secret_key(SecretKey.random()) + assert bytes(pk) == bytes(pk.point()) + + +def test_public_key_str(): + pk = PublicKey.from_secret_key(SecretKey.random()) + s = str(pk) + assert 'PublicKey' in s + + +def test_keying_material_serialization(): + factory = SecretKeyFactory.random() + + encoded_factory = bytes(factory) + decoded_factory = SecretKeyFactory.from_bytes(encoded_factory) + + label = os.urandom(32) + sk1 = factory.secret_key_by_label(label) + sk2 = decoded_factory.secret_key_by_label(label) + assert sk1 == sk2 + + +def test_public_key_is_hashable(): + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + + sk2 = SecretKey.random() + pk2 = PublicKey.from_secret_key(sk2) + assert hash(pk) != hash(pk2) + + pk3 = PublicKey.from_bytes(bytes(pk)) + assert hash(pk) == hash(pk3) + + +@pytest.mark.parametrize('execution_number', range(20)) # Run this test 20 times. +def test_sign_and_verify(execution_number): + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + + message = b"peace at dawn" + dst = b"dst" + + digest = Hash(dst) + digest.update(message) + signature = sk.sign_digest(digest) + + digest = Hash(dst) + digest.update(message) + assert signature.verify_digest(pk, digest) + + +@pytest.mark.parametrize('execution_number', range(20)) # Run this test 20 times. +def test_sign_serialize_and_verify(execution_number): + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + + message = b"peace at dawn" + dst = b"dst" + + digest = Hash(dst) + digest.update(message) + signature = sk.sign_digest(digest) + + signature_bytes = bytes(signature) + signature_restored = Signature.from_bytes(signature_bytes) + + digest = Hash(dst) + digest.update(message) + assert signature_restored.verify_digest(pk, digest) + + +def test_verification_fail(): + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + + message = b"peace at dawn" + dst = b"dst" + + digest = Hash(dst) + digest.update(message) + signature = sk.sign_digest(digest) + + # wrong DST + digest = Hash(b"other dst") + digest.update(message) + assert not signature.verify_digest(pk, digest) + + # wrong message + digest = Hash(dst) + digest.update(b"no peace at dawn") + assert not signature.verify_digest(pk, digest) + + # bad signature + signature_bytes = bytes(signature) + signature_bytes = b'\x00' + signature_bytes[1:] + signature_restored = Signature.from_bytes(signature_bytes) + + digest = Hash(dst) + digest.update(message) + assert not signature_restored.verify_digest(pk, digest) + + +def test_signature_repr(): + + sk = SecretKey.random() + pk = PublicKey.from_secret_key(sk) + + message = b"peace at dawn" + dst = b"dst" + + digest = Hash(dst) + digest.update(message) + signature = sk.sign_digest(digest) + + s = repr(signature) + assert 'Signature' in s diff --git a/tests/test_pre.py b/tests/test_pre.py new file mode 100644 index 00000000..0ee5b7a4 --- /dev/null +++ b/tests/test_pre.py @@ -0,0 +1,95 @@ +import pytest + +from umbral import ( + SecretKey, + PublicKey, + encrypt, + generate_kfrags, + decrypt_original, + reencrypt, + decrypt_reencrypted, + ) +from umbral.dem import ErrorInvalidTag + + +def test_public_key_encryption(alices_keys): + delegating_sk, _ = alices_keys + delegating_pk = PublicKey.from_secret_key(delegating_sk) + plaintext = b'peace at dawn' + capsule, ciphertext = encrypt(delegating_pk, plaintext) + plaintext_decrypted = decrypt_original(delegating_sk, capsule, ciphertext) + assert plaintext == plaintext_decrypted + + # Wrong secret key + sk = SecretKey.random() + with pytest.raises(ErrorInvalidTag): + decrypt_original(sk, capsule, ciphertext) + + +SIMPLE_API_PARAMETERS = ( + # (num_kfrags, threshold) + (1, 1), + (6, 1), + (6, 4), + (6, 6), + (50, 30) +) + +@pytest.mark.parametrize("num_kfrags, threshold", SIMPLE_API_PARAMETERS) +def test_simple_api(num_kfrags, threshold): + """ + This test models the main interactions between actors (i.e., Alice, + Bob, Data Source, and Ursulas) and artifacts (i.e., public and private keys, + ciphertexts, capsules, KFrags, CFrags, etc). + + The test covers all the main stages of data sharing: + key generation, delegation, encryption, decryption by + Alice, re-encryption by Ursula, and decryption by Bob. + """ + + # Key Generation (Alice) + delegating_sk = SecretKey.random() + delegating_pk = PublicKey.from_secret_key(delegating_sk) + + signing_sk = SecretKey.random() + signing_pk = PublicKey.from_secret_key(signing_sk) + + # Key Generation (Bob) + receiving_sk = SecretKey.random() + receiving_pk = PublicKey.from_secret_key(receiving_sk) + + # Encryption by an unnamed data source + plaintext = b'peace at dawn' + capsule, ciphertext = encrypt(delegating_pk, plaintext) + + # Decryption by Alice + plaintext_decrypted = decrypt_original(delegating_sk, capsule, ciphertext) + assert plaintext_decrypted == plaintext + + # Split Re-Encryption Key Generation (aka Delegation) + kfrags = generate_kfrags(delegating_sk, receiving_pk, signing_sk, threshold, num_kfrags) + + # Bob requests re-encryption to some set of M ursulas + cfrags = list() + for kfrag in kfrags[:threshold]: + # Ursula checks that the received kfrag is valid + assert kfrag.verify(signing_pk, delegating_pk, receiving_pk) + + # Re-encryption by an Ursula + cfrag = reencrypt(capsule, kfrag) + + # Bob collects the result + cfrags.append(cfrag) + + # Bob checks that the received cfrags are valid + assert all(cfrag.verify(capsule, delegating_pk, receiving_pk, signing_pk) for cfrag in cfrags) + + # Decryption by Bob + plaintext_reenc = decrypt_reencrypted(receiving_sk, + delegating_pk, + capsule, + cfrags[:threshold], + ciphertext, + ) + + assert plaintext_reenc == plaintext diff --git a/tests/test_serializable.py b/tests/test_serializable.py new file mode 100644 index 00000000..62d78e9b --- /dev/null +++ b/tests/test_serializable.py @@ -0,0 +1,92 @@ +import re + +import pytest + +from umbral.serializable import Serializable, serialize_bool, take_bool + + +class A(Serializable): + + def __init__(self, val: int): + assert 0 <= val < 2**32 + self.val = val + + @classmethod + def __take__(cls, data): + val_bytes, data = cls.__take_bytes__(data, 4) + return cls(int.from_bytes(val_bytes, byteorder='big')), data + + def __bytes__(self): + return self.val.to_bytes(4, byteorder='big') + + def __eq__(self, other): + return isinstance(other, A) and self.val == other.val + + +class B(Serializable): + + def __init__(self, val: int): + assert 0 <= val < 2**16 + self.val = val + + @classmethod + def __take__(cls, data): + val_bytes, data = cls.__take_bytes__(data, 2) + return cls(int.from_bytes(val_bytes, byteorder='big')), data + + def __bytes__(self): + return self.val.to_bytes(2, byteorder='big') + + def __eq__(self, other): + return isinstance(other, B) and self.val == other.val + + +class C(Serializable): + + def __init__(self, a: A, b: B): + self.a = a + self.b = b + + @classmethod + def __take__(cls, data): + components, data = cls.__take_types__(data, A, B) + return cls(*components), data + + def __bytes__(self): + return bytes(self.a) + bytes(self.b) + + def __eq__(self, other): + return isinstance(other, C) and self.a == other.a and self.b == other.b + + +def test_normal_operation(): + a = A(2**32 - 123) + b = B(2**16 - 456) + c = C(a, b) + c_back = C.from_bytes(bytes(c)) + assert c_back == c + + +def test_too_many_bytes(): + a = A(2**32 - 123) + b = B(2**16 - 456) + c = C(a, b) + with pytest.raises(ValueError, match="1 bytes remaining after deserializing"): + C.from_bytes(bytes(c) + b'\x00') + + +def test_not_enough_bytes(): + a = A(2**32 - 123) + b = B(2**16 - 456) + c = C(a, b) + # Will happen on deserialization of B - 1 byte missing + with pytest.raises(ValueError, match="cannot take 2 bytes from a bytestring of size 1"): + C.from_bytes(bytes(c)[:-1]) + + +def test_serialize_bool(): + assert take_bool(serialize_bool(True) + b'1234') == (True, b'1234') + assert take_bool(serialize_bool(False) + b'12') == (False, b'12') + error_msg = re.escape("Incorrectly serialized boolean; expected b'\\x00' or b'\\x01', got b'z'") + with pytest.raises(ValueError, match=error_msg): + take_bool(b'z1234') diff --git a/umbral/dem.py b/umbral/dem.py index bd705bac..b23fe9ba 100644 --- a/umbral/dem.py +++ b/umbral/dem.py @@ -4,11 +4,13 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes +import nacl from nacl.bindings.crypto_aead import ( crypto_aead_xchacha20poly1305_ietf_encrypt as xchacha_encrypt, crypto_aead_xchacha20poly1305_ietf_decrypt as xchacha_decrypt, crypto_aead_xchacha20poly1305_ietf_KEYBYTES as XCHACHA_KEY_SIZE, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES as XCHACHA_NONCE_SIZE, + crypto_aead_xchacha20poly1305_ietf_ABYTES as XCHACHA_TAG_SIZE, ) from . import openssl @@ -28,10 +30,15 @@ def kdf(data: bytes, return hkdf.derive(data) +class ErrorInvalidTag(Exception): + pass + + class DEM: KEY_SIZE = XCHACHA_KEY_SIZE NONCE_SIZE = XCHACHA_NONCE_SIZE + TAG_SIZE = XCHACHA_TAG_SIZE def __init__(self, key_material: bytes, @@ -53,5 +60,11 @@ def decrypt(self, nonce_and_ciphertext: bytes, authenticated_data: bytes = b"") nonce = nonce_and_ciphertext[:self.NONCE_SIZE] ciphertext = nonce_and_ciphertext[self.NONCE_SIZE:] - # TODO: replace `nacl.exceptions.CryptoError` with our error? - return xchacha_decrypt(ciphertext, authenticated_data, nonce, self._key) + # Prevent an out of bounds error deep in NaCl + if len(ciphertext) < self.TAG_SIZE: + raise ValueError(f"The authentication tag is missing or malformed") + + try: + return xchacha_decrypt(ciphertext, authenticated_data, nonce, self._key) + except nacl.exceptions.CryptoError: + raise ErrorInvalidTag From bcb0071f9ea2ea80182c07b20ae10691eeb8125d Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 20 Mar 2021 18:19:36 -0700 Subject: [PATCH 17/25] Update docs --- README.rst | 67 +++++----- docs/examples/umbral_simple_api.py | 123 +++++++----------- docs/notebooks/pyUmbral Simple API.ipynb | 145 +++++++++++----------- docs/source/choosing_and_using_curves.rst | 63 ---------- docs/source/index.rst | 4 +- docs/source/using_pyumbral.rst | 135 +++++++++----------- setup.py | 2 +- 7 files changed, 216 insertions(+), 323 deletions(-) delete mode 100644 docs/source/choosing_and_using_curves.rst diff --git a/README.rst b/README.rst index e914bec4..3fc364dd 100644 --- a/README.rst +++ b/README.rst @@ -62,19 +62,18 @@ Additionally, users that delegate access to their data (like Alice, in this exam .. code-block:: python - from umbral import pre, keys, signing + from umbral import SecretKey, PublicKey # Generate Umbral keys for Alice. - alices_private_key = keys.UmbralPrivateKey.gen_key() - alices_public_key = alices_private_key.get_pubkey() + alices_secret_key = SecretKey.random() + alices_public_key = PublicKey.from_secret_key(alices_secret_key) - alices_signing_key = keys.UmbralPrivateKey.gen_key() - alices_verifying_key = alices_signing_key.get_pubkey() - alices_signer = signing.Signer(private_key=alices_signing_key) + alices_signing_key = SecretKey.random() + alices_verifying_key = PublicKey.from_secret_key(alices_signing_key) # Generate Umbral keys for Bob. - bobs_private_key = keys.UmbralPrivateKey.gen_key() - bobs_public_key = bobs_private_key.get_pubkey() + bobs_secret_key = SecretKey.random() + bobs_public_key = PublicKey.from_secret_key(bobs_secret_key) **Encryption** @@ -89,14 +88,14 @@ Alice can open the capsule and decrypt the ciphertext with her private key. .. code-block:: python + from umbral import encrypt, decrypt_original + # Encrypt data with Alice's public key. plaintext = b'Proxy Re-Encryption is cool!' - ciphertext, capsule = pre.encrypt(alices_public_key, plaintext) + capsule, ciphertext = encrypt(alices_public_key, plaintext) # Decrypt data with Alice's private key. - cleartext = pre.decrypt(ciphertext=ciphertext, - capsule=capsule, - decrypting_key=alices_private_key) + cleartext = decrypt_original(alices_secret_key, capsule, ciphertext) **Re-Encryption Key Fragments** @@ -107,13 +106,15 @@ which are next sent to N proxies or *Ursulas*. .. code-block:: python + from umbral import generate_kfrags + # Alice generates "M of N" re-encryption key fragments (or "KFrags") for Bob. # In this example, 10 out of 20. - kfrags = pre.generate_kfrags(delegating_privkey=alices_private_key, - signer=alices_signer, - receiving_pubkey=bobs_public_key, - threshold=10, - N=20) + kfrags = generate_kfrags(delegating_sk=alices_secret_key, + receiving_pk=bobs_public_key, + signing_sk=alices_signing_key, + threshold=10, + num_kfrags=20) **Re-Encryption** @@ -127,17 +128,13 @@ Bob must gather at least ``threshold`` cfrags in order to activate the capsule. .. code-block:: python - # Several Ursulas perform re-encryption, and Bob collects the resulting `cfrags`. - # He must gather at least `threshold` `cfrags` in order to activate the capsule. - - capsule.set_correctness_keys(delegating=alices_public_key, - receiving=bobs_public_key, - verifying=alices_verifying_key) + from umbral import reencrypt - cfrags = list() # Bob's cfrag collection - for kfrag in kfrags[:10]: - cfrag = pre.reencrypt(kfrag=kfrag, capsule=capsule) - cfrags.append(cfrag) # Bob collects a cfrag + # Several Ursulas perform re-encryption, and Bob collects the resulting `cfrags`. + cfrags = list() # Bob's cfrag collection + for kfrag in kfrags[:10]: + cfrag = pre.reencrypt(capsule=capsule, kfrag=kfrag) + cfrags.append(cfrag) # Bob collects a cfrag **Decryption by Bob** @@ -147,14 +144,14 @@ and then decrypts the re-encrypted ciphertext. .. code-block:: python - # Bob activates and opens the capsule - for cfrag in cfrags: - capsule.attach_cfrag(cfrag) + from umbral import decrypt_reencrypted - bob_cleartext = pre.decrypt(ciphertext=ciphertext, - capsule=capsule, - decrypting_key=bobs_private_key) - assert bob_cleartext == plaintext + bob_cleartext = pre.decrypt_reencrypted(decrypting_sk=bobs_secret_key, + delegating_pk=alices_public_key, + capsule=capsule, + cfrags=cfrags, + ciphertext=ciphertext) + assert bob_cleartext == plaintext See more detailed usage examples in the docs_ directory. @@ -171,7 +168,7 @@ To install pyUmbral, simply use ``pip``: $ pip3 install umbral -Alternatively, you can checkout the repo and install it from there. +Alternatively, you can checkout the repo and install it from there. The NuCypher team uses ``pipenv`` for managing pyUmbral's dependencies. The recommended installation procedure is as follows: diff --git a/docs/examples/umbral_simple_api.py b/docs/examples/umbral_simple_api.py index f87f7719..22e1030f 100644 --- a/docs/examples/umbral_simple_api.py +++ b/docs/examples/umbral_simple_api.py @@ -1,41 +1,20 @@ -""" -This file is part of pyUmbral. - -pyUmbral is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyUmbral is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyUmbral. If not, see . -""" - -#1 -# Sets a default curve (secp256k1) import random -from umbral import pre, keys, config, signing +from umbral import ( + SecretKey, PublicKey, + encrypt, generate_kfrags, reencrypt, decrypt_original, decrypt_reencrypted) +from umbral.dem import ErrorInvalidTag -config.set_default_curve() - -#2 # Generate an Umbral key pair # --------------------------- # First, Let's generate two asymmetric key pairs for Alice: # A delegating key pair and a Signing key pair. -alices_private_key = keys.UmbralPrivateKey.gen_key() -alices_public_key = alices_private_key.get_pubkey() +alices_secret_key = SecretKey.random() +alices_public_key = PublicKey.from_secret_key(alices_secret_key) -alices_signing_key = keys.UmbralPrivateKey.gen_key() -alices_verifying_key = alices_signing_key.get_pubkey() -alices_signer = signing.Signer(private_key=alices_signing_key) +alices_signing_key = SecretKey.random() +alices_verifying_key = PublicKey.from_secret_key(alices_signing_key) -#3 # Encrypt some data for Alice # --------------------------- # Now let's encrypt data with Alice's public key. @@ -44,98 +23,86 @@ # this operation. plaintext = b'Proxy Re-encryption is cool!' -ciphertext, capsule = pre.encrypt(alices_public_key, plaintext) +capsule, ciphertext = encrypt(alices_public_key, plaintext) print(ciphertext) -#4 # Decrypt data for Alice # ---------------------- # Since data was encrypted with Alice's public key, # Alice can open the capsule and decrypt the ciphertext with her private key. -cleartext = pre.decrypt(ciphertext=ciphertext, - capsule=capsule, - decrypting_key=alices_private_key) +cleartext = decrypt_original(alices_secret_key, capsule, ciphertext) print(cleartext) -#5 # Bob Exists # ----------- -bobs_private_key = keys.UmbralPrivateKey.gen_key() -bobs_public_key = bobs_private_key.get_pubkey() +bobs_secret_key = SecretKey.random() +bobs_public_key = PublicKey.from_secret_key(bobs_secret_key) -#6 # Bob receives a capsule through a side channel (s3, ipfs, Google cloud, etc) bob_capsule = capsule -#7 # Attempt Bob's decryption (fail) try: - fail_decrypted_data = pre.decrypt(ciphertext=ciphertext, - capsule=bob_capsule, - decrypting_key=bobs_private_key) -except pre.UmbralDecryptionError: + fail_decrypted_data = decrypt_original(bobs_secret_key, bob_capsule, ciphertext) +except ErrorInvalidTag: print("Decryption failed! Bob doesn't has access granted yet.") -#8 -# Alice grants access to Bob by generating kfrags +# Alice grants access to Bob by generating kfrags # ----------------------------------------------- -# When Alice wants to grant Bob access to open her encrypted messages, -# she creates *threshold split re-encryption keys*, or *"kfrags"*, -# which are next sent to N proxies or *Ursulas*. -# She uses her private key, and Bob's public key, and she sets a minimum +# When Alice wants to grant Bob access to open her encrypted messages, +# she creates *threshold split re-encryption keys*, or *"kfrags"*, +# which are next sent to N proxies or *Ursulas*. +# She uses her private key, and Bob's public key, and she sets a minimum # threshold of 10, for 20 total shares -kfrags = pre.generate_kfrags(delegating_privkey=alices_private_key, - signer=alices_signer, - receiving_pubkey=bobs_public_key, - threshold=10, - N=20) +kfrags = generate_kfrags(delegating_sk=alices_secret_key, + receiving_pk=bobs_public_key, + signing_sk=alices_signing_key, + threshold=10, + num_kfrags=20) -#9 # Ursulas perform re-encryption # ------------------------------ -# Bob asks several Ursulas to re-encrypt the capsule so he can open it. -# Each Ursula performs re-encryption on the capsule using the `kfrag` +# Bob asks several Ursulas to re-encrypt the capsule so he can open it. +# Each Ursula performs re-encryption on the capsule using the `kfrag` # provided by Alice, obtaining this way a "capsule fragment", or `cfrag`. # Let's mock a network or transport layer by sampling `threshold` random `kfrags`, # one for each required Ursula. -import random - kfrags = random.sample(kfrags, # All kfrags from above 10) # M - Threshold -# Bob collects the resulting `cfrags` from several Ursulas. -# Bob must gather at least `threshold` `cfrags` in order to activate the capsule. - -bob_capsule.set_correctness_keys(delegating=alices_public_key, - receiving=bobs_public_key, - verifying=alices_verifying_key) +# Bob collects the resulting `cfrags` from several Ursulas. +# Bob must gather at least `threshold` `cfrags` in order to open the capsule. cfrags = list() # Bob's cfrag collection for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag=kfrag, capsule=bob_capsule) + cfrag = reencrypt(capsule=capsule, kfrag=kfrag) cfrags.append(cfrag) # Bob collects a cfrag assert len(cfrags) == 10 -#10 -# Bob attaches cfrags to the capsule -# ---------------------------------- -# Bob attaches at least `threshold` `cfrags` to the capsule; -# then it can become *activated*. +# Bob checks the capsule fragments +# -------------------------------- +# Bob can verify that the capsule fragments are valid and really originate from Alice, +# using Alice's public keys. -for cfrag in cfrags: - bob_capsule.attach_cfrag(cfrag) +assert all(cfrag.verify(capsule, + delegating_pk=alices_public_key, + receiving_pk=bobs_public_key, + signing_pk=alices_verifying_key) + for cfrag in cfrags) -#11 -# Bob activates and opens the capsule +# Bob opens the capsule # ------------------------------------ -# Finally, Bob activates and opens the capsule, -# then decrypts the re-encrypted ciphertext. +# Finally, Bob decrypts the re-encrypted ciphertext using his key. -bob_cleartext = pre.decrypt(ciphertext=ciphertext, capsule=bob_capsule, decrypting_key=bobs_private_key) +bob_cleartext = decrypt_reencrypted(decrypting_sk=bobs_secret_key, + delegating_pk=alices_public_key, + capsule=bob_capsule, + cfrags=cfrags, + ciphertext=ciphertext) print(bob_cleartext) assert bob_cleartext == plaintext diff --git a/docs/notebooks/pyUmbral Simple API.ipynb b/docs/notebooks/pyUmbral Simple API.ipynb index 7bdd010d..eff9dd1e 100644 --- a/docs/notebooks/pyUmbral Simple API.ipynb +++ b/docs/notebooks/pyUmbral Simple API.ipynb @@ -7,30 +7,6 @@ "# pyUmbral Python API" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting the default curve\n", - "\n", - "The first time you use umbral, you may want to specify an elliptic curve to use. If you do not specify a curve, secp256k1 will be used for all operations, with a slight performace hit for the lookup.\n", - "\n", - "To set the default curve use `umbral.config.set_default_curve()`\n", - "\n", - "Note: you can only set the dafault once, or `UmbralConfigurationError` will be raised." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from umbral.config import set_default_curve\n", - "\n", - "set_default_curve()" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -46,16 +22,15 @@ "metadata": {}, "outputs": [], "source": [ - "from umbral import keys, signing\n", + "from umbral import SecretKey, PublicKey\n", "\n", "\n", "# Alice's Keys\n", - "alices_private_key = keys.UmbralPrivateKey.gen_key()\n", - "alices_public_key = alices_private_key.get_pubkey()\n", + "alices_private_key = SecretKey.random()\n", + "alices_public_key = PublicKey.from_secret_key(alices_private_key)\n", "\n", - "alices_signing_key = keys.UmbralPrivateKey.gen_key()\n", - "alices_verifying_key = alices_signing_key.get_pubkey()\n", - "alices_signer = signing.Signer(private_key=alices_signing_key)" + "alices_signing_key = SecretKey.random()\n", + "alices_verifying_key = PublicKey.from_secret_key(alices_signing_key)" ] }, { @@ -80,16 +55,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "b'#\\xebQ\\xd4\\xad\\x8ah,9\\x8f\\xc9\\x18\\x84[\\x95M\\x8e\\xb1\\x85\\xf9\\xbe\\x97\\x07\\xf3\\x80@\\x11\\xab\\x82\\xac\\xa1\\xbf\\xc0\\x00e\\xecpTq\\xef\\x94\\xd94\\x94\\x1a\\xdf\\xf0\\x04)\\xf5\\r\\xc4\\xbd/:\\x8c'\n" + "b'\\x1c\\xa0\\xa83\\x0cv\\x97\\x02d\\xe9\\xe9\\xc5_\\x9d5NRGRx\\xd4\\xc9\\x17%\\x9b\\xb4\\x05\\xd1\\xc2\\x1e\\x9d\\x0b\\xbf\\xb4g\\xf0n\\xfe\\x9eM\\x93\\xe0\\xbf#l\\xf9\\x033\\xb00\\xf5\\r\\xff\\xc9\\x133C\\xf0\\xa3\\xc0\\xd1e\\xdb~.E$%'\n" ] } ], "source": [ - "from umbral import pre\n", + "from umbral import encrypt\n", "\n", "\n", "plaintext = b'Proxy Re-encryption is cool!'\n", - "ciphertext, capsule = pre.encrypt(alices_public_key, plaintext)\n", + "capsule, ciphertext = encrypt(alices_public_key, plaintext)\n", "print(ciphertext)" ] }, @@ -115,10 +90,13 @@ } ], "source": [ - "cleartext = pre.decrypt(ciphertext=ciphertext, \n", - " capsule=capsule, \n", - " decrypting_key=alices_private_key)\n", - "print(cleartext)\n" + "from umbral import decrypt_original\n", + "\n", + "\n", + "cleartext = decrypt_original(sk=alices_private_key,\n", + " capsule=capsule,\n", + " ciphertext=ciphertext)\n", + "print(cleartext)" ] }, { @@ -135,8 +113,8 @@ "metadata": {}, "outputs": [], "source": [ - "bobs_private_key = keys.UmbralPrivateKey.gen_key()\n", - "bobs_public_key = bobs_private_key.get_pubkey()\n", + "bobs_private_key = SecretKey.random()\n", + "bobs_public_key = PublicKey.from_secret_key(bobs_private_key)\n", "\n", "bob_capsule = capsule" ] @@ -162,11 +140,13 @@ } ], "source": [ + "from umbral.dem import ErrorInvalidTag\n", + "\n", "try:\n", - " fail_decrypted_data = pre.decrypt(ciphertext=ciphertext, \n", - " capsule=capsule, \n", - " decrypting_key=bobs_private_key)\n", - "except pre.UmbralDecryptionError:\n", + " fail_decrypted_data = decrypt_original(sk=bobs_private_key,\n", + " capsule=capsule,\n", + " ciphertext=ciphertext)\n", + "except ErrorInvalidTag:\n", " print(\"Decryption failed! Bob doesn't has access granted yet.\")\n" ] }, @@ -184,21 +164,24 @@ "metadata": {}, "source": [ "## Alice grants access to Bob by generating KFrags \n", - "When Alice wants to grant Bob access to open her encrypted messages, she creates *re-encryption key fragments*, or *\"kfrags\"*, which are next sent to N proxies or *Ursulas*. She uses her private key, and Bob's public key, and she sets a minimum threshold of 10, for 20 total shares\n" + "When Alice wants to grant Bob access to open her encrypted messages, she creates *re-encryption key fragments*, or \"kfrags\", which are next sent to N proxies or *Ursulas*. She uses her private key, and Bob's public key, and she sets a minimum threshold of 10, for 20 total shares\n" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ + "from umbral import generate_kfrags\n", + "\n", + "\n", "M, N = 10, 20\n", - "kfrags = pre.generate_kfrags(delegating_privkey=alices_private_key, \n", - " receiving_pubkey=bobs_public_key, \n", - " signer=alices_signer,\n", - " threshold=M, \n", - " N=N)\n" + "kfrags = generate_kfrags(delegating_sk=alices_private_key,\n", + " receiving_pk=bobs_public_key,\n", + " signing_sk=alices_signing_key,\n", + " threshold=10,\n", + " num_kfrags=20)" ] }, { @@ -207,12 +190,12 @@ "source": [ "\n", "## Ursulas Re-encrypt; Bob attaches fragments to `capsule`\n", - "Bob asks several Ursulas to re-encrypt the capsule so he can open it. Each Ursula performs re-encryption on the capsule using the `kfrag` provided by Alice, obtaining this way a \"capsule fragment\", or `cfrag`. Let's mock a network or transport layer by sampling `M` random `kfrags`, one for each required Ursula. Note that each Ursula must prepare the received capsule before re-encryption by setting the proper correctness keys. Bob collects the resulting `cfrags` from several Ursulas. He must gather at least `M` `cfrags` in order to activate the capsule.\n" + "Bob asks several Ursulas to re-encrypt the capsule so he can open it. Each Ursula performs re-encryption on the capsule using the `kfrag` provided by Alice, obtaining this way a \"capsule fragment\", or `cfrag`. Let's mock a network or transport layer by sampling `M` random `kfrags`, one for each required Ursula. Bob collects the resulting `cfrags` from several Ursulas. He must gather at least `M` `cfrags` in order to activate the capsule.\n" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -220,13 +203,13 @@ "kfrags = random.sample(kfrags, # All kfrags from above\n", " 10) # M - Threshold\n", "\n", - "bob_capsule.set_correctness_keys(delegating=alices_public_key,\n", - " receiving=bobs_public_key,\n", - " verifying=alices_verifying_key)\n", + "\n", + "from umbral import reencrypt\n", + "\n", "\n", "cfrags = list() # Bob's cfrag collection\n", "for kfrag in kfrags:\n", - " cfrag = pre.reencrypt(kfrag=kfrag, capsule=bob_capsule)\n", + " cfrag = reencrypt(capsule=capsule, kfrag=kfrag)\n", " cfrags.append(cfrag) # Bob collects a cfrag\n", "\n", "assert len(cfrags) == 10\n" @@ -236,15 +219,36 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Bob activates and opens the capsule; Decrypts data from Alice.\n", - "The `capsule` can become *activated* once Bob attaches at least `M` `cfrags` to it. Note that it has to be prepared in advance with the necessary `correctness_keys` (specifically, Alice's public key, Alice's signature verification key and his own public key). \n", - "\n", - "Finally, Bob activates and opens the capsule, then decrypts the re-encrypted ciphertext." + "## Bob checks the capsule fragments\n", + "Bob can verify that the capsule fragments are valid and really originate from Alice, using Alice's public keys." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "assert all(cfrag.verify(capsule,\n", + " delegating_pk=alices_public_key,\n", + " receiving_pk=bobs_public_key,\n", + " signing_pk=alices_verifying_key)\n", + " for cfrag in cfrags)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bob opens the capsule; Decrypts data from Alice.\n", + "Finally, Bob decrypts the re-encrypted ciphertext using his key." + ] + }, + { + "cell_type": "code", + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -256,17 +260,16 @@ } ], "source": [ - "bob_capsule.set_correctness_keys(delegating=alices_public_key,\n", - " receiving=bobs_public_key,\n", - " verifying=alices_verifying_key)\n", + "from umbral import decrypt_reencrypted\n", + "\n", + "bob_cleartext = decrypt_reencrypted(decrypting_sk=bobs_private_key,\n", + " delegating_pk=alices_public_key,\n", + " capsule=capsule,\n", + " cfrags=cfrags,\n", + " ciphertext=ciphertext)\n", "\n", - "for cfrag in cfrags:\n", - " bob_capsule.attach_cfrag(cfrag)\n", - " \n", - "bob_cleartext = pre.decrypt(ciphertext=ciphertext, capsule=capsule, decrypting_key=bobs_private_key)\n", "print(bob_cleartext)\n", - "assert bob_cleartext == plaintext\n", - "\n" + "assert bob_cleartext == plaintext" ] }, { @@ -293,7 +296,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.8.5" } }, "nbformat": 4, diff --git a/docs/source/choosing_and_using_curves.rst b/docs/source/choosing_and_using_curves.rst deleted file mode 100644 index fa1357b0..00000000 --- a/docs/source/choosing_and_using_curves.rst +++ /dev/null @@ -1,63 +0,0 @@ -========================= -Choosing and Using Curves -========================= - - -The matter of which curve to use is the subject of some debate. If you aren't sure, you might start here: -https://safecurves.cr.yp.to/ - -A number of curves are available in the Cryptography.io_ library, on which pyUmbral depends. -You can find them in the ``cryptography.hazmat.primitives.asymmetric.ec`` module. - -.. _Cryptography.io: https://cryptography.io/en/latest/ - -Be careful when choosing a curve - the security of your application depends on it. - -We provide curve ``SECP256K1`` as a default because it is the basis for a number of crypto-blockchain projects; -we don't otherwise endorse its security. -We additionally support curves ``SECP256R1`` (also known as "NIST P-256") and ``SECP384R1`` ("NIST P-384"). - - -Setting a default curve --------------------------- - -Before you perform any ECC operations, you can set a default curve. - -.. code-block:: python - - >>> from umbral.curve import SECP256K1 - >>> config.set_default_curve(SECP256K1) - -If you don't set a default curve, then SECP256K1 will be set for you when you perform the first ECC -operation. This causes a small one-time performance penalty. - - -.. code-block:: python - - >>> from umbral import keys - >>> private_key = keys.UmbralPrivateKey.gen_key() - - RuntimeWarning: No default curve has been set. Using SECP256K1. - A slight performance penalty has been incurred for only this call. - Set a default curve with umbral.config.set_default_curve(). - - -To use SECP256K1 and avoid this penalty, you can simply call ``set_default_curve()`` with no argument: - - -.. code-block:: python - - >>> config.set_default_curve() - -Attempting to set the default curve twice in the same runtime will raise -a ``UmbralConfigurationError``. - - -.. code-block:: python - - >>> from umbral import config - >>> config.set_default_curve() - >>> config.set_default_curve() - Traceback (most recent call last): - ... - umbral.config._CONFIG.UmbralConfigurationError diff --git a/docs/source/index.rst b/docs/source/index.rst index ba8074de..12ce4d96 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,7 +31,7 @@ pyUmbral .. end-badges pyUmbral is the reference implementation of the Umbral_ threshold proxy re-encryption scheme. -It is open-source, built with Python, and uses OpenSSL_ and Cryptography.io_. +It is open-source, built with Python, and uses OpenSSL_ via Cryptography.io_ and libsodium_ via PyNaCl_. Using Umbral, Alice (the data owner) can *delegate decryption rights* to Bob for any ciphertext intended to her, through a re-encryption process performed by a @@ -50,6 +50,8 @@ a proxy re-encryption network to empower privacy in decentralized systems. .. _Cryptography.io: https://cryptography.io/en/latest/ .. _OpenSSL: https://www.openssl.org/ .. _nucypher: https://github.com/nucypher/nucypher +.. _libsodium: https://github.com/jedisct1/libsodium +.. _PyNaCl: https://pynacl.readthedocs.io/en/latest/ .. toctree:: :maxdepth: 3 diff --git a/docs/source/using_pyumbral.rst b/docs/source/using_pyumbral.rst index b7fe0f80..9fdde746 100644 --- a/docs/source/using_pyumbral.rst +++ b/docs/source/using_pyumbral.rst @@ -12,30 +12,22 @@ Using pyUmbral sys.path.append(os.path.abspath(os.getcwd())) -.. testcleanup:: capsule_story +Elliptic Curves +=============== - from umbral import config - config._CONFIG.___CONFIG__curve = None - config._CONFIG.___CONFIG__params = None +The matter of which curve to use is the subject of some debate. If you aren't sure, you might start here: +https://safecurves.cr.yp.to/ +A number of curves are available in the Cryptography.io_ library, on which pyUmbral depends. +You can find them in the ``cryptography.hazmat.primitives.asymmetric.ec`` module. -Configuration -============== - - -Setting the default curve --------------------------- - -The best way to start using pyUmbral is to decide on an elliptic curve to use and set it as your default. - - -.. doctest:: capsule_story +.. _Cryptography.io: https://cryptography.io/en/latest/ - >>> from umbral import config - >>> from umbral.curve import SECP256K1 - >>> config.set_default_curve(SECP256K1) +Be careful when choosing a curve - the security of your application depends on it. -For more information on curves, see :doc:`choosing_and_using_curves`. +We provide curve ``SECP256K1`` as a default because it is the basis for a number of crypto-blockchain projects; +we don't otherwise endorse its security. +We additionally support curves ``SECP256R1`` (also known as "NIST P-256") and ``SECP384R1`` ("NIST P-384"), but they cannot currently be selected via the public API. Encryption @@ -49,28 +41,27 @@ A delegating key pair and a signing key pair. .. doctest:: capsule_story - >>> from umbral import keys, signing + >>> from umbral import SecretKey, PublicKey - >>> alices_private_key = keys.UmbralPrivateKey.gen_key() - >>> alices_public_key = alices_private_key.get_pubkey() + >>> alices_secret_key = SecretKey.random() + >>> alices_public_key = PublicKey.from_secret_key(alices_secret_key) - >>> alices_signing_key = keys.UmbralPrivateKey.gen_key() - >>> alices_verifying_key = alices_signing_key.get_pubkey() - >>> alices_signer = signing.Signer(private_key=alices_signing_key) + >>> alices_signing_key = SecretKey.random() + >>> alices_verifying_key = PublicKey.from_secret_key(alices_signing_key) Encrypt with a public key -------------------------- Now let's encrypt data with Alice's public key. -Invocation of ``pre.encrypt`` returns both the ``ciphertext`` and a ``capsule``. +Invocation of :py:func:`encrypt` returns both a ``capsule`` and a ``ciphertext``. Note that anyone with Alice's public key can perform this operation. .. doctest:: capsule_story - >>> from umbral import pre + >>> from umbral import encrypt >>> plaintext = b'Proxy Re-encryption is cool!' - >>> ciphertext, capsule = pre.encrypt(alices_public_key, plaintext) + >>> capsule, ciphertext = encrypt(alices_public_key, plaintext) Decrypt with a private key @@ -80,9 +71,8 @@ Alice can open the capsule and decrypt the ciphertext with her private key. .. doctest:: capsule_story - >>> cleartext = pre.decrypt(ciphertext=ciphertext, - ... capsule=capsule, - ... decrypting_key=alices_private_key) + >>> from umbral import decrypt_original + >>> cleartext = decrypt_original(alices_secret_key, capsule, ciphertext) Threshold Re-Encryption @@ -93,29 +83,29 @@ Bob Exists .. doctest:: capsule_story - >>> from umbral import keys - >>> bobs_private_key = keys.UmbralPrivateKey.gen_key() - >>> bobs_public_key = bobs_private_key.get_pubkey() + >>> bobs_secret_key = SecretKey.random() + >>> bobs_public_key = PublicKey.from_secret_key(bobs_secret_key) -Alice grants access to Bob by generating kfrags +Alice grants access to Bob by generating kfrags ----------------------------------------------- -When Alice wants to grant Bob access to open her encrypted messages, +When Alice wants to grant Bob access to open her encrypted messages, she creates *re-encryption key fragments*, or *"kfrags"*, which are next sent to N proxies or *Ursulas*. -Alice must specify ``N`` (the total number of kfrags), +Alice must specify ``num_kfrags`` (the total number of kfrags), and a ``threshold`` (the minimum number of kfrags needed to activate a capsule). In the following example, Alice creates 20 kfrags, but Bob needs to get only 10 re-encryptions to activate the capsule. .. doctest:: capsule_story - >>> kfrags = pre.generate_kfrags(delegating_privkey=alices_private_key, - ... signer=alices_signer, - ... receiving_pubkey=bobs_public_key, - ... threshold=10, - ... N=20) + >>> from umbral import generate_kfrags + >>> kfrags = generate_kfrags(delegating_sk=alices_secret_key, + ... receiving_pk=bobs_public_key, + ... signing_sk=alices_signing_key, + ... threshold=10, + ... num_kfrags=20) Bob receives a capsule @@ -137,25 +127,24 @@ or re-encrypted for him by Ursula, he will not be able to open it. .. doctest:: capsule_story - >>> fail = pre.decrypt(ciphertext=ciphertext, - ... capsule=capsule, - ... decrypting_key=bobs_private_key) + >>> fail = decrypt_original(sk=bobs_secret_key, + ... capsule=capsule, + ... ciphertext=ciphertext) Traceback (most recent call last): ... - umbral.pre.UmbralDecryptionError + umbral.dem.ErrorInvalidTag Ursulas perform re-encryption ------------------------------ -Bob asks several Ursulas to re-encrypt the capsule so he can open it. +Bob asks several Ursulas to re-encrypt the capsule so he can open it. Each Ursula performs re-encryption on the capsule using the ``kfrag`` provided by Alice, obtaining this way a "capsule fragment", or ``cfrag``. Let's mock a network or transport layer by sampling ``threshold`` random kfrags, -one for each required Ursula. Note that each Ursula must prepare the received -capsule before re-encryption by setting the proper correctness keys. +one for each required Ursula. Bob collects the resulting cfrags from several Ursulas. -Bob must gather at least ``threshold`` cfrags in order to activate the capsule. +Bob must gather at least ``threshold`` cfrags in order to open the capsule. .. doctest:: capsule_story @@ -164,14 +153,10 @@ Bob must gather at least ``threshold`` cfrags in order to activate the capsule. >>> kfrags = random.sample(kfrags, # All kfrags from above ... 10) # M - Threshold - >>> capsule.set_correctness_keys(delegating=alices_public_key, - ... receiving=bobs_public_key, - ... verifying=alices_verifying_key) - (True, True, True) - + >>> from umbral import reencrypt >>> cfrags = list() # Bob's cfrag collection >>> for kfrag in kfrags: - ... cfrag = pre.reencrypt(kfrag=kfrag, capsule=capsule) + ... cfrag = reencrypt(capsule=capsule, kfrag=kfrag) ... cfrags.append(cfrag) # Bob collects a cfrag .. doctest:: capsule_story @@ -183,32 +168,34 @@ Bob must gather at least ``threshold`` cfrags in order to activate the capsule. Decryption ================================== -Bob attaches cfrags to the capsule ----------------------------------- -Bob attaches at least ``threshold`` cfrags to the capsule, -which has to be prepared in advance with the necessary correctness keys. -Only then it can become *activated*. +Bob checks the capsule fragments +-------------------------------- +Bob can verify that the capsule fragments are valid and really originate from Alice, +using Alice's public keys. .. doctest:: capsule_story - >>> capsule.set_correctness_keys(delegating=alices_public_key, - ... receiving=bobs_public_key, - ... verifying=alices_verifying_key) - (False, False, False) + >>> all(cfrag.verify(capsule, + ... delegating_pk=alices_public_key, + ... receiving_pk=bobs_public_key, + ... signing_pk=alices_verifying_key) + ... for cfrag in cfrags) + True - >>> for cfrag in cfrags: - ... capsule.attach_cfrag(cfrag) - -Bob activates and opens the capsule ------------------------------------- -Finally, Bob decrypts the re-encrypted ciphertext using the activated capsule. +Bob opens the capsule +--------------------- +Finally, Bob decrypts the re-encrypted ciphertext using his key. .. doctest:: capsule_story - >>> cleartext = pre.decrypt(ciphertext=ciphertext, - ... capsule=capsule, - ... decrypting_key=bobs_private_key) + >>> from umbral import decrypt_reencrypted + >>> cleartext = decrypt_reencrypted(decrypting_sk=bobs_secret_key, + ... delegating_pk=alices_public_key, + ... capsule=capsule, + ... cfrags=cfrags, + ... ciphertext=ciphertext) + .. doctest:: capsule_story :hide: diff --git a/setup.py b/setup.py index ca53b41e..992124d4 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ def run(self): EXTRAS_REQUIRE = { 'testing': DEV_INSTALL_REQUIRES, - 'docs': ['sphinx', 'sphinx-autobuild'], + 'docs': ['sphinx', 'sphinx-autobuild', 'sphinx_rtd_theme'], 'benchmarks': ['pytest-benchmark'], } From 0f82580c7e01f4b18d0d756bd1e4f9c1de5eb2f0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 20 Mar 2021 20:06:03 -0700 Subject: [PATCH 18/25] Add some API docs --- docs/source/api.rst | 57 ++++++++++++++++++++++++++++++++++++ docs/source/index.rst | 1 + docs/source/installation.rst | 3 +- umbral/capsule.py | 3 ++ umbral/capsule_frag.py | 8 +++++ umbral/key_frag.py | 15 ++++++++++ umbral/keys.py | 24 +++++++++++---- umbral/pre.py | 13 ++++++-- umbral/serializable.py | 22 ++++++++++++++ 9 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 docs/source/api.rst diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 00000000..27e691b4 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,57 @@ +Public API +========== + +.. automodule:: umbral + +Keys +---- + +.. autoclass:: SecretKey() + :members: + :show-inheritance: + +.. autoclass:: PublicKey() + :members: + :special-members: __eq__, __hash__ + :show-inheritance: + +.. autoclass:: SecretKeyFactory() + :members: + :show-inheritance: + +Intermediate objects +-------------------- + +.. autoclass:: Capsule() + :special-members: __eq__, __hash__ + :show-inheritance: + +.. autoclass:: KeyFrag() + :members: verify + :special-members: __eq__, __hash__ + :show-inheritance: + +.. autoclass:: CapsuleFrag() + :members: verify + :special-members: __eq__, __hash__ + :show-inheritance: + +Encryption, re-encryption and decryption +---------------------------------------- + +.. autofunction:: encrypt + +.. autofunction:: decrypt_original + +.. autofunction:: generate_kfrags + +.. autofunction:: reencrypt + +.. autofunction:: decrypt_reencrypted + +Utilities +--------- + +.. autoclass:: umbral.serializable.Serializable + :members: from_bytes + :special-members: __bytes__ diff --git a/docs/source/index.rst b/docs/source/index.rst index 12ce4d96..00510458 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -59,6 +59,7 @@ a proxy re-encryption network to empower privacy in decentralized systems. installation using_pyumbral + api Academic Whitepaper diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 90b99cdc..b6e8239f 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,7 +1,6 @@ - Installing pyUmbral ==================== -v0.1.3-alpha.2 + Using pip ------------------------- diff --git a/umbral/capsule.py b/umbral/capsule.py index 80b00e35..0ca1d4b4 100644 --- a/umbral/capsule.py +++ b/umbral/capsule.py @@ -20,6 +20,9 @@ def lambda_coeff(xs: Sequence[CurveScalar], i: int) -> CurveScalar: class Capsule(Serializable): + """ + Encapsulated symmetric key. + """ class NotValid(ValueError): """ diff --git a/umbral/capsule_frag.py b/umbral/capsule_frag.py index e8fc8168..b1d48f47 100644 --- a/umbral/capsule_frag.py +++ b/umbral/capsule_frag.py @@ -95,6 +95,9 @@ def from_kfrag_and_cfrag(cls, class CapsuleFrag(Serializable): + """ + Re-encrypted fragment of :py:class:`Capsule`. + """ def __init__(self, point_e1: CurvePoint, @@ -160,6 +163,11 @@ def verify(self, signing_pk: PublicKey, metadata: Optional[bytes] = None, ) -> bool: + """ + Verifies the validity of this fragment. + + ``metadata`` should coincide with the one given to :py:func:`reencrypt`. + """ params = PARAMETERS diff --git a/umbral/key_frag.py b/umbral/key_frag.py index 6d0d8cf7..de8fbf39 100644 --- a/umbral/key_frag.py +++ b/umbral/key_frag.py @@ -126,6 +126,9 @@ def poly_eval(coeffs: List[CurveScalar], x: CurveScalar) -> CurveScalar: class KeyFrag(Serializable): + """ + A signed fragment of the delegating key. + """ def __init__(self, id_: KeyFragID, @@ -195,6 +198,12 @@ def verify(self, delegating_pk: Optional[PublicKey] = None, receiving_pk: Optional[PublicKey] = None, ) -> bool: + """ + Verifies the validity of this fragment. + + If the delegating and/or receiving key were not signed in :py:func:`generate_kfrags`, + but are given to this function, they are ignored. + """ u = PARAMETERS.u @@ -280,6 +289,12 @@ def generate_kfrags(delegating_sk: SecretKey, sign_delegating_key: bool = True, sign_receiving_key: bool = True, ) -> List[KeyFrag]: + """ + Generates ``num_kfrags`` key fragments to pass to proxies for re-encryption. + At least ``threshold`` of them will be needed for decryption. + If ``sign_delegating_key`` or ``sign_receiving_key`` are ``True``, + the corresponding keys will have to be provided to :py:meth:`KeyFrag.verify`. + """ base = KeyFragBase(delegating_sk, receiving_pk, signing_sk, threshold) diff --git a/umbral/keys.py b/umbral/keys.py index 0badfa1f..aea9cc76 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -17,6 +17,9 @@ class SecretKey(Serializable): + """ + Umbral secret (private) key. + """ __SERIALIZATION_INFO = b"SECRET_KEY" @@ -26,7 +29,7 @@ def __init__(self, scalar_key: CurveScalar): @classmethod def random(cls) -> 'SecretKey': """ - Generates a secret key and returns it. + Generates a random secret key and returns it. """ return cls(CurveScalar.random_nonzero()) @@ -74,8 +77,6 @@ def sign_digest(self, digest: 'Hash') -> 'Signature': class Signature(Serializable): """ Wrapper for ECDSA signatures. - We store signatures as r and s; this class allows interoperation - between (r, s) and DER formatting. """ def __init__(self, r: CurveScalar, s: CurveScalar): @@ -114,6 +115,9 @@ def __eq__(self, other): class PublicKey(Serializable): + """ + Umbral public key. + """ def __init__(self, point_key: CurvePoint): self._point_key = point_key @@ -123,6 +127,9 @@ def point(self): @classmethod def from_secret_key(cls, sk: SecretKey) -> 'PublicKey': + """ + Creates the public key corresponding to the given secret key. + """ return cls(CurvePoint.generator() * sk.secret_scalar()) @classmethod @@ -145,8 +152,9 @@ def __hash__(self) -> int: class SecretKeyFactory(Serializable): """ - This class handles keying material for Umbral, by allowing deterministic - derivation of SecretKeys based on labels. + This class handles keyring material for Umbral, by allowing deterministic + derivation of :py:class:`SecretKey` objects based on labels. + Don't use this key material directly as a key. """ @@ -158,9 +166,15 @@ def __init__(self, key_seed: bytes): @classmethod def random(cls) -> 'SecretKeyFactory': + """ + Creates a random factory. + """ return cls(os.urandom(cls._KEY_SEED_SIZE)) def secret_key_by_label(self, label: bytes) -> SecretKey: + """ + Creates a :py:class:`SecretKey` from the given label. + """ tag = b"KEY_DERIVATION/" + label key = kdf(self.__key_seed, self._DERIVED_KEY_SIZE, info=tag) diff --git a/umbral/pre.py b/umbral/pre.py index 20eb4dc4..b96fb397 100644 --- a/umbral/pre.py +++ b/umbral/pre.py @@ -9,8 +9,7 @@ def encrypt(pk: PublicKey, plaintext: bytes) -> Tuple[Capsule, bytes]: """ - Performs an encryption using the UmbralDEM object and encapsulates a key - for the sender using the public key provided. + Generates and encapsulates a symmetric key and uses it to encrypt the given plaintext. Returns the KEM Capsule and the ciphertext. """ @@ -32,6 +31,13 @@ def decrypt_original(sk: SecretKey, capsule: Capsule, ciphertext: bytes) -> byte def reencrypt(capsule: Capsule, kfrag: KeyFrag, metadata: Optional[bytes] = None) -> CapsuleFrag: + """ + Creates a capsule fragment using the given key fragment. + Capsule fragments can later be used to decrypt the ciphertext. + + If `metadata` is provided, it will have to be used for verification in + :py:meth:`CapsuleFrag.verify`. + """ return CapsuleFrag.reencrypted(capsule, kfrag, metadata) @@ -41,6 +47,9 @@ def decrypt_reencrypted(decrypting_sk: SecretKey, cfrags: Sequence[CapsuleFrag], ciphertext: bytes, ) -> bytes: + """ + Decrypts the ciphertext using the original capsule and the reencrypted capsule fragments. + """ key_seed = capsule.open_reencrypted(decrypting_sk, delegating_pk, cfrags) # TODO: add salt and info here? diff --git a/umbral/serializable.py b/umbral/serializable.py index dde3d14a..27968fde 100644 --- a/umbral/serializable.py +++ b/umbral/serializable.py @@ -3,11 +3,17 @@ class Serializable(ABC): + """ + A mixin for composable serialization. + """ _T = TypeVar('_T', bound='Serializable') @classmethod def from_bytes(cls: Type[_T], data: bytes) -> _T: + """ + Restores the object from serialized bytes. + """ obj, remainder = cls.__take__(data) if len(remainder) != 0: raise ValueError(f"{len(remainder)} bytes remaining after deserializing {cls}") @@ -15,12 +21,19 @@ def from_bytes(cls: Type[_T], data: bytes) -> _T: @classmethod def __take_bytes__(cls, data: bytes, size: int) -> Tuple[bytes, bytes]: + """ + Takes ``size`` bytes from the bytestring and returns them along with the remainder. + """ if len(data) < size: raise ValueError(f"{cls} cannot take {size} bytes from a bytestring of size {len(data)}") return data[:size], data[size:] @classmethod def __take_types__(cls, data: bytes, *types: Type) -> Tuple[List[Any], bytes]: + """ + Given a list of ``Serializable`` types, attempts to deserialize them from the bytestring + one by one and returns the list of the resulting objects and the remaining bytestring. + """ objs = [] for tp in types: obj, data = tp.__take__(data) @@ -30,10 +43,19 @@ def __take_types__(cls, data: bytes, *types: Type) -> Tuple[List[Any], bytes]: @classmethod @abstractmethod def __take__(cls: Type[_T], data: bytes) -> Tuple[_T, bytes]: + """ + Take however much is necessary from ``data`` and instantiate the object, + returning it and the remaining bytestring. + + Must be implemented by the derived class. + """ raise NotImplementedError @abstractmethod def __bytes__(self): + """ + Serializes the object into bytes. + """ raise NotImplementedError From fe6e32be9dfc081ddcae4b62d74bdec9268096fb Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 20 Mar 2021 20:25:27 -0700 Subject: [PATCH 19/25] Update vector generating script and regenerate vectors --- tests/test_vectors.py | 167 ++++++++++++++++++++++ vectors/generate_test_vectors.py | 156 +++++++++----------- vectors/vectors_cfrags.json | 51 +++---- vectors/vectors_curvebn_hash.json | 96 ------------- vectors/vectors_curvebn_operations.json | 40 ------ vectors/vectors_kfrags.json | 26 ++-- vectors/vectors_point_operations.json | 22 +-- vectors/vectors_scalar_from_digest.json | 92 ++++++++++++ vectors/vectors_scalar_operations.json | 24 ++++ vectors/vectors_unsafe_hash_to_point.json | 66 ++++----- 10 files changed, 435 insertions(+), 305 deletions(-) create mode 100644 tests/test_vectors.py delete mode 100644 vectors/vectors_curvebn_hash.json delete mode 100644 vectors/vectors_curvebn_operations.json create mode 100644 vectors/vectors_scalar_from_digest.json create mode 100644 vectors/vectors_scalar_operations.json diff --git a/tests/test_vectors.py b/tests/test_vectors.py new file mode 100644 index 00000000..967c9359 --- /dev/null +++ b/tests/test_vectors.py @@ -0,0 +1,167 @@ +import json +import os + +from umbral import ( + Capsule, KeyFrag, CapsuleFrag, SecretKey, PublicKey, encrypt, generate_kfrags, reencrypt) +from umbral.curve_scalar import CurveScalar +from umbral.curve_point import CurvePoint +from umbral.hashing import Hash, unsafe_hash_to_point +from umbral.dem import DEM, kdf + + +def test_scalar_operations(): + + vector_file = os.path.join('vectors', 'vectors_scalar_operations.json') + try: + with open(vector_file) as f: + vector_suite = json.load(f) + except OSError: + raise + + bn1 = CurveScalar.from_bytes(bytes.fromhex(vector_suite['first operand'])) + bn2 = CurveScalar.from_bytes(bytes.fromhex(vector_suite['second operand'])) + + expected = dict() + for op_result in vector_suite['vectors']: + result = bytes.fromhex(op_result['result']) + expected[op_result['operation']] = CurveScalar.from_bytes(result) + + test = [('Addition', bn1 + bn2), + ('Subtraction', bn1 - bn2), + ('Multiplication', bn1 * bn2), + ('Inverse', bn1.invert()), + ] + + for (operation, result) in test: + assert result == expected[operation], 'Error in {}'.format(operation) + +def test_scalar_hash(): + + vector_file = os.path.join('vectors', 'vectors_scalar_from_digest.json') + try: + with open(vector_file) as f: + vector_suite = json.load(f) + except OSError: + raise + + for vector in vector_suite['vectors']: + hash_input = [bytes.fromhex(item['bytes']) for item in vector['input']] + expected = CurveScalar.from_bytes(bytes.fromhex(vector['output'])) + + digest = Hash(b'some_dst') + for input_ in hash_input: + digest.update(input_) + scalar = CurveScalar.from_digest(digest) + assert scalar == expected + + +def test_point_operations(): + + vector_file = os.path.join('vectors', 'vectors_point_operations.json') + try: + with open(vector_file) as f: + vector_suite = json.load(f) + except OSError: + raise + + point1 = CurvePoint.from_bytes(bytes.fromhex(vector_suite['first CurvePoint operand'])) + point2 = CurvePoint.from_bytes(bytes.fromhex(vector_suite['second CurvePoint operand'])) + bn1 = CurveScalar.from_bytes(bytes.fromhex(vector_suite['CurveScalar operand'])) + + expected = dict() + for op_result in vector_suite['vectors']: + expected[op_result['operation']] = bytes.fromhex(op_result['result']) + + test = [('Addition', point1 + point2), + ('Subtraction', point1 - point2), + ('Multiplication', point1 * bn1), + ('Inversion', -point1), + ] + + for (operation, result) in test: + assert result == CurvePoint.from_bytes(expected[operation]), 'Error in {}'.format(operation) + + test = [('To_affine.X', point1.to_affine()[0]), + ('To_affine.Y', point1.to_affine()[1]), + ] + + for (operation, result) in test: + assert result == int.from_bytes(expected[operation], 'big'), 'Error in {}'.format(operation) + + assert kdf(bytes(point1), DEM.KEY_SIZE) == expected['kdf'] + + +def test_unsafe_hash_to_point(): + + vector_file = os.path.join('vectors', 'vectors_unsafe_hash_to_point.json') + try: + with open(vector_file) as f: + vector_suite = json.load(f) + except OSError: + raise + + for item in vector_suite['vectors']: + data = bytes.fromhex(item['data']) + dst = bytes.fromhex(item['dst']) + expected = CurvePoint.from_bytes(bytes.fromhex(item['point'])) + assert expected == unsafe_hash_to_point(dst=dst, data=data) + + +def test_kfrags(): + + vector_file = os.path.join('vectors', 'vectors_kfrags.json') + try: + with open(vector_file) as f: + vector_suite = json.load(f) + except OSError: + raise + + verifying_pk = PublicKey.from_bytes(bytes.fromhex(vector_suite['verifying_pk'])) + delegating_pk = PublicKey.from_bytes(bytes.fromhex(vector_suite['delegating_pk'])) + receiving_pk = PublicKey.from_bytes(bytes.fromhex(vector_suite['receiving_pk'])) + + for json_kfrag in vector_suite['vectors']: + kfrag = KeyFrag.from_bytes(bytes.fromhex(json_kfrag['kfrag'])) + assert kfrag.verify(signing_pk=verifying_pk, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk), \ + 'Invalid KeyFrag {}'.format(bytes(kfrag).hex()) + + +def test_cfrags(): + + vector_file = os.path.join('vectors', 'vectors_cfrags.json') + try: + with open(vector_file) as f: + vector_suite = json.load(f) + except OSError: + raise + + capsule = Capsule.from_bytes(bytes.fromhex(vector_suite['capsule'])) + + verifying_pk = PublicKey.from_bytes(bytes.fromhex(vector_suite['verifying_pk'])) + delegating_pk = PublicKey.from_bytes(bytes.fromhex(vector_suite['delegating_pk'])) + receiving_pk = PublicKey.from_bytes(bytes.fromhex(vector_suite['receiving_pk'])) + + kfrags_n_cfrags = [(KeyFrag.from_bytes(bytes.fromhex(json_kfrag['kfrag'])), + CapsuleFrag.from_bytes(bytes.fromhex(json_kfrag['cfrag']))) + for json_kfrag in vector_suite['vectors']] + + metadata = bytes.fromhex(vector_suite['metadata']) + + for kfrag, cfrag in kfrags_n_cfrags: + assert kfrag.verify(signing_pk=verifying_pk, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk), \ + 'Invalid KeyFrag {}'.format(bytes(kfrag.to_bytes).hex()) + + new_cfrag = reencrypt(capsule, kfrag, metadata=metadata) + assert new_cfrag.point_e1 == cfrag.point_e1 + assert new_cfrag.point_v1 == cfrag.point_v1 + assert new_cfrag.kfrag_id == cfrag.kfrag_id + assert new_cfrag.precursor == cfrag.precursor + assert new_cfrag.verify(capsule, + signing_pk=verifying_pk, + delegating_pk=delegating_pk, + receiving_pk=receiving_pk, + metadata=metadata) diff --git a/vectors/generate_test_vectors.py b/vectors/generate_test_vectors.py index eb22071e..434cfb04 100644 --- a/vectors/generate_test_vectors.py +++ b/vectors/generate_test_vectors.py @@ -1,13 +1,11 @@ import json import os -from umbral import pre -from umbral.keys import UmbralPrivateKey -from umbral.signing import Signer -from umbral.curvebn import CurveBN -from umbral.point import Point -from umbral.random_oracles import hash_to_curvebn, unsafe_hash_to_point, kdf -from umbral.config import set_default_curve, default_params +from umbral import SecretKey, PublicKey, encrypt, generate_kfrags, reencrypt +from umbral.curve_scalar import CurveScalar +from umbral.curve_point import CurvePoint +from umbral.hashing import Hash, unsafe_hash_to_point +from umbral.dem import DEM, kdf ####################### @@ -35,90 +33,77 @@ def create_test_vector_file(vector, filename, generate_again=False): # If True, this will overwrite existing test vector files with new randomly generated instances -generate_again = False +generate_again = True ######### # SETUP # ######### -set_default_curve() -params = default_params() -curve = params.curve # We create also some Umbral objects for later -delegating_privkey = UmbralPrivateKey.gen_key(params=params) -receiving_privkey = UmbralPrivateKey.gen_key(params=params) -signing_privkey = UmbralPrivateKey.gen_key(params=params) +delegating_sk = SecretKey.random() +receiving_sk = SecretKey.random() +signing_sk = SecretKey.random() -verifying_key = signing_privkey.get_pubkey() -delegating_key = delegating_privkey.get_pubkey() -receiving_key = receiving_privkey.get_pubkey() +verifying_pk = PublicKey.from_secret_key(signing_sk) +delegating_pk = PublicKey.from_secret_key(delegating_sk) +receiving_pk = PublicKey.from_secret_key(receiving_sk) -signer = Signer(signing_privkey) - -kfrags = pre.generate_kfrags(delegating_privkey=delegating_privkey, - receiving_pubkey=receiving_key, - threshold=6, - N=10, - signer=signer, - ) +kfrags = generate_kfrags(delegating_sk=delegating_sk, + receiving_pk=receiving_pk, + signing_sk=signing_sk, + threshold=6, + num_kfrags=10, + ) plain_data = b'peace at dawn' -ciphertext, capsule = pre.encrypt(delegating_key, plain_data) - -capsule.set_correctness_keys(delegating=delegating_key, - receiving=receiving_key, - verifying=verifying_key) +capsule, ciphertext = encrypt(delegating_pk, plain_data) -cfrag = pre.reencrypt(kfrags[0], capsule) +cfrag = reencrypt(capsule, kfrags[0]) points = [capsule.point_e, cfrag.point_e1, cfrag.proof.point_e2, capsule.point_v, cfrag.point_v1, cfrag.proof.point_v2, - capsule.params.u, cfrag.proof.point_kfrag_commitment, cfrag.proof.point_kfrag_pok] + cfrag.proof.kfrag_commitment, cfrag.proof.kfrag_pok] -z = cfrag.proof.bn_sig +z = cfrag.proof.signature -####################### -# CurveBN arithmetics # -####################### +########################### +# CurveScalar arithmetics # +########################### -# Let's generate two random CurveBNs -bn1 = CurveBN.gen_rand(curve) -bn2 = CurveBN.gen_rand(curve) +# Let's generate two random CurveScalars +bn1 = CurveScalar.random_nonzero() +bn2 = CurveScalar.random_nonzero() # Expected results for some binary operations expected = [('Addition', bn1 + bn2), ('Subtraction', bn1 - bn2), ('Multiplication', bn1 * bn2), - ('Division', bn1 / bn2), - ('Pow', bn1 ** bn2), - ('Mod', bn1 % bn2), - ('Inverse', ~bn1), - ('Neg', -bn1), + ('Inverse', bn1.invert()), ] expected = [{'operation': op, 'result': hexlify(result)} for (op, result) in expected] # Definition of test vector vector_suite = { - 'name': 'Test vectors for CurveBN operations', + 'name': 'Test vectors for CurveScalar operations', 'params': 'default', 'first operand': hexlify(bn1), 'second operand': hexlify(bn2), 'vectors': expected } -json_file = 'vectors_curvebn_operations.json' +json_file = 'vectors_scalar_operations.json' create_test_vector_file(vector_suite, json_file, generate_again=generate_again) -################### -# hash_to_curvebn # -################### +############################### +# CurveScalar.from_digest() # +############################### -# Test vectors for different kinds of inputs (bytes, Points, CurveBNs, etc.) +# Test vectors for different kinds of inputs (bytes, CurvePoints, CurveScalars, etc.) inputs = ([b''], [b'abc'], [capsule.point_e], @@ -129,51 +114,54 @@ def create_test_vector_file(vector, filename, generate_again=False): vectors = list() for input_to_hash in inputs: - bn_output = hash_to_curvebn(*input_to_hash, params=params) + digest = Hash(b'some_dst') + for input_ in input_to_hash: + digest.update(input_) + scalar = CurveScalar.from_digest(digest) json_input = [{'class': data.__class__.__name__, 'bytes': hexlify(data), } for data in input_to_hash] - json_input = {'input': json_input, 'output': hexlify(bn_output) } + json_input = {'input': json_input, 'output': hexlify(scalar) } vectors.append(json_input) vector_suite = { - 'name' : 'Test vectors for umbral.curvebn.CurveBN.hash()', + 'name' : 'Test vectors for umbral.curvebn.CurveScalar.from_digest()', 'params' : 'default', 'vectors' : vectors } -create_test_vector_file(vector_suite, 'vectors_curvebn_hash.json', generate_again=generate_again) +create_test_vector_file(vector_suite, 'vectors_scalar_from_digest.json', generate_again=generate_again) #print(json.dumps(vector_suite, indent=2)) -########## -# Points # -########## +############### +# CurvePoints # +############### -point1 = Point.gen_rand(curve) -point2 = Point.gen_rand(curve) +point1 = CurvePoint.random() +point2 = CurvePoint.random() -# Expected results for some Point operations +# Expected results for some CurvePoint operations expected = [('Addition', point1 + point2), ('Subtraction', point1 - point2), - ('Multiplication', bn1 * point1), + ('Multiplication', point1 * bn1), ('Inversion', -point1), ('To_affine.X', point1.to_affine()[0]), ('To_affine.Y', point1.to_affine()[1]), - ('kdf', kdf(point1, pre.DEM_KEYSIZE)), + ('kdf', kdf(bytes(point1), DEM.KEY_SIZE)), ] expected = [{'operation': op, 'result': hexlify(result)} for (op, result) in expected] # Definition of test vector vector_suite = { - 'name': 'Test vectors for Point operations', + 'name': 'Test vectors for CurvePoint operations', 'params': 'default', - 'first Point operand': hexlify(point1), - 'second Point operand': hexlify(point2), - 'CurveBN operand': hexlify(bn1), + 'first CurvePoint operand': hexlify(point1), + 'second CurvePoint operand': hexlify(point2), + 'CurveScalar operand': hexlify(bn1), 'vectors': expected } @@ -194,17 +182,17 @@ def create_test_vector_file(vector, filename, generate_again=False): vectors = list() for data in inputs: - for label in inputs: - point = unsafe_hash_to_point(label=label, data=data, params=params) + for dst in inputs: + point = unsafe_hash_to_point(dst=dst, data=data) json_input = {'data': hexlify(data), - 'label': hexlify(label), + 'dst': hexlify(dst), 'point': hexlify(point), } vectors.append(json_input) vector_suite = { - 'name': 'Test vectors for umbral.point.Point.unsafe_hash_to_point', + 'name': 'Test vectors for unsafe_hash_to_point()', 'params': 'default', 'vectors': vectors } @@ -219,7 +207,7 @@ def create_test_vector_file(vector, filename, generate_again=False): vectors = list() for kfrag in kfrags: - assert kfrag.verify(verifying_key, delegating_key, receiving_key) + assert kfrag.verify(verifying_pk, delegating_pk, receiving_pk) json_input = {'kfrag': hexlify(kfrag)} @@ -232,9 +220,9 @@ def create_test_vector_file(vector, filename, generate_again=False): 'Each of them must deserialize correctly and the ' 'call to verify() must succeed.'), 'params': 'default', - 'verifying_key': hexlify(verifying_key), - 'delegating_key': hexlify(delegating_key), - 'receiving_key': hexlify(receiving_key), + 'verifying_pk': hexlify(verifying_pk), + 'delegating_pk': hexlify(delegating_pk), + 'receiving_pk': hexlify(receiving_pk), 'vectors': vectors } @@ -246,14 +234,11 @@ def create_test_vector_file(vector, filename, generate_again=False): # CFrags # ########## -capsule.set_correctness_keys(delegating=delegating_key, - receiving=receiving_key, - verifying=verifying_key) - vectors = list() +metadata = b'kfrag_metadata' for kfrag in kfrags: - cfrag = pre.reencrypt(kfrag, capsule, provide_proof=False) + cfrag = reencrypt(capsule, kfrag, metadata) json_input = {'kfrag': hexlify(kfrag), 'cfrag': hexlify(cfrag)} vectors.append(json_input) @@ -263,18 +248,15 @@ def create_test_vector_file(vector, filename, generate_again=False): 'enclosed Capsule, under the enclosed delegating, ' 'verifying and receiving keys. Each CFrag must deserialize ' 'correctly and can be replicated with a call to ' - '`pre.reencrypt(kfrag, capsule, provide_proof=False)`'), + '`reencrypt(kfrag, capsule, , b\'kfrag_metadata\')`'), 'params': 'default', 'capsule': hexlify(capsule), - 'verifying_key': hexlify(verifying_key), - 'delegating_key': hexlify(delegating_key), - 'receiving_key': hexlify(receiving_key), + 'metadata': hexlify(metadata), + 'verifying_pk': hexlify(verifying_pk), + 'delegating_pk': hexlify(delegating_pk), + 'receiving_pk': hexlify(receiving_pk), 'vectors': vectors } #print(json.dumps(vector_suite, indent=2)) create_test_vector_file(vector_suite, 'vectors_cfrags.json', generate_again=generate_again) - - - - diff --git a/vectors/vectors_cfrags.json b/vectors/vectors_cfrags.json index 5da3fc6b..996d045d 100644 --- a/vectors/vectors_cfrags.json +++ b/vectors/vectors_cfrags.json @@ -1,51 +1,52 @@ { "name": "Test vectors for CFrags", - "description": "This is a collection of CFrags, originated from the enclosed Capsule, under the enclosed delegating, verifying and receiving keys. Each CFrag must deserialize correctly and can be replicated with a call to `pre.reencrypt(kfrag, capsule, provide_proof=False)`", + "description": "This is a collection of CFrags, originated from the enclosed Capsule, under the enclosed delegating, verifying and receiving keys. Each CFrag must deserialize correctly and can be replicated with a call to `reencrypt(kfrag, capsule, , b'kfrag_metadata')`", "params": "default", - "capsule": "03cdaadd5e0493c8426fd65004ea1e10ddcd3fcb37ac87ce84d0ffdda36d8d60720278adf9ca33ea46b2c0fb7b5a14a2820b171a97f8c69846e9866194ed1315438a1a525f615e246d744b82f7283aeba923f38cff0181e9013cfefe4747e32dc076", - "verifying_key": "03714109aee5bded1ea98ced8660ad1d49697e14f2a1bbefd5944e4ce443493d08", - "delegating_key": "021c0e2868064f20441df0050bf501137ec84db6ca3596b88a967308e14bea2caa", - "receiving_key": "036285c48fbb7c38bccceb6e15959a33ed3dd5ab5c96daaff67636b8557d6885d4", + "capsule": "02a03893438c0502dd13818f65c039b2ef4fce33bfed150c6ad166554b4e8a51c2028da9ebf8cc0966bc010152fd3917a8f12dfff0af3b06e34e17f300d622893159367ce07602310c78fc65a8b6115d75f65d362abdf2fd799bebc55aa0cda44dbb", + "metadata": "6b667261675f6d65746164617461", + "verifying_pk": "03f24761ac8b02de08ad1622d023f669d6214c3bab81a33087ed3ec5505e4d43db", + "delegating_pk": "03a73623a2e72fd52b2d313214c7495580c14fe6cd8de7ad0d63bbfbfd6fb6bd4d", + "receiving_pk": "02952a1903b9c929f0d93d935b34b272ea25a84833a04e22d887f27bc3bf0cc409", "vectors": [ { - "kfrag": "417225b396fb91a82f0ed97b4dfb235200742a58d9632d46e352e507c5c716863b349c46905501861f950d11aefd2c69a1f4fe31ae91b4c8624c7556ec7d01ef026fa5450221297328a37043220b0f82a7fdf2a4ab01c9b30cd572181736390ad303b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103428244169c9d2d3cfefa6b88251e7600c4a6f9a4edb5e371a0af7bc5134bf68ce9ad3b8a1acbd66926687e2cf7a30762527a85d41b1eaee133866df9dc4fe49b43601bb4d9b496f59e44e2e172141f9480bad0cfda18e5dd6c8dd7a5e72b4bc59281ed11ba75b87f7f1919c30b56d7f0c522b77fd15d37f893fc784eb77d1493", - "cfrag": "02aa6a41b809ba8d2816444788d597e6af5b81c56f7645f67e8dde417fca052828027adba799a3cf3935abb5366b5ec3f132f05a9baa0647734e7be50952e3309bd2417225b396fb91a82f0ed97b4dfb235200742a58d9632d46e352e507c5c7168603b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "3f3453856117dedb3d7518d0435c04b10734700c8aca48fa5a8b85eded515cdffefbefddcd3cb728dcc6d05061b5e201aa1cef55ce89e94c34b6887b307d5ab2025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb7490374d2e59ea274c8011f6bf26ca8fe8eb8e7837cafed8547485e3fde6ffe0368e98d26b0d7e12095941daffae8ca429147d9686dc3285ea182c0d7b15db41c7afd3fae3c88100707ef28b53d7cf93961dc509864eb319f0cce274544306c96099b5792af0b820bf758f5c8f2a69cfc1e0a5f91fab96ab82c10c7740602f90efdd315ff5dbd07323526ca1c9a93132b4d008fdf1796a55b92e2fe75c544652256c80101", + "cfrag": "02ecbc4dc0aed60efa211c7bf0e238593d292a042373b891928bcee459151c2f440281171b7e330ebd097575dadb210e8a405bc162e293881457a301da03f7571c7a3f3453856117dedb3d7518d0435c04b10734700c8aca48fa5a8b85eded515cdf025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902889960dbc86c87e5a1275f2c1df31e43ed0b3e4616126845a7394b27a83f6d1e03f0b7c60a76c7c9f590c9ac2d1f0ebf6aecaa9d371e2dcd04e19caa134e9c1b530374d2e59ea274c8011f6bf26ca8fe8eb8e7837cafed8547485e3fde6ffe0368e902660d6c64d749050f027983ebd4631838373a47c887537bea95aa139d53bfe38ea532a6b7464b0b5f45522c495a2b8a772b98c4950aa2bb830f6602e7636a1bd95792af0b820bf758f5c8f2a69cfc1e0a5f91fab96ab82c10c7740602f90efdd315ff5dbd07323526ca1c9a93132b4d008fdf1796a55b92e2fe75c544652256c8" }, { - "kfrag": "fe11bce6ea1d451fbe396c6c7d00747264324eebc93bd6ca9e949c2970be214e56571ecd1c02064be2058043cbe5d7ddde90771c5709c1cb48f2a4866bef246b03cb7014d5978aba65b7849bc5756d0e94ce515cc31725ed86550e4e38424b63d903b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103adbaf6ef97a68ad61ca39b1ab244b5d1881b00fbf7e2f0eec06907a0078ec4fcb573d101c2d1976985fa7d6d7c96b327030333ac3c66fc0223b456a70e996e19a31822dea4781891b054e86ff34abbfcce05f7c613b7296133ad5b10455061368a102dff42a913edb8e5d248a8d979879a3cfca1f8d3fbf6b8a24ca5357ae057", - "cfrag": "03bfcdf5dafbc9482fab6da942ed7849869d92333a815e9e81116eeff31a97ac680243d1d352ea857eef383300d41f1e1df14d00beb54abd43bc112c2d8af98e4be7fe11bce6ea1d451fbe396c6c7d00747264324eebc93bd6ca9e949c2970be214e03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "d15ac028be9938b5e2d7a9fb9957b416db538a11d141d320a5f656ab4e3cb0cab3a2ddd2672f3514e7da2f29cbe3815c83b6d704e09d595a68fdd5aeff52ef8a025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749033d6984555d41614a2ba08c2182e27a7105cb60ed414c9732a156c3e44d196dd3507335e3781d6c9a4543e3ae81a4c7538cf280292c9c0d92e4f513e4e073721f2f72a1ea0e69912e21754e32edf9cefd3a96183f5501266bedb301f291709ffb25ed5213660db4c8bfc086e794650455bfde1251f92e1fe49f1feb016ad44fe859b342964db14dddfa6a33eba53021149d8663d306850b8907f9dabf4a5d3ecd0101", + "cfrag": "03a98cc0d807a4f2b6183dfb9b7ff589f376183b33f88ae07a50323edaac946d9b02fc04f97b7afa5c8664082e4433a24184d54724e9e18afd44922bd052bea87e49d15ac028be9938b5e2d7a9fb9957b416db538a11d141d320a5f656ab4e3cb0ca025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903bf5c856a6b918df7a70a10db9854d43009ee517e7fc4403185c99eb05f42638c0213c86d5b3f3816a4807bf797731b8b23ef2563e6698e9380a0ee4044755504a9033d6984555d41614a2ba08c2182e27a7105cb60ed414c9732a156c3e44d196dd303de157a0d01a46d89e57d5586bee352b17bb123ea52dea60ef283456fa50cb5a98eca7141ac4bd117511c10922216c69e548c5005e49571087fefb69d50cecc8225ed5213660db4c8bfc086e794650455bfde1251f92e1fe49f1feb016ad44fe859b342964db14dddfa6a33eba53021149d8663d306850b8907f9dabf4a5d3ecd" }, { - "kfrag": "4edffcacb49f5620c1cebd67f43366aa22dc380dd9fd0f4853ffbbd44e31986f4a99f953e5c553f08a83cb16c0b5c793a4f691d4d3d239e29f232d2b058970d903041b77b8891d845c240a8a12b71dfe9651de67ceb817b54eb578636fe8a9605103b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41035ba90e2a06117e205dd807c780b5687ff6dadf19454fffb367e7af6b3f9cadf03572dc3311c0e1706354ba3340d1b1f915afc82007f9e09ddd34ce07a038a7bb7ecc7343b1532b175e5b8c7d3c712e3eecb18ff69c22237ba9ab6b47b4572529a35b29c48874f396def9971c0230a51e26975a953ff87af8fdbfd53bf42aae07", - "cfrag": "02c6870bd1336ec336752ce7d6b99f36d3ae69983d7a06cb5a743f20c562c0b2ba037fb7d9db1faf763c49e257f906e3345e33df144eeba8b9794462a31a305459c44edffcacb49f5620c1cebd67f43366aa22dc380dd9fd0f4853ffbbd44e31986f03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "f780e25b78581e70d28f102264e1f04b482eb9ecd45c188f2f9cd90026627f904b903c6da7529805e1d91fbaebc384588083e2c5f235c31e7f768b8d0df43513025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749032f4d57c068c255281703ba6d449345e2f6240a4b8b3047209ff2aca5edec0ad2394020e796b7b9f9130c0ab02b4c8151d050f38f3d0ff5f38dc164e4d254ad2737758c4b67c0b54267b113692f11a410ffc5fe3e41c2f8ec9246854baa63dbfaebb58f0cc89c528e119788c521bbfa8dd33c69129c1a05ed454d7534224c9df35007ef11b38769ee8d2788c72745e50e14e32b0c2b9d988e2550dd3f89cbd6b50101", + "cfrag": "03ec5772868ec577fb8e054a7f0717dc56b7eded542a06c593ac7166093ae196c10394fc79d3d700a9536b10233ad1cf145a533a43d35c48028c7b2a6137a8d6097ff780e25b78581e70d28f102264e1f04b482eb9ecd45c188f2f9cd90026627f90025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903ad08b8333f7a44acf71a2738e7e6213d715d7cee759a825abce04b72ca8d56d0033eba87004940ec899c4eb593265e55138f2d17e8b4c712027009388c18724bd0032f4d57c068c255281703ba6d449345e2f6240a4b8b3047209ff2aca5edec0ad2037403376f5789c4d7c755e4a0b45541a88cd045c77fe61a615e9b6ab6af9e97bbd868593f2b973cd492439c4fdbc6351739e8e9e7f5ba501fb9bbc96605dcf144ebb58f0cc89c528e119788c521bbfa8dd33c69129c1a05ed454d7534224c9df35007ef11b38769ee8d2788c72745e50e14e32b0c2b9d988e2550dd3f89cbd6b5" }, { - "kfrag": "762a4ae9fdbb74de327a29d25c577c46521ee44a951c6e33739e5ceb2a340aefd2a93d8cbe37b9149dd26ce59b0b7ffcd9adf1ca822c5b477595ecba1933291503409c76b74f0f415f5b6a8421514c250f71a5c5b6c5b0d89148e03ba2a624a9a503b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41034f04b1666bc36ac5d3b7ba66420b660d19caf9a995b6cc7cc0aa7c9d0e739f25b1048e754ffd0703e16b453ae8279e5410eb16d2d53683f2d80c30c39918834d70970a09ae17c014fc939979c1e3659b92a40d8688ed77861407316a22d4a8e0b5628c5eec0a2123129ebdb627eae78f8c404f8e129dd407feb2e970b0ba5e39", - "cfrag": "038bbe8c32677553121c7b708c4266e85930ae38541fec43df9b4eb131fcda4b06036ab69b2d77584016302c8391a13bc821811fe8837bd28523ad04081faa6fc498762a4ae9fdbb74de327a29d25c577c46521ee44a951c6e33739e5ceb2a340aef03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "afdbd2183491cec86259c4e6785c9048f17e1f0c86df597983e78f50eb9e88ebfc725b6addb07ce67e5e33e404237dfd9baf15825329d780bbcf07952d899709025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902fbba98644ea41f7abbef7f0a80d645d428844eb6a08c848b73122057f02cc264f27b1b019d667b1a3c89e176b9203870c42dcf3bb6af9191a89db98b2cd1a1b82c61d188f2d2a5d8e1c92a64d3402e98ca09e4594dd67ad74a9583e47b5ee2fd0d38772f8dee198e6e7fa2f7acca079a90b280f98e880106bc337c314bdea20b1e8d1d43ed0912a4a55475c2587ba81a16b9775681774a8bc9d1af2395875e000101", + "cfrag": "02b6b320eb5ef6bb58adc6b0379b3052d7fe3ce290d62d8a9edc5d9560c7472f75036dd5f864310b2f9f7239bca239877edf63b42ac00637fb6cdcdcf944fb0ef2f2afdbd2183491cec86259c4e6785c9048f17e1f0c86df597983e78f50eb9e88eb025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749037b55bdc05a83cf07d672c695d0c57a961be2a7289d9386b6e86953d6abe90f17036e5e32b140cf076873542eb4551c328608c8f2d951a3d52083b7a2f6612c21d902fbba98644ea41f7abbef7f0a80d645d428844eb6a08c848b73122057f02cc264027ddbf0d8eec3118c1765eb0c636184375b5ce593c1fd83bfbf501d94687695954edccdff64dd78e9ae94aa137754c502c03cacbaeeb09b1dcb798bb8442476ba0d38772f8dee198e6e7fa2f7acca079a90b280f98e880106bc337c314bdea20b1e8d1d43ed0912a4a55475c2587ba81a16b9775681774a8bc9d1af2395875e00" }, { - "kfrag": "b40778198920434d132c4d3724a7b7422428c9921bd627710c3764a6c54376f6a03be8af10cb0f1c3b74d132dda5feeb4340a9f6213da859c05b6b57249e54e903e9198cc0e86b880251931ab162130f93242164a5b242015195290ba75b5a6bea03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103f27829a4798466ed68c2c53137e4e658e86faebfabf2556837b60b502a1721de3e073a98974686d1b847c27167f118eeea6a27029775800cb1618b393bca9cc35b341b546c8851395b3b68945b73b01fdbe94a9f6c400b22bfb5e3bebeee95887d888c49ccf99553a2941de1cbd3f845c5725d1b6b26017dea5cf161fadb5fda", - "cfrag": "020604cacae76ef0f2a81a582b2c642accc4bc9d29ea9b172211a3d8225cfc4975026ac1a78becd4be2f09b939b3bdac3901bbeec488ab1013965c40565034586824b40778198920434d132c4d3724a7b7422428c9921bd627710c3764a6c54376f603b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "831f7b7b14181746fcfd7e26e03e3fb63e7a8903acff3dd3c7ad0c2c94550d9abcced3aacb62fa59fd2847a2b18cc6a6f0a6f646e39ec94f05ef186186dfbe10025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749020db8647602d7401b47c556f25540bd45da99e69462ee8eef1b9c26a26a3757c77f90b015143d8516a5bf2ada51a24b8192e1326d38bf47c0f54591619db42c7c122fd0d955f788dfdeee8d762022887216a3de332632d44092f1f06793eb351fd6acaf0f2060ace737a11e1c1cec72ba361ce754c913765113e1695de323225f24c3f0bbf832c9062d004b69e40090930b40d5986eb9ede462e5d044083127450101", + "cfrag": "038a54e78c4dd408df7b242610d5c21bc6a34f7b16af21a059ee8e44eea24337c702d4584beebc883104f375e922aba70ad1449d54f6a835ba6716b36271bfae8e53831f7b7b14181746fcfd7e26e03e3fb63e7a8903acff3dd3c7ad0c2c94550d9a025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903c1d11486753d8d4b60a4e8a890007d021ea00aaed7d608b25475e69364e5f3120304ef459543d9482aa3b48c5605176e87bf55c0bef3f03244bac83446dd84fe3d020db8647602d7401b47c556f25540bd45da99e69462ee8eef1b9c26a26a3757c702182da47b2bef272ed9840eb2cff17f8f62d3e9544cfc699905e40e8ab061d4d17dc4e840d39308e81fae434b30ed124ce08f846a1a9ab73c926a6a222d15657ad6acaf0f2060ace737a11e1c1cec72ba361ce754c913765113e1695de323225f24c3f0bbf832c9062d004b69e40090930b40d5986eb9ede462e5d04408312745" }, { - "kfrag": "bb063b1ae2a2e2c804671a40571b5d9a7746a7b11b98fd1794207daf68e8d408e4ad0acf429b3e97d7d098766cf304fc43454a79d541a8e85cd95ee93b7bfb460242325b9a16de25bca5ce9225091b2d9eec4b98fa627f6c2fec591cfba3561bd303b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41034c2742c62c69c571623a751dc842998cc47a6d33c7e2d30e099bc10d2675fa8bed049ff1474d604eb97e6c1b997dc506bfcefd6aed3838db23fd08f0cebe176fad3a0c5734d986b506145f0770e6689eb1425eb2817d699be1c993d64b26f2dd59d3acf01c5372adabebaa04336ff03219eadf1fd085a6957bd5e90b83919774", - "cfrag": "022edd320a1560725a60c8116810ac0debf2595bdf274510477181e37e1ce9684c02b56d7468d8a29c5034602dd1960d4a09cc19edf11fd7b90b536a0463f465854bbb063b1ae2a2e2c804671a40571b5d9a7746a7b11b98fd1794207daf68e8d40803b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "c63acdf5fb6820df50354e74e65eefacaf75e7e22c780b2b5e2c49a0db9d353e7217ca6a942fce879bd4b596aabd489e0c5af75bd94b72aed37999799e9c8dc9025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902fcaa0a22aabe3b4564972a9368d6a752bc5e359b306cfaff1626b49911158cda599e662db88909f430575f44b3677a8161a5003519e48cf0da635cb20b15f18d632a1bf41eb525b9ee9fd47f196fdd15059d989f9166e5b437cbbd66459e03fad404248f1da8048a5bf9dd9650fbdd50d1afcbadabbcd7a5aca1882b5eeb803443cd4caff8c4b090bc263f56f496aca086bb6f1fedfd5a99e1715ebfe2c6b5010101", + "cfrag": "038caa0566b556fb81617617ee8d4a4808cfaac1fa65b1e1999b58bbefafb30623022a43ae7cd2595e155f3cc7cf5e716168f2b959d574e07f7037ba30329dc384dec63acdf5fb6820df50354e74e65eefacaf75e7e22c780b2b5e2c49a0db9d353e025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903361957fd737be52d9423a400e677264d8e57ffc1ab35f19aff29ac7d13275ece03a1b57ec36f35089242d2586be843853ce830f1fff20cea9e6a354598ec10c1e002fcaa0a22aabe3b4564972a9368d6a752bc5e359b306cfaff1626b49911158cda0300a120a88ee5994de9a4a5287dbb2e63bd1cd3fbca77740ddad0789dc4a40547a792377ec089eadb1771196797c6835cf01bd4307c6c33be4e61acdb6e05f583d404248f1da8048a5bf9dd9650fbdd50d1afcbadabbcd7a5aca1882b5eeb803443cd4caff8c4b090bc263f56f496aca086bb6f1fedfd5a99e1715ebfe2c6b501" }, { - "kfrag": "9a2d97e3bba40307b7c60f6f22c5965f6587206cef4d5d7980ad8d75f0f4ae69de90b3ac071a58a06c4d2d0895db9e71a4f36b86fd078dfce6e436514f70797d03d959ebb3d7ee2fd90f2a390da0d7377f7549877f7720739e7e84e0c488a6d16203b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f410394ba445cfe40a9ebf1bc6a6f9588b704b11b129a2a102e8c5bf892fbb695e2d37d396c1b34f8eed4f348ecb11a29fa2b68c3790ef2ddc169e6fa7dc86e03b1a7fd8faf67773903168ee5f9c2ac60468cd81c7ee389af0f03f3fed356c65e7f182faa22867aa4544554d235f78d28afe7d90fff1cf60184bb1ea947a343c13d07", - "cfrag": "035f87f2fecce4d4734a01d444e3614e2f32be3f9277373f1338ddb0044a4b79ae03a604f360df7cb23097d7b49826088bd3628d82f601f2b4ea9262931c3b4bc5809a2d97e3bba40307b7c60f6f22c5965f6587206cef4d5d7980ad8d75f0f4ae6903b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "1365f76b3cecb0d27cd839263d6e91e8a3e703cb10590fc4ee8c533caed2fcb4e390938ab2eb832d51d7436058182fb46fb4288c9409a2465c25e447919e61eb025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903ffe0345c282a9890aa6759dadd14afdd50a82c19123a110d37f4582a1549ce5d4308de0dbbc76897e910253bd1fdb10e65bbbe45cede7dcbf494eba5dee18f9c141cc095ff275208485abe81b229b730c11a1fd87de00756e3d705faff9e6f508cff3f81aec66079b4737c5d55ada03f7a749da2605e6b1bb57324528c373cac773526c02a0cf876744a5e7cd7dee1bad8ccf1e81a46d4c7049743cabca94dad0101", + "cfrag": "03e39b8d144411b2db704a4394b9291bc8897ce619df95e2a8355066a9da8fe91b02037fe5e553f4e7e711502abe59bff33fae5ed0eee63747252744701ad1c550f31365f76b3cecb0d27cd839263d6e91e8a3e703cb10590fc4ee8c533caed2fcb4025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903a0bf927bc8b75413f899b06ec3bb3ebba8b73f2a5d22c007e157440c2ec6496703be800d325be2e0d10d3cf81d9fe65e035a31d9f4b857b81af38d52d1a815a40b03ffe0345c282a9890aa6759dadd14afdd50a82c19123a110d37f4582a1549ce5d02518eea3159155d5ec2437907ae06a3bf0ece2e8747e13c1528ccffc62eedeebeb50690a4f2f1e5ebcec48789e7d08039ae4853de86a2a2fd991e70ba37e7dc1c8cff3f81aec66079b4737c5d55ada03f7a749da2605e6b1bb57324528c373cac773526c02a0cf876744a5e7cd7dee1bad8ccf1e81a46d4c7049743cabca94dad" }, { - "kfrag": "3d09cbb50ab9ff970c93a03976efaf5f13aef88fc4e1b8840cebad96d0469605b343d7f3d4297ada1fcc08009260250f29dbb64bfb6dd64c2dd72174be3d87a202f665886dabd4fd94513aa1082a403efcb9722115e7c44f8ce5394799e10c2ca203b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41037ec97969ef20834c753f460c1c29360ce535cb19e0dd0da3bc9c568157137c3f741dadb514586112bfa22c4aa64c28ba854f7393a04ce77a3c4b731e6eaa41e29f2d240b3adf5a973688c7691e8bde140647f4d2c4d0c06a0d690415a82f18f095fb69a084607734579a590c6d1e477d93716919c67d6627a890fa5a2250109a", - "cfrag": "0321674813fe3ba72c9164b7016299862ce1efdbf57c7eef83bd4d3f6535bc3eae03940fdcd41dfaeb7667a0d2a3e929944ea6a3c2c6d7e9f2fff4265ffb011169b53d09cbb50ab9ff970c93a03976efaf5f13aef88fc4e1b8840cebad96d046960503b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "d961d60a31b197ee26437b1ff6b484c63bcfd208f509af1225a8db8f65240e366f7924ae66d6cd64c25c74761067bcb3a388cf2d4d2e90738666252868e790b0025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749037c88f9bf80bb6a406843e339dcc8548b3f8bf16e525be9c94d5e3b8c243f466fbb19cf3eec376f9c46d9c52040555927c7eb506af29718ed9b728ac210d43b2b4958eae22852a1e3761a71cedff210a52f5d312e5cc1b12bdbb2edbe3f955f5535b8b1ce9e3e8b04a010a2dd227c9d906d7806fdf9df07d8ce379597bf7513c23db95d84e7774a3fe0de392cd7390c6ec7cab60ad22317d8c00de0865bd0ceb80101", + "cfrag": "03367a497f07271e9b505f58d762527cfd8b0bd30f4b925219b495c8ff825c8cef023b856c54ee517300280f9b9109ee6f8e260341c4013a0b74911fda9eac562b71d961d60a31b197ee26437b1ff6b484c63bcfd208f509af1225a8db8f65240e36025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902d5dd9d1d29e06800648c0b2b45a2755f5a36ed4e7179d17c5ed9506d1d455daf02fe8deea5266b7059e6bdf53cb4f23c3f27814e14f955eb6236ca911639c3d6f8037c88f9bf80bb6a406843e339dcc8548b3f8bf16e525be9c94d5e3b8c243f466f03bdd9ce48dbeafe54277c44c4cf3cb5d5eb10e036640180c0d0e197594f2bebe029f79a6eafcac19331d83eab043df3aa6486a70409c46c2509a4b03ead49666b35b8b1ce9e3e8b04a010a2dd227c9d906d7806fdf9df07d8ce379597bf7513c23db95d84e7774a3fe0de392cd7390c6ec7cab60ad22317d8c00de0865bd0ceb8" }, { - "kfrag": "71c39f563bdda903d8397370b077ba1348e07cf644eaffdfbca251f63d8e0c8377d0f143019bca10090a07c15ab9d6d71857451443e0e77ca6768b45b93892b802b54fd786330796eebd5df0a7d5dc26422177d5637028aaea293dfa223a72a56203b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103aaaf48203b37dc6152c0735493517a31284be4c46b57a21f01359572d1e1e3d1b0fd64fc82efb401f25dab88e48474450b274625c28e16cb51f31259206aba6fe4886083237bdb5f7da67c8e1e2da7fdef57789c919ccceae6b5453eaa22fb6dcb58a2f9f1f20f05d03326031d4817ec52cf3dc9eb37f2d3b014ba315f5b8c16", - "cfrag": "02b37b1fc91e5d99f8249e6c70d19ac9af80cfeab42d2c8b5dbb43b791e5818c480345339e46528e5f7f2d48ab5dd6ba50dc7a44ed7a99a9ef0cee902839076f13a671c39f563bdda903d8397370b077ba1348e07cf644eaffdfbca251f63d8e0c8303b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "8618ade86dd8b8f15c206274caba6955c30171e466e220ef2344af25f3ca5976ea2eb761d4e8d083e4bf30052c90f6fde525716bd19b5abe98d66bdd48745f85025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902bb441051315ea81c8d9d9cc4e63d3f66e6fe3ef52217e8639e0af421d5dbba1be604f5168408cc4bf118346126b9a105b57524b741e141d5cebca15c8964669858bed28994196a23e4f99706cdb8d912d9c53b7c58f98e6d5eb5a4ef3aa2b7b9e0a270af089c667647613c6abe1b7aba8103ee386a04df80341e51e132d6d35d28753fa65e7f8d8c8f3a10d8adc8499a174bcb8041cf6078520ef5e9e00853290101", + "cfrag": "03ae6db34ac0125757de78dc64cb563d32ef5ce3983b67f8053cbb9e3539f5240c03855d0862b9930d7fa36e5c60785352de497b84e6ca6d1712627a35fa49df79688618ade86dd8b8f15c206274caba6955c30171e466e220ef2344af25f3ca5976025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749021594688234f9a0ba6331cf6d341dcea4992d0354492247c528a011620fe7271a03ebb8652e960aa91511db011d16dc3f15df71d81b6e5555a41dc29480158fd8e302bb441051315ea81c8d9d9cc4e63d3f66e6fe3ef52217e8639e0af421d5dbba1b0398e26bb2caa42e1cc1f55f3688bfcaaadc7714a9f79f1910f5c158faa0d53b370668c081a5622181957a2af1fed562695ec390b01b8855411518f401638e185de0a270af089c667647613c6abe1b7aba8103ee386a04df80341e51e132d6d35d28753fa65e7f8d8c8f3a10d8adc8499a174bcb8041cf6078520ef5e9e0085329" }, { - "kfrag": "55faf9c9be6d59da90b2c2e9f09d85e3965e00629b31c3854367f86039725ecb5c560659b6dc7d16a5f3202da0489aa02cbbabb65d8e1107c3645ca7e5bf36c3020e06cdf7d03bafc42e77c86d07b76840ffa7db6a5c511f3530d44b34edb69aba03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103f5674cd594e92cace034b8b443a7aae0ae48ed57ca9eeca5ad6e2855334a989c0ea976c30e4f5eb2527b01caa468f150db4a19cae08ba6015df41fc07d4ea6bb33433a543608fd59cacba037daecf6e069f0954ce9a5f1651297c28cf31fdf76b655ed10284f5bb436c93934bb89b2434b7ff9f7bcb414b3e879a5571c88a0b1", - "cfrag": "03a48d0c6578a259efdebdf0b12ab6e1d29b472a520a1f14ecf628eb161a386ce2021ab6cafd3c7778bef9c199911306939ed679e7b4c09fbbd02c897f994417a81f55faf9c9be6d59da90b2c2e9f09d85e3965e00629b31c3854367f86039725ecb03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41" + "kfrag": "939cfdfc368ae2c011ade7b7d18543f66f6b2f5aea0ed6732830d8ab281ec492cd90cd1756a44414e51d741e99f9a7d46cbedbfc7bdb60f6b8d33ffb1d4d6b31025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749020d6a191832eed71367b495a514ee4ac442ce19040c00c4e6ab331f844b2fff11d1db98034e5ff8f86a5b2d015cfa439774dd83644adb80bcba1881f09bdb1b105e37329495d03775e99da0f1c99e753e6868fbe04766572a086ce8e1c575e0d253698578672fbfdcdf2607513d0d0c03f346cb8183506d179e9b999988c7ca317c10e8606e3f7a18ac9146dcc2f484dc6c46a149c371c7a22b17c14f0e3e84220101", + "cfrag": "03b45ef82c8e3439a698e3cc4383cd223d6ae84a6c1c7e6ea6b1e89fb501273ef1024e2b1a83a2e675e7f3e4f1d803f456dc83d6536bc33c07b01fc521380cec39a4939cfdfc368ae2c011ade7b7d18543f66f6b2f5aea0ed6732830d8ab281ec492025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903893bc5023c9da3e0acf0028256ae37fe1015e09b8fcaea89b3aff10a49b1ac5d03c295e648f0dc3df65314ce141ff21b04fcf53432300ff8994ba765d8205bd90d020d6a191832eed71367b495a514ee4ac442ce19040c00c4e6ab331f844b2fff110390d4511e87f7dee7091cbffb952da38c7efbf1ee5a6890ea8afcfa22ee425c0cecd183c8351af7f917ccaa9bd7771de0ef9cf240bc02a7dc32c10e0bd4ed725653698578672fbfdcdf2607513d0d0c03f346cb8183506d179e9b999988c7ca317c10e8606e3f7a18ac9146dcc2f484dc6c46a149c371c7a22b17c14f0e3e8422" } ] } \ No newline at end of file diff --git a/vectors/vectors_curvebn_hash.json b/vectors/vectors_curvebn_hash.json deleted file mode 100644 index 2a8b5f4b..00000000 --- a/vectors/vectors_curvebn_hash.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "Test vectors for umbral.curvebn.CurveBN.hash()", - "params": "default", - "vectors": [ - { - "input": [ - { - "class": "bytes", - "bytes": "" - } - ], - "output": "fd307d78e9f94e1b76762b8efd1284067c4ddd49f64f95441d57784d05a6323d" - }, - { - "input": [ - { - "class": "bytes", - "bytes": "616263" - } - ], - "output": "26249427a90ff70e82597a97a80f62fd133038a1ddae58b446eddf1a08098da3" - }, - { - "input": [ - { - "class": "Point", - "bytes": "03cdaadd5e0493c8426fd65004ea1e10ddcd3fcb37ac87ce84d0ffdda36d8d6072" - } - ], - "output": "b8070774566dbc192f9aeb4e34eb94b39bacdee67f7c373ca1371dd4b8b1ab41" - }, - { - "input": [ - { - "class": "CurveBN", - "bytes": "b32e9abad43a3724ff37dd9d15253fe66339f978e7581dff5b34b4db970b7bf3" - } - ], - "output": "7d1978b2203d57abb5fe3383d7929ff8aec7b2ea08bc7f4120d12843792ceadb" - }, - { - "input": [ - { - "class": "Point", - "bytes": "03cdaadd5e0493c8426fd65004ea1e10ddcd3fcb37ac87ce84d0ffdda36d8d6072" - }, - { - "class": "CurveBN", - "bytes": "b32e9abad43a3724ff37dd9d15253fe66339f978e7581dff5b34b4db970b7bf3" - } - ], - "output": "57d604fa36e3acbe841c5367f29393868c57290fa7f411b1b9fbdbfe1c320c4f" - }, - { - "input": [ - { - "class": "Point", - "bytes": "03cdaadd5e0493c8426fd65004ea1e10ddcd3fcb37ac87ce84d0ffdda36d8d6072" - }, - { - "class": "Point", - "bytes": "02aa6a41b809ba8d2816444788d597e6af5b81c56f7645f67e8dde417fca052828" - }, - { - "class": "Point", - "bytes": "0336877b38451dd8baaa4830794091a38b7abb2c8187e1062cc2512e202739d5eb" - }, - { - "class": "Point", - "bytes": "0278adf9ca33ea46b2c0fb7b5a14a2820b171a97f8c69846e9866194ed1315438a" - }, - { - "class": "Point", - "bytes": "027adba799a3cf3935abb5366b5ec3f132f05a9baa0647734e7be50952e3309bd2" - }, - { - "class": "Point", - "bytes": "03ee94ece91a0091e3f2c132f348716b0ab01d4ec320ad408ff9308e092dfe1e1b" - }, - { - "class": "Point", - "bytes": "0203c98795773ff1c241fc0b1cced85e80f8366581dda5c9452175ebd41385fa1f" - }, - { - "class": "Point", - "bytes": "026fa5450221297328a37043220b0f82a7fdf2a4ab01c9b30cd572181736390ad3" - }, - { - "class": "Point", - "bytes": "038a295338304923b70a233778a1b93f869062a5d1663127a6e5a5a5b07739f21d" - } - ], - "output": "c384b9566bc17b5ae424862116c89f882374c5bb2ec2c5c54bbf7b4e2cc8e179" - } - ] -} \ No newline at end of file diff --git a/vectors/vectors_curvebn_operations.json b/vectors/vectors_curvebn_operations.json deleted file mode 100644 index f0e1660a..00000000 --- a/vectors/vectors_curvebn_operations.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "Test vectors for CurveBN operations", - "params": "default", - "first operand": "42bd8598f460a2334e921feaa58a64700bdd5eeb4a6d5f59f7363f5d167a2830", - "second operand": "cead0bb7e8b078069664f7a4ec81945b9c3b2233a39097f0310670c29e1e34fc", - "vectors": [ - { - "operation": "Addition", - "result": "116a9150dd111a39e4f7178f920bf8cced69a4383eb5570e686a5192e4621beb" - }, - { - "operation": "Subtraction", - "result": "741079e10bb02a2cb82d2845b908d0132a51199e562567a586022d2748923475" - }, - { - "operation": "Multiplication", - "result": "14ae3ce9b69ed69bbd9fe81e50c9482afc72651b5a5f69b40e8b9235d6fd459b" - }, - { - "operation": "Division", - "result": "1ef7189cfb9918d4fdde0298e811631ccb147036fdc2557bcc6b81db7e3e7222" - }, - { - "operation": "Pow", - "result": "f558c003bc71493f8ebbe39b42316c514ccc610437f1b4f37c73f81fe903dad2" - }, - { - "operation": "Mod", - "result": "42bd8598f460a2334e921feaa58a64700bdd5eeb4a6d5f59f7363f5d167a2830" - }, - { - "operation": "Inverse", - "result": "6c6640d846d233a6c705c1c2c06bb1372d95bca81ea8cdbb6840ccb9a415dc3d" - }, - { - "operation": "Neg", - "result": "bd427a670b9f5dccb16de0155a759b8eaed17dfb64db40e1c89c1f2fb9bc1911" - } - ] -} \ No newline at end of file diff --git a/vectors/vectors_kfrags.json b/vectors/vectors_kfrags.json index 33b0eae4..e1f3a959 100644 --- a/vectors/vectors_kfrags.json +++ b/vectors/vectors_kfrags.json @@ -2,39 +2,39 @@ "name": "Test vectors for KFrags", "description": "This is a collection of KFrags generated under the enclosed delegating, verifying and receiving keys. Each of them must deserialize correctly and the call to verify() must succeed.", "params": "default", - "verifying_key": "03714109aee5bded1ea98ced8660ad1d49697e14f2a1bbefd5944e4ce443493d08", - "delegating_key": "021c0e2868064f20441df0050bf501137ec84db6ca3596b88a967308e14bea2caa", - "receiving_key": "036285c48fbb7c38bccceb6e15959a33ed3dd5ab5c96daaff67636b8557d6885d4", + "verifying_pk": "03f24761ac8b02de08ad1622d023f669d6214c3bab81a33087ed3ec5505e4d43db", + "delegating_pk": "03a73623a2e72fd52b2d313214c7495580c14fe6cd8de7ad0d63bbfbfd6fb6bd4d", + "receiving_pk": "02952a1903b9c929f0d93d935b34b272ea25a84833a04e22d887f27bc3bf0cc409", "vectors": [ { - "kfrag": "417225b396fb91a82f0ed97b4dfb235200742a58d9632d46e352e507c5c716863b349c46905501861f950d11aefd2c69a1f4fe31ae91b4c8624c7556ec7d01ef026fa5450221297328a37043220b0f82a7fdf2a4ab01c9b30cd572181736390ad303b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103428244169c9d2d3cfefa6b88251e7600c4a6f9a4edb5e371a0af7bc5134bf68ce9ad3b8a1acbd66926687e2cf7a30762527a85d41b1eaee133866df9dc4fe49b43601bb4d9b496f59e44e2e172141f9480bad0cfda18e5dd6c8dd7a5e72b4bc59281ed11ba75b87f7f1919c30b56d7f0c522b77fd15d37f893fc784eb77d1493" + "kfrag": "3f3453856117dedb3d7518d0435c04b10734700c8aca48fa5a8b85eded515cdffefbefddcd3cb728dcc6d05061b5e201aa1cef55ce89e94c34b6887b307d5ab2025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb7490374d2e59ea274c8011f6bf26ca8fe8eb8e7837cafed8547485e3fde6ffe0368e98d26b0d7e12095941daffae8ca429147d9686dc3285ea182c0d7b15db41c7afd3fae3c88100707ef28b53d7cf93961dc509864eb319f0cce274544306c96099b5792af0b820bf758f5c8f2a69cfc1e0a5f91fab96ab82c10c7740602f90efdd315ff5dbd07323526ca1c9a93132b4d008fdf1796a55b92e2fe75c544652256c80101" }, { - "kfrag": "fe11bce6ea1d451fbe396c6c7d00747264324eebc93bd6ca9e949c2970be214e56571ecd1c02064be2058043cbe5d7ddde90771c5709c1cb48f2a4866bef246b03cb7014d5978aba65b7849bc5756d0e94ce515cc31725ed86550e4e38424b63d903b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103adbaf6ef97a68ad61ca39b1ab244b5d1881b00fbf7e2f0eec06907a0078ec4fcb573d101c2d1976985fa7d6d7c96b327030333ac3c66fc0223b456a70e996e19a31822dea4781891b054e86ff34abbfcce05f7c613b7296133ad5b10455061368a102dff42a913edb8e5d248a8d979879a3cfca1f8d3fbf6b8a24ca5357ae057" + "kfrag": "d15ac028be9938b5e2d7a9fb9957b416db538a11d141d320a5f656ab4e3cb0cab3a2ddd2672f3514e7da2f29cbe3815c83b6d704e09d595a68fdd5aeff52ef8a025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749033d6984555d41614a2ba08c2182e27a7105cb60ed414c9732a156c3e44d196dd3507335e3781d6c9a4543e3ae81a4c7538cf280292c9c0d92e4f513e4e073721f2f72a1ea0e69912e21754e32edf9cefd3a96183f5501266bedb301f291709ffb25ed5213660db4c8bfc086e794650455bfde1251f92e1fe49f1feb016ad44fe859b342964db14dddfa6a33eba53021149d8663d306850b8907f9dabf4a5d3ecd0101" }, { - "kfrag": "4edffcacb49f5620c1cebd67f43366aa22dc380dd9fd0f4853ffbbd44e31986f4a99f953e5c553f08a83cb16c0b5c793a4f691d4d3d239e29f232d2b058970d903041b77b8891d845c240a8a12b71dfe9651de67ceb817b54eb578636fe8a9605103b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41035ba90e2a06117e205dd807c780b5687ff6dadf19454fffb367e7af6b3f9cadf03572dc3311c0e1706354ba3340d1b1f915afc82007f9e09ddd34ce07a038a7bb7ecc7343b1532b175e5b8c7d3c712e3eecb18ff69c22237ba9ab6b47b4572529a35b29c48874f396def9971c0230a51e26975a953ff87af8fdbfd53bf42aae07" + "kfrag": "f780e25b78581e70d28f102264e1f04b482eb9ecd45c188f2f9cd90026627f904b903c6da7529805e1d91fbaebc384588083e2c5f235c31e7f768b8d0df43513025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749032f4d57c068c255281703ba6d449345e2f6240a4b8b3047209ff2aca5edec0ad2394020e796b7b9f9130c0ab02b4c8151d050f38f3d0ff5f38dc164e4d254ad2737758c4b67c0b54267b113692f11a410ffc5fe3e41c2f8ec9246854baa63dbfaebb58f0cc89c528e119788c521bbfa8dd33c69129c1a05ed454d7534224c9df35007ef11b38769ee8d2788c72745e50e14e32b0c2b9d988e2550dd3f89cbd6b50101" }, { - "kfrag": "762a4ae9fdbb74de327a29d25c577c46521ee44a951c6e33739e5ceb2a340aefd2a93d8cbe37b9149dd26ce59b0b7ffcd9adf1ca822c5b477595ecba1933291503409c76b74f0f415f5b6a8421514c250f71a5c5b6c5b0d89148e03ba2a624a9a503b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41034f04b1666bc36ac5d3b7ba66420b660d19caf9a995b6cc7cc0aa7c9d0e739f25b1048e754ffd0703e16b453ae8279e5410eb16d2d53683f2d80c30c39918834d70970a09ae17c014fc939979c1e3659b92a40d8688ed77861407316a22d4a8e0b5628c5eec0a2123129ebdb627eae78f8c404f8e129dd407feb2e970b0ba5e39" + "kfrag": "afdbd2183491cec86259c4e6785c9048f17e1f0c86df597983e78f50eb9e88ebfc725b6addb07ce67e5e33e404237dfd9baf15825329d780bbcf07952d899709025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902fbba98644ea41f7abbef7f0a80d645d428844eb6a08c848b73122057f02cc264f27b1b019d667b1a3c89e176b9203870c42dcf3bb6af9191a89db98b2cd1a1b82c61d188f2d2a5d8e1c92a64d3402e98ca09e4594dd67ad74a9583e47b5ee2fd0d38772f8dee198e6e7fa2f7acca079a90b280f98e880106bc337c314bdea20b1e8d1d43ed0912a4a55475c2587ba81a16b9775681774a8bc9d1af2395875e000101" }, { - "kfrag": "b40778198920434d132c4d3724a7b7422428c9921bd627710c3764a6c54376f6a03be8af10cb0f1c3b74d132dda5feeb4340a9f6213da859c05b6b57249e54e903e9198cc0e86b880251931ab162130f93242164a5b242015195290ba75b5a6bea03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103f27829a4798466ed68c2c53137e4e658e86faebfabf2556837b60b502a1721de3e073a98974686d1b847c27167f118eeea6a27029775800cb1618b393bca9cc35b341b546c8851395b3b68945b73b01fdbe94a9f6c400b22bfb5e3bebeee95887d888c49ccf99553a2941de1cbd3f845c5725d1b6b26017dea5cf161fadb5fda" + "kfrag": "831f7b7b14181746fcfd7e26e03e3fb63e7a8903acff3dd3c7ad0c2c94550d9abcced3aacb62fa59fd2847a2b18cc6a6f0a6f646e39ec94f05ef186186dfbe10025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749020db8647602d7401b47c556f25540bd45da99e69462ee8eef1b9c26a26a3757c77f90b015143d8516a5bf2ada51a24b8192e1326d38bf47c0f54591619db42c7c122fd0d955f788dfdeee8d762022887216a3de332632d44092f1f06793eb351fd6acaf0f2060ace737a11e1c1cec72ba361ce754c913765113e1695de323225f24c3f0bbf832c9062d004b69e40090930b40d5986eb9ede462e5d044083127450101" }, { - "kfrag": "bb063b1ae2a2e2c804671a40571b5d9a7746a7b11b98fd1794207daf68e8d408e4ad0acf429b3e97d7d098766cf304fc43454a79d541a8e85cd95ee93b7bfb460242325b9a16de25bca5ce9225091b2d9eec4b98fa627f6c2fec591cfba3561bd303b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41034c2742c62c69c571623a751dc842998cc47a6d33c7e2d30e099bc10d2675fa8bed049ff1474d604eb97e6c1b997dc506bfcefd6aed3838db23fd08f0cebe176fad3a0c5734d986b506145f0770e6689eb1425eb2817d699be1c993d64b26f2dd59d3acf01c5372adabebaa04336ff03219eadf1fd085a6957bd5e90b83919774" + "kfrag": "c63acdf5fb6820df50354e74e65eefacaf75e7e22c780b2b5e2c49a0db9d353e7217ca6a942fce879bd4b596aabd489e0c5af75bd94b72aed37999799e9c8dc9025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902fcaa0a22aabe3b4564972a9368d6a752bc5e359b306cfaff1626b49911158cda599e662db88909f430575f44b3677a8161a5003519e48cf0da635cb20b15f18d632a1bf41eb525b9ee9fd47f196fdd15059d989f9166e5b437cbbd66459e03fad404248f1da8048a5bf9dd9650fbdd50d1afcbadabbcd7a5aca1882b5eeb803443cd4caff8c4b090bc263f56f496aca086bb6f1fedfd5a99e1715ebfe2c6b5010101" }, { - "kfrag": "9a2d97e3bba40307b7c60f6f22c5965f6587206cef4d5d7980ad8d75f0f4ae69de90b3ac071a58a06c4d2d0895db9e71a4f36b86fd078dfce6e436514f70797d03d959ebb3d7ee2fd90f2a390da0d7377f7549877f7720739e7e84e0c488a6d16203b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f410394ba445cfe40a9ebf1bc6a6f9588b704b11b129a2a102e8c5bf892fbb695e2d37d396c1b34f8eed4f348ecb11a29fa2b68c3790ef2ddc169e6fa7dc86e03b1a7fd8faf67773903168ee5f9c2ac60468cd81c7ee389af0f03f3fed356c65e7f182faa22867aa4544554d235f78d28afe7d90fff1cf60184bb1ea947a343c13d07" + "kfrag": "1365f76b3cecb0d27cd839263d6e91e8a3e703cb10590fc4ee8c533caed2fcb4e390938ab2eb832d51d7436058182fb46fb4288c9409a2465c25e447919e61eb025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74903ffe0345c282a9890aa6759dadd14afdd50a82c19123a110d37f4582a1549ce5d4308de0dbbc76897e910253bd1fdb10e65bbbe45cede7dcbf494eba5dee18f9c141cc095ff275208485abe81b229b730c11a1fd87de00756e3d705faff9e6f508cff3f81aec66079b4737c5d55ada03f7a749da2605e6b1bb57324528c373cac773526c02a0cf876744a5e7cd7dee1bad8ccf1e81a46d4c7049743cabca94dad0101" }, { - "kfrag": "3d09cbb50ab9ff970c93a03976efaf5f13aef88fc4e1b8840cebad96d0469605b343d7f3d4297ada1fcc08009260250f29dbb64bfb6dd64c2dd72174be3d87a202f665886dabd4fd94513aa1082a403efcb9722115e7c44f8ce5394799e10c2ca203b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f41037ec97969ef20834c753f460c1c29360ce535cb19e0dd0da3bc9c568157137c3f741dadb514586112bfa22c4aa64c28ba854f7393a04ce77a3c4b731e6eaa41e29f2d240b3adf5a973688c7691e8bde140647f4d2c4d0c06a0d690415a82f18f095fb69a084607734579a590c6d1e477d93716919c67d6627a890fa5a2250109a" + "kfrag": "d961d60a31b197ee26437b1ff6b484c63bcfd208f509af1225a8db8f65240e366f7924ae66d6cd64c25c74761067bcb3a388cf2d4d2e90738666252868e790b0025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749037c88f9bf80bb6a406843e339dcc8548b3f8bf16e525be9c94d5e3b8c243f466fbb19cf3eec376f9c46d9c52040555927c7eb506af29718ed9b728ac210d43b2b4958eae22852a1e3761a71cedff210a52f5d312e5cc1b12bdbb2edbe3f955f5535b8b1ce9e3e8b04a010a2dd227c9d906d7806fdf9df07d8ce379597bf7513c23db95d84e7774a3fe0de392cd7390c6ec7cab60ad22317d8c00de0865bd0ceb80101" }, { - "kfrag": "71c39f563bdda903d8397370b077ba1348e07cf644eaffdfbca251f63d8e0c8377d0f143019bca10090a07c15ab9d6d71857451443e0e77ca6768b45b93892b802b54fd786330796eebd5df0a7d5dc26422177d5637028aaea293dfa223a72a56203b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103aaaf48203b37dc6152c0735493517a31284be4c46b57a21f01359572d1e1e3d1b0fd64fc82efb401f25dab88e48474450b274625c28e16cb51f31259206aba6fe4886083237bdb5f7da67c8e1e2da7fdef57789c919ccceae6b5453eaa22fb6dcb58a2f9f1f20f05d03326031d4817ec52cf3dc9eb37f2d3b014ba315f5b8c16" + "kfrag": "8618ade86dd8b8f15c206274caba6955c30171e466e220ef2344af25f3ca5976ea2eb761d4e8d083e4bf30052c90f6fde525716bd19b5abe98d66bdd48745f85025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb74902bb441051315ea81c8d9d9cc4e63d3f66e6fe3ef52217e8639e0af421d5dbba1be604f5168408cc4bf118346126b9a105b57524b741e141d5cebca15c8964669858bed28994196a23e4f99706cdb8d912d9c53b7c58f98e6d5eb5a4ef3aa2b7b9e0a270af089c667647613c6abe1b7aba8103ee386a04df80341e51e132d6d35d28753fa65e7f8d8c8f3a10d8adc8499a174bcb8041cf6078520ef5e9e00853290101" }, { - "kfrag": "55faf9c9be6d59da90b2c2e9f09d85e3965e00629b31c3854367f86039725ecb5c560659b6dc7d16a5f3202da0489aa02cbbabb65d8e1107c3645ca7e5bf36c3020e06cdf7d03bafc42e77c86d07b76840ffa7db6a5c511f3530d44b34edb69aba03b890c5614684ac49f59bae91f699cf15761d4c994e0561a1dd570d8dc1081f4103f5674cd594e92cace034b8b443a7aae0ae48ed57ca9eeca5ad6e2855334a989c0ea976c30e4f5eb2527b01caa468f150db4a19cae08ba6015df41fc07d4ea6bb33433a543608fd59cacba037daecf6e069f0954ce9a5f1651297c28cf31fdf76b655ed10284f5bb436c93934bb89b2434b7ff9f7bcb414b3e879a5571c88a0b1" + "kfrag": "939cfdfc368ae2c011ade7b7d18543f66f6b2f5aea0ed6732830d8ab281ec492cd90cd1756a44414e51d741e99f9a7d46cbedbfc7bdb60f6b8d33ffb1d4d6b31025141c7f166303d33491fb9aae65c941fd7f40e5c9270a9600c20281090dbb749020d6a191832eed71367b495a514ee4ac442ce19040c00c4e6ab331f844b2fff11d1db98034e5ff8f86a5b2d015cfa439774dd83644adb80bcba1881f09bdb1b105e37329495d03775e99da0f1c99e753e6868fbe04766572a086ce8e1c575e0d253698578672fbfdcdf2607513d0d0c03f346cb8183506d179e9b999988c7ca317c10e8606e3f7a18ac9146dcc2f484dc6c46a149c371c7a22b17c14f0e3e84220101" } ] } \ No newline at end of file diff --git a/vectors/vectors_point_operations.json b/vectors/vectors_point_operations.json index 520d4d43..2e39456c 100644 --- a/vectors/vectors_point_operations.json +++ b/vectors/vectors_point_operations.json @@ -1,37 +1,37 @@ { - "name": "Test vectors for Point operations", + "name": "Test vectors for CurvePoint operations", "params": "default", - "first Point operand": "036893b75f97f4200270c07d043c86d4b9a48e0f0cf4a649b5311cb557cd49f65d", - "second Point operand": "022a7baeacffd711253616cc83da6c17451e5511b92fbef51da2a74f17db1989c6", - "CurveBN operand": "42bd8598f460a2334e921feaa58a64700bdd5eeb4a6d5f59f7363f5d167a2830", + "first CurvePoint operand": "02eb0184eedd9d14e10ad0714afd9915c58b2b40b582283e3e741d0141189246c0", + "second CurvePoint operand": "02b925f594ea60040f470195c72fc7aa8caeac7161af3abe65dceb2908bc866754", + "CurveScalar operand": "d63d8806eba7bfc2f5fd77d21aa5d7cc7cffcee26ac096f7c5904629c0db1c12", "vectors": [ { "operation": "Addition", - "result": "024e520b33f362bf072339db00c591ed9082914fa3c4f3e32b0e614019bf4961de" + "result": "02de2d20749111f538575c81e0abc406c71cf6d4c7c1b3555476d6f8778f271d5a" }, { "operation": "Subtraction", - "result": "021e4e230982f00e23b7b95eb4fcd36881f478b90928a35f5fb0a46fe36d7eb8ed" + "result": "031bd3e6f5b33b3f94601d3a243efaf2c359c3b097aff1aa3fa8fbf5210f110b8d" }, { "operation": "Multiplication", - "result": "03f72a2a0bfef906e5ae98931f156db679bfbb3e72aed461b217d9a035cb2882ad" + "result": "020fd46d21ec56d94a787c6d717222489b7a070a4f57065f442f588789d1feed87" }, { "operation": "Inversion", - "result": "026893b75f97f4200270c07d043c86d4b9a48e0f0cf4a649b5311cb557cd49f65d" + "result": "03eb0184eedd9d14e10ad0714afd9915c58b2b40b582283e3e741d0141189246c0" }, { "operation": "To_affine.X", - "result": "6893b75f97f4200270c07d043c86d4b9a48e0f0cf4a649b5311cb557cd49f65d" + "result": "eb0184eedd9d14e10ad0714afd9915c58b2b40b582283e3e741d0141189246c0" }, { "operation": "To_affine.Y", - "result": "f940cab3f294e54a5ccd5643a212350be822b145e6dab7cae5ad1d98ffb9bbad" + "result": "d9bed51d198e8ccd919b54a6eabfed2032cc737d410e0364643716986de89afc" }, { "operation": "kdf", - "result": "1bbe934cc018de9b123002acdf658b80d63456206c948279ea832832349328f3" + "result": "40b49492ba7924c421dd61ea39bf94ac6566feff43a1ef14e7adc2b9af3f6664" } ] } \ No newline at end of file diff --git a/vectors/vectors_scalar_from_digest.json b/vectors/vectors_scalar_from_digest.json new file mode 100644 index 00000000..b3ad32e2 --- /dev/null +++ b/vectors/vectors_scalar_from_digest.json @@ -0,0 +1,92 @@ +{ + "name": "Test vectors for umbral.curvebn.CurveScalar.from_digest()", + "params": "default", + "vectors": [ + { + "input": [ + { + "class": "bytes", + "bytes": "" + } + ], + "output": "42184a0ea1e39037cad1ed7f3bb0cd8b7fe978e6d8b94f965e47d582cbdb8208" + }, + { + "input": [ + { + "class": "bytes", + "bytes": "616263" + } + ], + "output": "02e2c58350c30e80f9deea1ae19e21a0baa7761f4448c792f205b8e9b7ac1ab3" + }, + { + "input": [ + { + "class": "CurvePoint", + "bytes": "02a03893438c0502dd13818f65c039b2ef4fce33bfed150c6ad166554b4e8a51c2" + } + ], + "output": "cd175898869252f3d6c6e77eaed94a72e74410d99534b349f27df2362c35745a" + }, + { + "input": [ + { + "class": "CurveScalar", + "bytes": "5e8601ee29241f263bf49b9999594413f863193fa1b8a985fff981cda7cc9087" + } + ], + "output": "2b83903fedca70169024365a4a5b387536a9ba38bd7b9fa0462f5f932f41a493" + }, + { + "input": [ + { + "class": "CurvePoint", + "bytes": "02a03893438c0502dd13818f65c039b2ef4fce33bfed150c6ad166554b4e8a51c2" + }, + { + "class": "CurveScalar", + "bytes": "5e8601ee29241f263bf49b9999594413f863193fa1b8a985fff981cda7cc9087" + } + ], + "output": "9f7cea094a5ed29ab2ec83391527db31850f915781c07c0b13853869e9885968" + }, + { + "input": [ + { + "class": "CurvePoint", + "bytes": "02a03893438c0502dd13818f65c039b2ef4fce33bfed150c6ad166554b4e8a51c2" + }, + { + "class": "CurvePoint", + "bytes": "02ecbc4dc0aed60efa211c7bf0e238593d292a042373b891928bcee459151c2f44" + }, + { + "class": "CurvePoint", + "bytes": "02aa6a09c61286e36d82f6371038f1c33b2095b3b6dc8d09de7489f516c2dfe49a" + }, + { + "class": "CurvePoint", + "bytes": "028da9ebf8cc0966bc010152fd3917a8f12dfff0af3b06e34e17f300d622893159" + }, + { + "class": "CurvePoint", + "bytes": "0281171b7e330ebd097575dadb210e8a405bc162e293881457a301da03f7571c7a" + }, + { + "class": "CurvePoint", + "bytes": "03e2869b26bbf46a2e1f46116d8d9eeeb45f18acf4a808defee52040221dd08af9" + }, + { + "class": "CurvePoint", + "bytes": "0374d2e59ea274c8011f6bf26ca8fe8eb8e7837cafed8547485e3fde6ffe0368e9" + }, + { + "class": "CurvePoint", + "bytes": "024526ff9ab3c9c4cf619166ff897b8c023a6ff01f54c42a921ec1ab564d5a65eb" + } + ], + "output": "072c408c4631491eb12b00b38b8f7f20080b802e3e7c5c421f89887055248b1a" + } + ] +} \ No newline at end of file diff --git a/vectors/vectors_scalar_operations.json b/vectors/vectors_scalar_operations.json new file mode 100644 index 00000000..d7e342c4 --- /dev/null +++ b/vectors/vectors_scalar_operations.json @@ -0,0 +1,24 @@ +{ + "name": "Test vectors for CurveScalar operations", + "params": "default", + "first operand": "d63d8806eba7bfc2f5fd77d21aa5d7cc7cffcee26ac096f7c5904629c0db1c12", + "second operand": "c2cc9d2f0b39201a5d4d4aa755c0506eab19c1abc89068d216f23f4965427ac4", + "vectors": [ + { + "operation": "Addition", + "result": "990a2535f6e0dfdd534ac2797066283c6d6ab3a784085f8e1cb026e655e75595" + }, + { + "operation": "Subtraction", + "result": "1370ead7e06e9fa898b02d2ac4e5875dd1e60d36a2302e25ae9e06e05b98a14e" + }, + { + "operation": "Multiplication", + "result": "88cdbd2959262c74f26d4315e65b7e8c4fb5d1326fb9f1c6dbfd7c951d43485f" + }, + { + "operation": "Inverse", + "result": "663c74e198bd4dcd2db7b78895fe8994a727d8bcba073818475a22483bb0103a" + } + ] +} \ No newline at end of file diff --git a/vectors/vectors_unsafe_hash_to_point.json b/vectors/vectors_unsafe_hash_to_point.json index 920ed742..fd2a4394 100644 --- a/vectors/vectors_unsafe_hash_to_point.json +++ b/vectors/vectors_unsafe_hash_to_point.json @@ -1,86 +1,86 @@ { - "name": "Test vectors for umbral.point.Point.unsafe_hash_to_point", + "name": "Test vectors for unsafe_hash_to_point()", "params": "default", "vectors": [ { "data": "", - "label": "", - "point": "03bb27bd2a7b7ff7500284bc44285d80ce5527448c79e7ef25843d1209c5df40c3" + "dst": "", + "point": "0215ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b" }, { "data": "", - "label": "616263", - "point": "03eec257593bbefd7f617350bd2aea9b14cc14a93528bad07b6e39c5db433adbd8" + "dst": "616263", + "point": "0297427e8f434c897d66d7ad40b51e0a11f8bdfed31e724ca4ac86c14fb5e2668f" }, { "data": "", - "label": "4e75437970686572", - "point": "0352184687070d943946f5da637b8e2aa00640b0da2231e7598ceea0286657a7cb" + "dst": "4e75437970686572", + "point": "025aa967c88c73854f4f4d9286a7ce4292898dfa42c7593b75cf823cd1b6dada96" }, { "data": "", - "label": "4e75637970686572", - "point": "0290c2fff33ad722da7cc813f57153afbcf33fc2acd1607f188770f9dcc7c00a0d" + "dst": "4e75637970686572", + "point": "02f3fcae4fb3596fbb34feaf3e2fff938b177b55d89c66f728e51fef220d9b702f" }, { "data": "616263", - "label": "", - "point": "02a79e7fe8a6559b5de0614702a4e13d97a672a899d5943e5c6b1a0c45fca933b0" + "dst": "", + "point": "0204c19746f60b6c4abbce9dbe31f2e0df9b22d8130cc0844cbf67db154d944db3" }, { "data": "616263", - "label": "616263", - "point": "0234cb950681a734bc29feaa5c1dd029a9062352488bae035acd56c0ebe99f0b7f" + "dst": "616263", + "point": "024c70ca862edba77a8265ee46e0137729826a79721855888bf7791feea42b9990" }, { "data": "616263", - "label": "4e75437970686572", - "point": "032106a37075e17b6f01b4ab63d5c84622efe5111843155c7d53e34548f287115d" + "dst": "4e75437970686572", + "point": "02357334755ceedaef03cb81b6dbbebd8399e0cf40a122a586069ae241e34fc869" }, { "data": "616263", - "label": "4e75637970686572", - "point": "034b4db429ccf36858718cb864c0c27520fb16218992a3290d1fc4758756ee0bef" + "dst": "4e75637970686572", + "point": "0256ec5dbf81d55fbdad3c2095177982a068bb0043dd2cf2834cc6a53e538157bf" }, { "data": "4e75437970686572", - "label": "", - "point": "03828335c1f3ccac7ec40d6dd4771e9ba527b039d7104fa0477a96cdccbf16c7cf" + "dst": "", + "point": "02b0cd14ef08638d57804c768d3b0a171461268f6faede586751f2919bdd7490b6" }, { "data": "4e75437970686572", - "label": "616263", - "point": "03193846a6ba4d9dab59990e53317050fe301477d3ac699f8cc260dd33cbc2c9c5" + "dst": "616263", + "point": "02a1c3e1c00f45a059fcf7749e31c5206388aa72bcc7c10195907e9c70c2a0a700" }, { "data": "4e75437970686572", - "label": "4e75437970686572", - "point": "0324215e7df37205a23c0b7f0a7e168c7984d9109ef31ebe7b37edc7d81ede4b55" + "dst": "4e75437970686572", + "point": "02100c656eed3ed2e175e5430bbd644ac86f24fa69fc1c5b3fd65ece562b480764" }, { "data": "4e75437970686572", - "label": "4e75637970686572", - "point": "03d5744667f3f2ff36217380a5d1701edf939d70b79d17d78a3969b533acb7c326" + "dst": "4e75637970686572", + "point": "0227a46bc66817fcaa803535a1c109674d300de5df0d8d11f6588325cf6cedf2b1" }, { "data": "4e75637970686572", - "label": "", - "point": "03f31c20a8264446d224c4ae6368193ac8b97247cf386bafe2cc27ffb6783ace19" + "dst": "", + "point": "02483315691815818fa1f1804406fc4246940cc8cb39405401e2aa5fd8d94bfa64" }, { "data": "4e75637970686572", - "label": "616263", - "point": "0347b4a8559bf9eb00cdc4c183ee4ff0a5c3a12692988d88927b07157c3fdd50a1" + "dst": "616263", + "point": "02dc01829e4725f8cacf6990c12ab0a5f837770b21e41bdd9964bb0f1ad52fcc31" }, { "data": "4e75637970686572", - "label": "4e75437970686572", - "point": "02b158ccb0b3384c2a8af92ac3e0e59ed9d0647fb62715fefdc9c0796c0b28bebb" + "dst": "4e75437970686572", + "point": "02b6653e2ed79579380104598cf83fc2b119dd8b91afae2a2a8077ffdca0b212ad" }, { "data": "4e75637970686572", - "label": "4e75637970686572", - "point": "0356b5aa5ef6be8229f3611095a47842aad8a3e54b3aa1f6caea696fa4bfdfe95e" + "dst": "4e75637970686572", + "point": "02192de02d9c15a52d90ef7192794a2fc925c09f7dcdb4b584b8c7fab33bbda1df" } ] } \ No newline at end of file From ee7d31bda0b33a8b59773083cebc2f839017d736 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Thu, 25 Mar 2021 19:16:39 -0700 Subject: [PATCH 20/25] RFC for docs --- docs/notebooks/pyUmbral Simple API.ipynb | 14 ++++++-------- docs/source/index.rst | 2 +- docs/source/using_pyumbral.rst | 6 ++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/notebooks/pyUmbral Simple API.ipynb b/docs/notebooks/pyUmbral Simple API.ipynb index eff9dd1e..5ac9da0c 100644 --- a/docs/notebooks/pyUmbral Simple API.ipynb +++ b/docs/notebooks/pyUmbral Simple API.ipynb @@ -176,12 +176,12 @@ "from umbral import generate_kfrags\n", "\n", "\n", - "M, N = 10, 20\n", + "M, N = 10, 20 # the threshold and the total number of fragments\n", "kfrags = generate_kfrags(delegating_sk=alices_private_key,\n", " receiving_pk=bobs_public_key,\n", " signing_sk=alices_signing_key,\n", - " threshold=10,\n", - " num_kfrags=20)" + " threshold=M,\n", + " num_kfrags=N)" ] }, { @@ -201,7 +201,7 @@ "source": [ "import random\n", "kfrags = random.sample(kfrags, # All kfrags from above\n", - " 10) # M - Threshold\n", + " M) # Threshold\n", "\n", "\n", "from umbral import reencrypt\n", @@ -210,9 +210,7 @@ "cfrags = list() # Bob's cfrag collection\n", "for kfrag in kfrags:\n", " cfrag = reencrypt(capsule=capsule, kfrag=kfrag)\n", - " cfrags.append(cfrag) # Bob collects a cfrag\n", - "\n", - "assert len(cfrags) == 10\n" + " cfrags.append(cfrag) # Bob collects a cfrag" ] }, { @@ -243,7 +241,7 @@ "metadata": {}, "source": [ "## Bob opens the capsule; Decrypts data from Alice.\n", - "Finally, Bob decrypts the re-encrypted ciphertext using his key." + "Finally, Bob decrypts the re-encrypted ciphertext using his secret key." ] }, { diff --git a/docs/source/index.rst b/docs/source/index.rst index 00510458..801a629e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,7 +31,7 @@ pyUmbral .. end-badges pyUmbral is the reference implementation of the Umbral_ threshold proxy re-encryption scheme. -It is open-source, built with Python, and uses OpenSSL_ via Cryptography.io_ and libsodium_ via PyNaCl_. +It is open-source, built with Python, and uses OpenSSL_ via Cryptography.io_, and libsodium_ via PyNaCl_. Using Umbral, Alice (the data owner) can *delegate decryption rights* to Bob for any ciphertext intended to her, through a re-encryption process performed by a diff --git a/docs/source/using_pyumbral.rst b/docs/source/using_pyumbral.rst index 9fdde746..56f6c15f 100644 --- a/docs/source/using_pyumbral.rst +++ b/docs/source/using_pyumbral.rst @@ -23,7 +23,9 @@ You can find them in the ``cryptography.hazmat.primitives.asymmetric.ec`` module .. _Cryptography.io: https://cryptography.io/en/latest/ -Be careful when choosing a curve - the security of your application depends on it. +.. important:: + + Be careful when choosing a curve - the security of your application depends on it. We provide curve ``SECP256K1`` as a default because it is the basis for a number of crypto-blockchain projects; we don't otherwise endorse its security. @@ -89,7 +91,7 @@ Bob Exists Alice grants access to Bob by generating kfrags ----------------------------------------------- -When Alice wants to grant Bob access to open her encrypted messages, +When Alice wants to grant Bob access to view her encrypted data, she creates *re-encryption key fragments*, or *"kfrags"*, which are next sent to N proxies or *Ursulas*. From a08a552708c59b2733557d2e35e8997ff1ff693c Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 26 Mar 2021 20:30:23 -0700 Subject: [PATCH 21/25] Replace `dem.ErrorInvalidTag` and `Capsule.NotValid` with `GenericError`. --- docs/examples/umbral_simple_api.py | 5 ++--- docs/notebooks/pyUmbral Simple API.ipynb | 4 ++-- docs/source/api.rst | 3 +++ docs/source/using_pyumbral.rst | 2 +- tests/test_capsule.py | 5 +++-- tests/test_dem.py | 7 ++++--- tests/test_pre.py | 4 ++-- umbral/__init__.py | 2 ++ umbral/capsule.py | 10 +++------- umbral/dem.py | 9 ++++----- umbral/errors.py | 5 +++++ 11 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 umbral/errors.py diff --git a/docs/examples/umbral_simple_api.py b/docs/examples/umbral_simple_api.py index 22e1030f..e28d3802 100644 --- a/docs/examples/umbral_simple_api.py +++ b/docs/examples/umbral_simple_api.py @@ -1,8 +1,7 @@ import random from umbral import ( - SecretKey, PublicKey, + SecretKey, PublicKey, GenericError, encrypt, generate_kfrags, reencrypt, decrypt_original, decrypt_reencrypted) -from umbral.dem import ErrorInvalidTag # Generate an Umbral key pair # --------------------------- @@ -46,7 +45,7 @@ # Attempt Bob's decryption (fail) try: fail_decrypted_data = decrypt_original(bobs_secret_key, bob_capsule, ciphertext) -except ErrorInvalidTag: +except GenericError: print("Decryption failed! Bob doesn't has access granted yet.") # Alice grants access to Bob by generating kfrags diff --git a/docs/notebooks/pyUmbral Simple API.ipynb b/docs/notebooks/pyUmbral Simple API.ipynb index 5ac9da0c..6962723c 100644 --- a/docs/notebooks/pyUmbral Simple API.ipynb +++ b/docs/notebooks/pyUmbral Simple API.ipynb @@ -140,13 +140,13 @@ } ], "source": [ - "from umbral.dem import ErrorInvalidTag\n", + "from umbral import GenericError\n", "\n", "try:\n", " fail_decrypted_data = decrypt_original(sk=bobs_private_key,\n", " capsule=capsule,\n", " ciphertext=ciphertext)\n", - "except ErrorInvalidTag:\n", + "except GenericError:\n", " print(\"Decryption failed! Bob doesn't has access granted yet.\")\n" ] }, diff --git a/docs/source/api.rst b/docs/source/api.rst index 27e691b4..de61bfdd 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -52,6 +52,9 @@ Encryption, re-encryption and decryption Utilities --------- +.. autoclass:: umbral.GenericError + :show-inheritance: + .. autoclass:: umbral.serializable.Serializable :members: from_bytes :special-members: __bytes__ diff --git a/docs/source/using_pyumbral.rst b/docs/source/using_pyumbral.rst index 56f6c15f..396f9f15 100644 --- a/docs/source/using_pyumbral.rst +++ b/docs/source/using_pyumbral.rst @@ -134,7 +134,7 @@ or re-encrypted for him by Ursula, he will not be able to open it. ... ciphertext=ciphertext) Traceback (most recent call last): ... - umbral.dem.ErrorInvalidTag + umbral.GenericError Ursulas perform re-encryption diff --git a/tests/test_capsule.py b/tests/test_capsule.py index 1896ca26..9f6566cf 100644 --- a/tests/test_capsule.py +++ b/tests/test_capsule.py @@ -4,6 +4,7 @@ Capsule, SecretKey, PublicKey, + GenericError, encrypt, decrypt_original, reencrypt, @@ -27,7 +28,7 @@ def test_capsule_serialization(alices_keys): capsule.point_e = CurvePoint.random() capsule_bytes = bytes(capsule) - with pytest.raises(Capsule.NotValid): + with pytest.raises(GenericError): Capsule.from_bytes(capsule_bytes) @@ -84,7 +85,7 @@ def test_open_reencrypted(alices_keys, bobs_keys): capsule.open_reencrypted(receiving_sk, delegating_pk, []) # Not enough cfrags - with pytest.raises(ValueError, match="Internal validation failed"): + with pytest.raises(GenericError, match="Internal validation failed"): capsule.open_reencrypted(receiving_sk, delegating_pk, cfrags[:threshold-1]) # Repeating cfrags diff --git a/tests/test_dem.py b/tests/test_dem.py index d0e6c9a9..7d5a5b12 100644 --- a/tests/test_dem.py +++ b/tests/test_dem.py @@ -1,7 +1,8 @@ import pytest import os -from umbral.dem import DEM, ErrorInvalidTag +from umbral import GenericError +from umbral.dem import DEM def test_encrypt_decrypt(): @@ -47,7 +48,7 @@ def test_malformed_ciphertext(): dem.decrypt(ciphertext[:DEM.NONCE_SIZE + DEM.TAG_SIZE - 1]) # Too long - with pytest.raises(ErrorInvalidTag): + with pytest.raises(GenericError): dem.decrypt(ciphertext + b'abcd') @@ -76,5 +77,5 @@ def test_encrypt_decrypt_associated_data(): assert cleartext1 == plaintext # Attempt decryption with invalid associated data - with pytest.raises(ErrorInvalidTag): + with pytest.raises(GenericError): cleartext2 = dem.decrypt(ciphertext0, authenticated_data=b'wrong data') diff --git a/tests/test_pre.py b/tests/test_pre.py index 0ee5b7a4..94617ebe 100644 --- a/tests/test_pre.py +++ b/tests/test_pre.py @@ -3,13 +3,13 @@ from umbral import ( SecretKey, PublicKey, + GenericError, encrypt, generate_kfrags, decrypt_original, reencrypt, decrypt_reencrypted, ) -from umbral.dem import ErrorInvalidTag def test_public_key_encryption(alices_keys): @@ -22,7 +22,7 @@ def test_public_key_encryption(alices_keys): # Wrong secret key sk = SecretKey.random() - with pytest.raises(ErrorInvalidTag): + with pytest.raises(GenericError): decrypt_original(sk, capsule, ciphertext) diff --git a/umbral/__init__.py b/umbral/__init__.py index 130758f0..38c55f4c 100644 --- a/umbral/__init__.py +++ b/umbral/__init__.py @@ -4,6 +4,7 @@ from .capsule import Capsule from .capsule_frag import CapsuleFrag +from .errors import GenericError from .key_frag import KeyFrag, generate_kfrags from .keys import SecretKey, PublicKey, SecretKeyFactory from .pre import encrypt, decrypt_original, decrypt_reencrypted, reencrypt @@ -23,6 +24,7 @@ "Capsule", "KeyFrag", "CapsuleFrag", + "GenericError", "encrypt", "decrypt_original", "generate_kfrags", diff --git a/umbral/capsule.py b/umbral/capsule.py index 0ca1d4b4..8af488b1 100644 --- a/umbral/capsule.py +++ b/umbral/capsule.py @@ -2,6 +2,7 @@ from .curve_point import CurvePoint from .curve_scalar import CurveScalar +from .errors import GenericError from .hashing import hash_capsule_points, hash_to_polynomial_arg, hash_to_shared_secret from .keys import PublicKey, SecretKey from .params import PARAMETERS @@ -24,11 +25,6 @@ class Capsule(Serializable): Encapsulated symmetric key. """ - class NotValid(ValueError): - """ - raised if the capsule does not pass verification. - """ - def __init__(self, point_e: CurvePoint, point_v: CurvePoint, signature: CurveScalar): self.point_e = point_e self.point_v = point_v @@ -40,7 +36,7 @@ def __take__(cls, data: bytes) -> Tuple['Capsule', bytes]: capsule = cls(e, v, sig) if not capsule._verify(): - raise cls.NotValid("Capsule verification failed.") + raise GenericError("Capsule self-verification failed. Serialized data may be damaged.") return capsule, data @@ -111,7 +107,7 @@ def open_reencrypted(self, # TODO: check for d == 0? Or just let if fail? inv_d = d.invert() if orig_pub_key * (s * inv_d) != (e_prime * h) + v_prime: - raise ValueError("Internal validation failed") + raise GenericError("Internal validation failed") return (e_prime + v_prime) * d diff --git a/umbral/dem.py b/umbral/dem.py index b23fe9ba..042a1ee7 100644 --- a/umbral/dem.py +++ b/umbral/dem.py @@ -14,6 +14,7 @@ ) from . import openssl +from .errors import GenericError def kdf(data: bytes, @@ -30,10 +31,6 @@ def kdf(data: bytes, return hkdf.derive(data) -class ErrorInvalidTag(Exception): - pass - - class DEM: KEY_SIZE = XCHACHA_KEY_SIZE @@ -67,4 +64,6 @@ def decrypt(self, nonce_and_ciphertext: bytes, authenticated_data: bytes = b"") try: return xchacha_decrypt(ciphertext, authenticated_data, nonce, self._key) except nacl.exceptions.CryptoError: - raise ErrorInvalidTag + raise GenericError("Decryption of ciphertext failed: " + "either someone tampered with the ciphertext or " + "you are using an incorrect decryption key.") diff --git a/umbral/errors.py b/umbral/errors.py new file mode 100644 index 00000000..328c3b9f --- /dev/null +++ b/umbral/errors.py @@ -0,0 +1,5 @@ +class GenericError(Exception): + """ + An interal Umbral error, see the message for details. + """ + pass From 7d0f2fe3e2dde4c1014d42f4539ad29b59730120 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 27 Mar 2021 14:10:33 -0700 Subject: [PATCH 22/25] Cache public key in the secret key to speed up `generate_kfrags()` --- umbral/keys.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/umbral/keys.py b/umbral/keys.py index aea9cc76..53fe0e1b 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -25,6 +25,11 @@ class SecretKey(Serializable): def __init__(self, scalar_key: CurveScalar): self._scalar_key = scalar_key + # Cached public key. Access it via `PublicKey.from_secret_key()` - + # it may be removed later. + # We are assuming here that there will be on average more calls to + # `PublicKey.from_secret_key()` than secret key instantiations. + self._public_key_point = CurvePoint.generator() * self._scalar_key @classmethod def random(cls) -> 'SecretKey': @@ -130,7 +135,7 @@ def from_secret_key(cls, sk: SecretKey) -> 'PublicKey': """ Creates the public key corresponding to the given secret key. """ - return cls(CurvePoint.generator() * sk.secret_scalar()) + return cls(sk._public_key_point) @classmethod def __take__(cls, data: bytes) -> Tuple['PublicKey', bytes]: From a7f4a7a63414f2af055a72ed691f7ccd1b87a61d Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 12 Apr 2021 21:55:26 -0700 Subject: [PATCH 23/25] Fix logic in bn_from_bytes(..., apply_modulus=True) --- umbral/openssl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/umbral/openssl.py b/umbral/openssl.py index cede9b52..646c6206 100644 --- a/umbral/openssl.py +++ b/umbral/openssl.py @@ -182,8 +182,9 @@ def bn_from_bytes(bytes_seq: bytes, set_consttime_flag=True, check_modulus=None, with backend._tmp_bn_ctx() as bn_ctx: res = backend._lib.BN_mod(bignum, bn, apply_modulus, bn_ctx) backend.openssl_assert(res == 1) - - return bn + return bignum + else: + return bn def bn_to_bytes(bn, length: int): From fd9e1d44e5c430eb9bb11ce536ba970665c42596 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 12 Apr 2021 21:56:22 -0700 Subject: [PATCH 24/25] Remove a TODO rust-umbral#43 is closed --- umbral/pre.py | 1 - 1 file changed, 1 deletion(-) diff --git a/umbral/pre.py b/umbral/pre.py index b96fb397..df02ef31 100644 --- a/umbral/pre.py +++ b/umbral/pre.py @@ -52,7 +52,6 @@ def decrypt_reencrypted(decrypting_sk: SecretKey, """ key_seed = capsule.open_reencrypted(decrypting_sk, delegating_pk, cfrags) - # TODO: add salt and info here? dem = DEM(bytes(key_seed)) return dem.decrypt(ciphertext, authenticated_data=bytes(capsule)) From 503a1c6a1826bc10ecf042e197d5bd772de05cef Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 12 Apr 2021 22:00:40 -0700 Subject: [PATCH 25/25] Always set constant time operations for OpenSSL bignums set_consttime_flag was always True anyway --- umbral/openssl.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/umbral/openssl.py b/umbral/openssl.py index 646c6206..927be3b3 100644 --- a/umbral/openssl.py +++ b/umbral/openssl.py @@ -116,19 +116,16 @@ def __str__(self): # -def _bn_new(set_consttime_flag=True): +def _bn_new(): """ Returns a new and initialized OpenSSL BIGNUM. - The set_consttime_flag is set to True by default. When this instance of a - CurveBN object has BN_FLG_CONSTTIME set, OpenSSL will use constant time - operations whenever this CurveBN is passed. """ new_bn = backend._lib.BN_new() backend.openssl_assert(new_bn != backend._ffi.NULL) new_bn = backend._ffi.gc(new_bn, backend._lib.BN_clear_free) - if set_consttime_flag: - backend._lib.BN_set_flags(new_bn, backend._lib.BN_FLG_CONSTTIME) + # Always use constant time operations. + backend._lib.BN_set_flags(new_bn, backend._lib.BN_FLG_CONSTTIME) return new_bn @@ -144,13 +141,10 @@ def bn_is_normalized(check_bn, modulus): return (check_sign == 1 or check_sign == 0) and range_check == -1 -def bn_from_int(py_int: int, check_modulus=None, set_consttime_flag=True): +def bn_from_int(py_int: int, check_modulus=None): """ Converts the given Python int to an OpenSSL BIGNUM. If ``modulus`` is provided, it will check if the Python integer is within ``[0, modulus)``. - - If set_consttime_flag is set to True, OpenSSL will use constant time - operations when using this CurveBN. """ conv_bn = backend._int_to_bn(py_int) conv_bn = backend._ffi.gc(conv_bn, backend._lib.BN_clear_free) @@ -158,18 +152,15 @@ def bn_from_int(py_int: int, check_modulus=None, set_consttime_flag=True): if check_modulus and not bn_is_normalized(conv_bn, check_modulus): raise ValueError(f"The Python integer given ({py_int}) is not under the provided modulus.") - if set_consttime_flag: - backend._lib.BN_set_flags(conv_bn, backend._lib.BN_FLG_CONSTTIME) + backend._lib.BN_set_flags(conv_bn, backend._lib.BN_FLG_CONSTTIME) return conv_bn -def bn_from_bytes(bytes_seq: bytes, set_consttime_flag=True, check_modulus=None, apply_modulus=None): +def bn_from_bytes(bytes_seq: bytes, check_modulus=None, apply_modulus=None): """ Converts the given byte sequence to an OpenSSL BIGNUM. - If set_consttime_flag is set to True, OpenSSL will use constant time - operations when using this BIGNUM. """ - bn = _bn_new(set_consttime_flag) + bn = _bn_new() backend._lib.BN_bin2bn(bytes_seq, len(bytes_seq), bn) backend.openssl_assert(bn != backend._ffi.NULL)