├── test ├── __init__.py └── tests.py ├── requirements.txt ├── threshold_crypto ├── __init__.py ├── number.py ├── central.py ├── participant.py └── data.py ├── .gitignore ├── .gitlab-ci.yml ├── setup.py ├── LICENSE ├── README.md └── eval ├── performance-eval.py └── draw_run.py /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pynacl 2 | pycryptodome 3 | coverage -------------------------------------------------------------------------------- /threshold_crypto/__init__.py: -------------------------------------------------------------------------------- 1 | from .participant import * 2 | from .central import * 3 | from .data import * 4 | from .number import * 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse and IDEA 2 | 3 | .metadata/ 4 | .classpath 5 | .project 6 | .settings/ 7 | bin/ 8 | .idea/ 9 | 10 | # Python 11 | 12 | __pycache__/ 13 | venv 14 | tmp 15 | .coverage 16 | 17 | # eval 18 | 19 | eval/eval-* 20 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: "python:3.6" 2 | 3 | before_script: 4 | - python --version 5 | - pip install -r requirements.txt 6 | 7 | stages: 8 | - test 9 | 10 | test: 11 | stage: test 12 | script: 13 | - coverage run --source=threshold_crypto -m unittest 14 | - coverage report 15 | 16 | doctest: 17 | stage: test 18 | script: 19 | - python -m doctest README.md 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def readme(): 5 | with open('README.md') as f: 6 | return f.read() 7 | 8 | 9 | setup( 10 | name='threshold-crypto', 11 | version='0.3.0', 12 | keywords='elgamal threshold decryption', 13 | description='ElGamal-based threshold decryption', 14 | long_description=readme(), 15 | url='https://github.com/tompetersen/threshold-crypto', 16 | author='Tom Petersen, SVS, Universität Hamburg', 17 | packages=find_packages(), 18 | classifiers=[ 19 | 'Development Status :: 3 - Alpha', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Programming Language :: Python :: 3', 22 | 'Topic :: Security :: Cryptography', 23 | ], 24 | install_requires=[ 25 | 'pynacl', 26 | ], 27 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Tom Petersen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /threshold_crypto/number.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import operator 3 | from typing import List, Optional 4 | 5 | from Crypto.PublicKey import ECC 6 | from Crypto.Random import random 7 | 8 | 9 | def int_to_bytes(value: int) -> bytes: 10 | """ Return the bytes for a given integer. """ 11 | return value.to_bytes((value.bit_length() + 7) // 8, byteorder='big') 12 | 13 | 14 | def ecc_sum(points: List[ECC.EccPoint]) -> Optional[ECC.EccPoint]: 15 | """ Compute the sum of a list of EccPoints. """ 16 | if len(points) == 0: 17 | return None 18 | elif len(points) == 1: 19 | return points[0].copy() 20 | else: 21 | result = points[0].copy() 22 | for point in points[1:]: 23 | result += point 24 | 25 | return result 26 | 27 | 28 | def random_in_range(a: int, b: int) -> int: 29 | """ Return a random number r with a <= r <= b. """ 30 | return random.randint(a, b) 31 | 32 | 33 | def prime_mod_inv(x: int, p: int) -> int: 34 | """ Compute the modular inverse of x in the finite field Z_p. """ 35 | return pow(x, p - 2, p) # Fermats little theorem 36 | 37 | 38 | def prod(factors: List[int]) -> int: 39 | """ Compute the product of a list of integers. """ 40 | return functools.reduce(operator.mul, factors, 1) 41 | 42 | 43 | class PolynomMod: 44 | 45 | @staticmethod 46 | def create_random_polynom(absolute_term: int, degree: int, q: int) -> 'PolynomMod': 47 | """ 48 | Create a polynomial with random coefficients in range [1, q - 1] and an given absolute term. 49 | 50 | :param absolute_term: the absolute term (constant term) 51 | :param degree: the polynomial degree 52 | :param q: the modular order of the underlying group 53 | :return: the polynom 54 | """ 55 | coefficients = [absolute_term] 56 | coefficients.extend([random_in_range(1, q - 1) for _ in range(0, degree)]) 57 | 58 | return PolynomMod(coefficients, q) 59 | 60 | def __init__(self, coefficients: List[int], q: int): 61 | # Make sure that the highest degree coefficient is set. 62 | # An alternative would be to strip trailing zero elements. 63 | assert coefficients[-1] != 0 64 | 65 | self.coefficients = coefficients 66 | self.q = q 67 | 68 | @property 69 | def degree(self) -> int: 70 | return len(self.coefficients) - 1 71 | 72 | def evaluate(self, x: int) -> int: 73 | """ 74 | Evaluate the polynomial for a given x value. 75 | 76 | :param x: the value 77 | :return: the result 78 | """ 79 | evaluated = ((self.coefficients[j] * pow(x, j)) for j in range(0, self.degree + 1)) 80 | return sum(evaluated) % self.q 81 | 82 | def __str__(self) -> str: 83 | c_list = ["%d*x^%d " % (c, i) for (i, c) in enumerate(self.coefficients)] 84 | return "Polynom of degree {}: f(x) = {}".format(self.degree, " + ".join(c_list)) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Threshold cryptography library 2 | 3 | 4 | A stateless library which offers functionality for ElGamal-based threshold decryption with centralized or distributed key generation. 5 | 6 | Threshold decryption means a message can be encrypted using a simple public key, but for decryption at least t out of n 7 | share owners must collaborate to decrypt the message. 8 | 9 | A hybrid approach (using [pynacl](https://pynacl.readthedocs.io) for symmetric encryption and 10 | [PyCryptodome](https://pycryptodome.readthedocs.io) for ECC operations) is used for message encryption and decryption. 11 | Therefore there are no limitations regarding message lengths or format. Additionally the integrity of a message is 12 | secured by using the AE-scheme, meaning changes to some parts of the ciphertext, to partial decryptions or even 13 | dishonest share owners can be detected. 14 | 15 | **Warning**: This library has never been (independently) audited and must not be used for productive applications. 16 | 17 | ## Usage 18 | 19 | Import the library: 20 | 21 | >>> import threshold_crypto as tc 22 | 23 | ### Parameter Generation 24 | 25 | Generate required parameters: 26 | 27 | >>> curve_params = tc.CurveParameters() 28 | >>> thresh_params = tc.ThresholdParameters(t=3, n=5) 29 | 30 | The `CurveParameters` describe the elliptic curve the operations are performed on. 31 | The `ThresholdParameters` determine the number of created shares `n` and the number of required participants for the decryption operation `t`. 32 | 33 | ### Centralized Key Generation 34 | 35 | The public key and shares of the private key can be computed in a centralized manner by a trusted third party: 36 | 37 | >>> pub_key, key_shares = tc.create_public_key_and_shares_centralized(curve_params, thresh_params) 38 | 39 | ### Distributed Key Generation 40 | 41 | But they can also be computed via a distributed key generation (DKG) protocol following "A threshold cryptosystem without a trusted party" by Pedersen (1991). 42 | This involves multiple steps performed by all participants in collaboration. 43 | The following example code uses lists to illustrate this, but has to be distributed over the different participant applications and machines in practice. 44 | 45 | The first step is the participant initialization: 46 | 47 | >>> participant_ids = list(range(1, thresh_params.n + 1)) 48 | >>> participants = [tc.Participant(id, participant_ids, curve_params, thresh_params) for id in participant_ids] 49 | 50 | Next each participant broadcasts a closed commitment to a share of the later public key to the other participants: 51 | 52 | >>> for pi in participants: 53 | ... for pj in participants: 54 | ... if pj != pi: 55 | ... closed_commitment = pj.closed_commmitment() 56 | ... pi.receive_closed_commitment(closed_commitment) 57 | 58 | After each participant has received all closed commitments they broadcast their open commitments: 59 | 60 | >>> for pi in participants: 61 | ... for pj in participants: 62 | ... if pj != pi: 63 | ... open_commitment = pj.open_commitment() 64 | ... pi.receive_open_commitment(open_commitment) 65 | 66 | Afterwards each participant should be able to compute the same public key: 67 | 68 | >>> public_key = participants[0].compute_public_key() 69 | >>> for pk in [p.compute_public_key() for p in participants[1:]]: 70 | ... assert public_key == pk 71 | 72 | Now each participant broadcasts his F_ij (following the notation of Pedersen) values to all other participants. 73 | These values are used to commit to the secret s_ij values send and received in the next step. 74 | 75 | >>> for pi in participants: 76 | ... for pj in participants: 77 | ... if pj != pi: 78 | ... F_ij = pj.F_ij_value() 79 | ... pi.receive_F_ij_value(F_ij) 80 | 81 | Ongoing each participant sends a share of his private secret value to every other participant SECRETLY. 82 | **Attention**: The library currently does NOT enforce this secrecy. Clients have to provide this functionality themselves. 83 | This is heavily important and the protocol does not fulfill its security guarantees otherwise (meaning it is completely broken). 84 | 85 | >>> for pi in participants: 86 | ... for pj in participants: 87 | ... if pj != pi: 88 | ... s_ij = pj.s_ij_value_for_participant(pi.id) 89 | ... pi.receive_sij(s_ij) 90 | 91 | Finally each participant can compute his `KeyShare`, which can be used for computing `PartialDecryption` or `PartialReEncryptionKey` objects. 92 | 93 | >>> shares = [p.compute_share() for p in participants] 94 | 95 | ### Encryption 96 | 97 | A message is encrypted using the public key: 98 | 99 | >>> message = 'Some secret message to be encrypted!' 100 | >>> encrypted_message = tc.encrypt_message(message, pub_key) 101 | 102 | ### Computing Partial Decryptions 103 | 104 | `t` share owners compute partial decryptions of a ciphertext using their shares: 105 | 106 | >>> partial_decryptions = [] 107 | >>> for participant in [0, 2, 4]: 108 | ... participant_share = key_shares[participant] 109 | ... partial_decryption = tc.compute_partial_decryption(encrypted_message, participant_share) 110 | ... partial_decryptions.append(partial_decryption) 111 | 112 | ### Combining Partial Decryptions 113 | 114 | Combine these partial decryptions to recover the message: 115 | 116 | >>> decrypted_message = tc.decrypt_message(partial_decryptions, encrypted_message, thresh_params) 117 | >>> print(decrypted_message) 118 | Some secret message to be encrypted! 119 | 120 | ### Updating Ciphertexts 121 | 122 | When the participants of the scheme change (adding participants, removing participants, ...) existing ciphertexts can be re-encrypted to be decryptable with the new shares. 123 | 124 | First, create the new shares (for simplicity the centralized approach is shown here, in practice you want to use distributed key generation): 125 | 126 | >>> new_pub_key, new_key_shares = tc.create_public_key_and_shares_centralized(curve_params, thresh_params) 127 | 128 | A third party computes non-secret values required for the generation of the re-encryption key for `max(t_old, t_new)` participants involved in the re-encryption key generation: 129 | 130 | >>> t_max = thresh_params.t 131 | >>> old_indices = [key_share.x for key_share in key_shares][:t_max] 132 | >>> new_indices = [key_share.x for key_share in new_key_shares][:t_max] 133 | 134 | >>> coefficients = [] 135 | >>> for p in range(1, t_max + 1): 136 | ... old_lc = tc.lagrange_coefficient_for_key_share_indices(old_indices, p, curve_params) 137 | ... new_lc = tc.lagrange_coefficient_for_key_share_indices(new_indices, p, curve_params) 138 | ... coefficients.append((old_lc, new_lc)) 139 | 140 | A number of `max(t_old, t_new)` participants now compute their partial re-encryption keys using these non-secret values and his shares: 141 | 142 | >>> partial_re_enc_keys = [] 143 | >>> for p in range(t_max): 144 | ... old_share = key_shares[p] 145 | ... new_share = new_key_shares[p] 146 | ... old_lc, new_lc = coefficients[p] 147 | ... partial_re_enc_key = tc.compute_partial_re_encryption_key(old_share, old_lc, new_share, new_lc) 148 | ... partial_re_enc_keys.append(partial_re_enc_key) 149 | 150 | The third party computes the re-encryption key by combining the partial re-encryption keys: 151 | 152 | >>> re_enc_key = tc.combine_partial_re_encryption_keys(partial_re_enc_keys, pub_key, new_pub_key, thresh_params, thresh_params) 153 | 154 | The encrypted message is re-encrypted to be decryptable by the new shares: 155 | 156 | >>> new_encrypted_message = tc.re_encrypt_message(encrypted_message, re_enc_key) 157 | 158 | Decryption can now be performed using the new shares: 159 | 160 | >>> reconstruct_shares = [new_key_shares[i] for i in [0, 2, 4]] 161 | >>> partial_decryptions = [tc.compute_partial_decryption(new_encrypted_message, share) for share in reconstruct_shares] 162 | >>> decrypted_message = tc.decrypt_message(partial_decryptions, new_encrypted_message, thresh_params) 163 | >>> print(decrypted_message) 164 | Some secret message to be encrypted! 165 | -------------------------------------------------------------------------------- /eval/performance-eval.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import datetime 3 | import os, sys 4 | import time 5 | 6 | currentdir = os.path.dirname(os.path.realpath(__file__)) 7 | parentdir = os.path.dirname(currentdir) 8 | sys.path.append(parentdir) 9 | 10 | import threshold_crypto as tc 11 | 12 | current_date_time = datetime.datetime.now().strftime("%Y%m%d-%H%M") 13 | EVAL_FILE_NAME = "eval-{}.txt".format(current_date_time) 14 | 15 | TIMING_ROUNDS = 10000 16 | GLOBAL_CP = tc.CurveParameters() 17 | GLOBAL_VAR_TP_PARAMS = [ 18 | (2, 3), 19 | (3, 5), 20 | (2, 10), 21 | (5, 10), 22 | (8, 10), 23 | (3, 20), 24 | (15, 20), 25 | (5, 50), 26 | (40, 50), 27 | ] 28 | 29 | # message_sizes_in_bytes = [32, 10 ** 3, 10 ** 4, 10 ** 5, 10 ** 6] 30 | MESSAGE_BYTE_SIZES = [5000 * i for i in range(1, 200)] 31 | #MESSAGE_BYTE_SIZES = [50000 * i for i in range(1, 20)] 32 | 33 | 34 | def write_csv(row): 35 | with open(EVAL_FILE_NAME, "a") as f: 36 | writer = csv.writer(f, ) 37 | writer.writerow(row) 38 | 39 | 40 | def eval_performance(task, params, func, timing_rounds=TIMING_ROUNDS, **kwargs): 41 | start = time.perf_counter() 42 | for _ in range(timing_rounds): 43 | func(**kwargs) 44 | stop = time.perf_counter() 45 | time_str = "{:0.3f}".format(stop - start) 46 | print(task, params, timing_rounds, time_str) 47 | write_csv([task, params, timing_rounds, time_str]) 48 | 49 | 50 | # EVALUATE CENTRAL KEY GENERATION 51 | 52 | 53 | def eval_ckg(timing_rounds): 54 | for t, n in GLOBAL_VAR_TP_PARAMS: 55 | tp = tc.ThresholdParameters(t, n) 56 | 57 | eval_performance("CKG", 58 | "({}, {})".format(t, n), 59 | tc.create_public_key_and_shares_centralized, 60 | curve_params=GLOBAL_CP, 61 | threshold_params=tp, 62 | timing_rounds=timing_rounds 63 | ) 64 | 65 | 66 | # EVALUATE DKG 67 | 68 | 69 | def run_dkg_centralized(thresh_params, curve_params): 70 | participant_ids = list(range(1, thresh_params.n + 1)) 71 | participants = [tc.Participant(id, participant_ids, curve_params, thresh_params) for id in participant_ids] 72 | 73 | # via broadcast 74 | for pi in participants: 75 | for pj in participants: 76 | if pj != pi: 77 | closed_commitment = pj.closed_commmitment() 78 | pi.receive_closed_commitment(closed_commitment) 79 | 80 | # via broadcast 81 | for pi in participants: 82 | for pj in participants: 83 | if pj != pi: 84 | open_commitment = pj.open_commitment() 85 | pi.receive_open_commitment(open_commitment) 86 | 87 | pks = [p.compute_public_key() for p in participants[1:]] 88 | 89 | # via broadcast 90 | for pi in participants: 91 | for pj in participants: 92 | if pj != pi: 93 | F_ij = pj.F_ij_value() 94 | pi.receive_F_ij_value(F_ij) 95 | 96 | # SECRETLY from i to j 97 | for pi in participants: 98 | for pj in participants: 99 | if pj != pi: 100 | s_ij = pj.s_ij_value_for_participant(pi.id) 101 | pi.receive_sij(s_ij) 102 | 103 | shares = [p.compute_share() for p in participants] 104 | 105 | 106 | def eval_dkg(timing_rounds): 107 | for t, n in GLOBAL_VAR_TP_PARAMS: 108 | tp = tc.ThresholdParameters(t, n) 109 | eval_performance("DKG", 110 | "({}, {})".format(t, n), 111 | run_dkg_centralized, 112 | timing_rounds=timing_rounds, 113 | thresh_params=tp, 114 | curve_params=GLOBAL_CP 115 | ) 116 | 117 | 118 | # EVALUATE ENCRYPTION 119 | # independent of tp, depends on message size (no huge impact) 120 | 121 | 122 | def eval_enc(timing_rounds): 123 | tp = tc.ThresholdParameters(3, 5) 124 | pub_key, shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 125 | 126 | for msg_size in MESSAGE_BYTE_SIZES: 127 | msg = "a" * msg_size # since encryption uses utf-8 encoding, this leads to messages of size msg_size 128 | eval_performance("Encrypt", 129 | "{}".format(msg_size), 130 | tc.encrypt_message, 131 | message=msg, 132 | public_key=pub_key, 133 | timing_rounds=timing_rounds 134 | ) 135 | 136 | 137 | # EVALUATE DECRYPTION depending on msg size 138 | # independent of tp, depends on message size (no huge impact) 139 | 140 | 141 | def eval_dec_msg_size(timing_rounds, t=2, n=3): 142 | tp = tc.ThresholdParameters(t, n) 143 | pub_key, shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 144 | 145 | for msg_size in MESSAGE_BYTE_SIZES: 146 | msg = "a" * msg_size 147 | enc_msg = tc.encrypt_message(msg, pub_key) 148 | pds = [tc.compute_partial_decryption(enc_msg, share) for share in shares[:t]] 149 | 150 | eval_performance("Decrypt" + str(t) + str(n), 151 | "{}".format(msg_size), 152 | tc.decrypt_message, 153 | partial_decryptions=pds, 154 | encrypted_message=enc_msg, 155 | threshold_params=tp, 156 | timing_rounds=timing_rounds 157 | ) 158 | 159 | 160 | # EVALUATE PARTIAL DECRYPTION COMPUTATION 161 | 162 | 163 | def eval_pd(timing_rounds): 164 | tp = tc.ThresholdParameters(3, 5) 165 | pub_key, shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 166 | em = tc.encrypt_message("a", pub_key) 167 | eval_performance("PartialDecryption", 168 | "", 169 | tc.compute_partial_decryption, 170 | encrypted_message=em, 171 | key_share=shares[0], 172 | timing_rounds=timing_rounds 173 | ) 174 | 175 | 176 | # EVALUATE DECRYPTION (combine partial decryptions) 177 | # we have tried it with different message sizes, but these did not matter at all. 178 | # The threshold parameters are the relevant part. 179 | 180 | # message_sizes_in_bytes = [16384, 81920, 147456] # subset of message_sizes_in_bytes from above 181 | # for (t, n), msg_size in itertools.product(diff_tp_params, message_sizes_in_bytes): 182 | 183 | 184 | def eval_dec(timing_rounds): 185 | msg_size = 1024 186 | msg = "a" * msg_size 187 | 188 | for (t, n) in GLOBAL_VAR_TP_PARAMS: 189 | tp = tc.ThresholdParameters(t, n) 190 | pub_key, shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 191 | enc_msg = tc.encrypt_message(msg, pub_key) 192 | pds = [tc.compute_partial_decryption(enc_msg, share) for share in shares[:t]] 193 | 194 | eval_performance("DecryptCombine", 195 | "({}, {})".format(t, n), 196 | tc.decrypt_message, 197 | partial_decryptions=pds, 198 | encrypted_message=enc_msg, 199 | threshold_params=tp, 200 | timing_rounds=timing_rounds 201 | ) 202 | 203 | 204 | # EVALUATE PARTIAL PROXY KEY COMPUTATION 205 | 206 | 207 | def eval_prek(): 208 | tp = tc.ThresholdParameters(5, 10) 209 | _, old_shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 210 | _, new_shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 211 | t_old_shares_x = [share.x for share in old_shares[:tp.t]] 212 | t_new_shares_x = [share.x for share in new_shares[:tp.t]] 213 | old_lc = tc.lagrange_coefficient_for_key_share_indices(t_old_shares_x, t_old_shares_x[0], GLOBAL_CP) 214 | new_lc = tc.lagrange_coefficient_for_key_share_indices(t_new_shares_x, t_new_shares_x[0], GLOBAL_CP) 215 | 216 | eval_performance("PartialReEncryptionKey", 217 | "", 218 | tc.compute_partial_re_encryption_key, 219 | old_share=old_shares[0], 220 | old_lc=old_lc, 221 | new_share=new_shares[0], 222 | new_lc=new_lc 223 | ) 224 | 225 | 226 | # EVALUATE PARTIAL PROXY KEY COMBINATION 227 | # minor impact of AS in comparison to others 228 | 229 | 230 | def eval_rek(): 231 | # for t, n in GLOBAL_VAR_TP_PARAMS: 232 | t, n = 3, 5 233 | tp = tc.ThresholdParameters(t, n) 234 | _, old_shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 235 | _, new_shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 236 | t_old_shares = old_shares[:t] 237 | t_new_shares = new_shares[:t] 238 | t_old_shares_x = [share.x for share in t_old_shares] 239 | t_new_shares_x = [share.x for share in t_new_shares] 240 | old_lc = [tc.lagrange_coefficient_for_key_share_indices(t_old_shares_x, s, GLOBAL_CP) for s in t_old_shares_x] 241 | new_lc = [tc.lagrange_coefficient_for_key_share_indices(t_new_shares_x, s, GLOBAL_CP) for s in t_new_shares_x] 242 | prek = [] 243 | for os, olc, ns, nlc in zip(t_old_shares, old_lc, t_new_shares, new_lc): 244 | prek.append(tc.compute_partial_re_encryption_key(os, olc, ns, nlc)) 245 | 246 | eval_performance("ReEncryptionKeyCombination", 247 | "({}, {})".format(t, n), 248 | tc.combine_partial_re_encryption_keys, 249 | partial_keys=prek, 250 | old_threshold_params=tp, 251 | new_threshold_params=tp 252 | ) 253 | 254 | 255 | # EVALUATE RE_ENCRYPTION 256 | 257 | 258 | def eval_reenc(timing_rounds): 259 | tp = tc.ThresholdParameters(5, 10) 260 | 261 | old_pub_key, old_shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 262 | new_pub_key, new_shares = tc.create_public_key_and_shares_centralized(GLOBAL_CP, tp) 263 | t_old_shares = old_shares[:tp.t] 264 | t_new_shares = new_shares[:tp.t] 265 | t_old_shares_x = [share.x for share in t_old_shares] 266 | t_new_shares_x = [share.x for share in t_new_shares] 267 | old_lc = [tc.lagrange_coefficient_for_key_share_indices(t_old_shares_x, s, GLOBAL_CP) for s in t_old_shares_x] 268 | new_lc = [tc.lagrange_coefficient_for_key_share_indices(t_new_shares_x, s, GLOBAL_CP) for s in t_new_shares_x] 269 | prek = [] 270 | for os, olc, ns, nlc in zip(t_old_shares, old_lc, t_new_shares, new_lc): 271 | prek.append(tc.compute_partial_re_encryption_key(os, olc, ns, nlc)) 272 | re_encryption_key = tc.combine_partial_re_encryption_keys(prek, old_pub_key, new_pub_key, tp, tp) 273 | 274 | em = tc.encrypt_message("a", old_pub_key) 275 | 276 | eval_performance("ReEncrypt", 277 | "", 278 | tc.re_encrypt_message, 279 | em=em, 280 | re_key=re_encryption_key, 281 | timing_rounds=timing_rounds 282 | ) 283 | 284 | 285 | # MAIN RUN 286 | 287 | 288 | def main(): 289 | write_csv(['task', 'parameters', 'rounds', 'time']) 290 | 291 | eval_ckg(timing_rounds=1) 292 | eval_dkg(timing_rounds=1) 293 | eval_enc(1000) 294 | eval_dec_msg_size(1000) 295 | eval_dec_msg_size(1000, t=3, n=5) 296 | eval_dec_msg_size(1000, t=2, n=10) 297 | eval_dec(timing_rounds=1000) 298 | eval_pd(timing_rounds=1000) 299 | eval_prek() 300 | eval_rek() 301 | eval_reenc(timing_rounds=1000) 302 | 303 | print("Done! Written to {}".format(EVAL_FILE_NAME)) 304 | 305 | 306 | if __name__ == '__main__': 307 | main() 308 | -------------------------------------------------------------------------------- /threshold_crypto/central.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import nacl.utils 4 | import nacl.secret 5 | import nacl.encoding 6 | import nacl.exceptions 7 | import nacl.hash 8 | from Crypto.PublicKey import ECC 9 | 10 | from threshold_crypto.data import (CurveParameters, 11 | ThresholdParameters, 12 | KeyShare, 13 | PartialDecryption, 14 | EncryptedMessage, 15 | ThresholdCryptoError, 16 | PartialReEncryptionKey, 17 | ReEncryptionKey, 18 | PublicKey, 19 | LagrangeCoefficient) 20 | from threshold_crypto import number 21 | 22 | 23 | # key generation 24 | 25 | 26 | def create_public_key_and_shares_centralized(curve_params: CurveParameters, 27 | threshold_params: ThresholdParameters) -> (PublicKey, List[KeyShare]): 28 | """ 29 | Creates a public key and n shares by choosing a random secret key and using it for computations. 30 | 31 | :param curve_params: curve parameters to use 32 | :param threshold_params: parameters t and n for the threshold scheme 33 | :return: (the public key, n key shares) 34 | """ 35 | d = number.random_in_range(1, curve_params.order) 36 | Q = d * curve_params.P 37 | pk = PublicKey(Q, curve_params) 38 | 39 | # Perform Shamir's secret sharing in Z_q 40 | polynom = number.PolynomMod.create_random_polynom(d, threshold_params.t - 1, curve_params.order) 41 | supporting_points = range(1, threshold_params.n + 1) 42 | shares = [KeyShare(x, polynom.evaluate(x), curve_params) for x in supporting_points] 43 | 44 | return pk, shares 45 | 46 | 47 | def _restore_priv_key(curve_params: CurveParameters, shares: List[KeyShare], treshold_params: ThresholdParameters) -> int: 48 | """ 49 | Combine multiple key shares to compute the (implicit) private key. 50 | 51 | ATTENTION: Just used for testing purposes - should never be used in a real scenario, if you don't have a special reason for this! 52 | 53 | :param curve_params: 54 | :param shares: 55 | :param treshold_params: 56 | :return: 57 | """ 58 | used_shares = shares[:treshold_params.t] 59 | x_shares = [share.x for share in used_shares] 60 | y_shares = [share.y for share in used_shares] 61 | 62 | lagrange_coefficients = [lagrange_coefficient_for_key_share_indices(x_shares, idx, curve_params) for idx in x_shares] 63 | 64 | restored_secret = sum([(lagrange_coefficients[i].coefficient * y_shares[i]) for i in range(0, len(used_shares))]) % curve_params.order 65 | 66 | return restored_secret 67 | 68 | 69 | # encryption 70 | 71 | 72 | def encrypt_message(message: str, public_key: PublicKey) -> EncryptedMessage: 73 | """ 74 | Encrypt a message using a public key. A hybrid encryption approach is used to include advantages of symmetric 75 | encryption (fast, independent of message-length, integrity-preserving by using AE-scheme). 76 | Internally a combination of Salsa20 and Poly1305 from the cryptographic library NaCl is used. 77 | 78 | :param message: the message to be encrypted 79 | :param public_key: the public key 80 | :return: the encrypted message 81 | """ 82 | curve_params = public_key.curve_params 83 | encoded_message = bytes(message, 'utf-8') 84 | 85 | # Create random subgroup element and use its hash as symmetric key to prevent 86 | # attacks described in "Why Textbook ElGamal and RSA Encryption Are Insecure" 87 | # by Boneh et. al. 88 | r = number.random_in_range(1, curve_params.order) 89 | key_point = r * curve_params.P 90 | point_bytes = _key_bytes_from_point(key_point) 91 | 92 | try: 93 | symmetric_key = nacl.hash.blake2b(point_bytes, 94 | digest_size=nacl.secret.SecretBox.KEY_SIZE, 95 | encoder=nacl.encoding.RawEncoder) 96 | # Use derived symmetric key to encrypt the message 97 | box = nacl.secret.SecretBox(symmetric_key) 98 | encrypted = box.encrypt(encoded_message) 99 | except nacl.exceptions.CryptoError as e: 100 | print('Encryption failed: ' + str(e)) 101 | raise ThresholdCryptoError('Message encryption failed.') 102 | 103 | # Use threshold scheme to encrypt the curve point used as hash input to derive the symmetric key 104 | C1, C2 = _encrypt_key_point(key_point, public_key.Q, curve_params) 105 | 106 | return EncryptedMessage(C1, C2, encrypted) 107 | 108 | 109 | def _key_bytes_from_point(p: ECC.EccPoint) -> bytes: 110 | key_point_byte_length = (int(p.x).bit_length() + 7) // 8 111 | point_bytes = int(p.x).to_bytes(key_point_byte_length, byteorder='big') 112 | return point_bytes 113 | 114 | 115 | def _encrypt_key_point(key_point: ECC.EccPoint, 116 | Q: ECC.EccPoint, 117 | curve_params: CurveParameters) -> (ECC.EccPoint, ECC.EccPoint): 118 | k = number.random_in_range(1, curve_params.order) 119 | C1 = k * curve_params.P 120 | kQ = k * Q 121 | C2 = key_point + kQ 122 | 123 | return C1, C2 124 | 125 | 126 | # decryption 127 | 128 | 129 | def decrypt_message(partial_decryptions: List[PartialDecryption], 130 | encrypted_message: EncryptedMessage, 131 | threshold_params: ThresholdParameters 132 | ) -> str: 133 | """ 134 | Decrypt a message using the combination of at least t partial decryptions. Similar to the encryption process 135 | the hybrid approach is used for decryption. 136 | 137 | :param partial_decryptions: at least t partial decryptions 138 | :param encrypted_message: the encrapted message to be decrypted 139 | :param threshold_params: the used threshold parameters 140 | :return: the decrypted message 141 | """ 142 | if len(partial_decryptions) < threshold_params.t: 143 | raise ThresholdCryptoError('less than t partial decryptions given') 144 | 145 | return _decrypt_message(partial_decryptions, encrypted_message) 146 | 147 | 148 | def _decrypt_message(partial_decryptions: List[PartialDecryption], 149 | encrypted_message: EncryptedMessage, 150 | ) -> str: 151 | # this method does not contain the check for given number of partial decryptions to allow testing the failing decryption 152 | curve_params = partial_decryptions[0].curve_params 153 | for partial_key in partial_decryptions: 154 | if partial_key.curve_params != curve_params: 155 | raise ThresholdCryptoError("Varying curve parameters found in partial re-encryption keys") 156 | 157 | key_point = _combine_shares( 158 | partial_decryptions, 159 | encrypted_message, 160 | curve_params, 161 | ) 162 | point_bytes = _key_bytes_from_point(key_point) 163 | 164 | try: 165 | key = nacl.hash.blake2b(point_bytes, 166 | digest_size=nacl.secret.SecretBox.KEY_SIZE, 167 | encoder=nacl.encoding.RawEncoder) 168 | box = nacl.secret.SecretBox(key) 169 | encoded_plaintext = box.decrypt(encrypted_message.ciphertext) 170 | except nacl.exceptions.CryptoError as e: 171 | raise ThresholdCryptoError('Message decryption failed. Internal: ' + str(e)) 172 | 173 | return str(encoded_plaintext, 'utf-8') 174 | 175 | 176 | def _combine_shares(partial_decryptions: List[PartialDecryption], 177 | encrypted_message: EncryptedMessage, 178 | curve_params: CurveParameters, 179 | ) -> ECC.EccPoint: 180 | # compute lagrange coefficients 181 | partial_indices = [dec.x for dec in partial_decryptions] 182 | lagrange_coefficients = [lagrange_coefficient_for_key_share_indices(partial_indices, idx, curve_params) for idx in partial_indices] 183 | 184 | summands = [lagrange_coefficients[i].coefficient * partial_decryptions[i].yC1 for i in range(0, len(partial_decryptions))] 185 | restored_kdP = number.ecc_sum(summands) 186 | 187 | restored_point = encrypted_message.C2 + (-restored_kdP) 188 | 189 | return restored_point 190 | 191 | 192 | # re-encryption 193 | 194 | 195 | def lagrange_coefficient_for_key_share_indices(key_share_indices: List[int], 196 | p_idx: int, 197 | curve_params: CurveParameters) -> LagrangeCoefficient: 198 | """ 199 | Create the ith Lagrange coefficient for a list of key shares. 200 | 201 | :param key_share_indices: the used indices for the participants key shares 202 | :param curve_params: the used curve parameters 203 | :param p_idx: the participant index (= the shares x value), the Lagrange coefficient belongs to 204 | :return: 205 | """ 206 | if p_idx not in key_share_indices: 207 | raise ThresholdCryptoError("Participant index {} not found in used indices {} for computation of Lagrange coefficient".format(p_idx, key_share_indices)) 208 | 209 | idx_len = len(key_share_indices) 210 | i = key_share_indices.index(p_idx) 211 | 212 | def x(idx): 213 | return key_share_indices[idx] 214 | 215 | tmp = [(- x(j) * number.prime_mod_inv(x(i) - x(j), curve_params.order)) for j in range(0, idx_len) if not j == i] 216 | coefficient = number.prod(tmp) % curve_params.order # lambda_i 217 | 218 | return LagrangeCoefficient(p_idx, key_share_indices, coefficient) 219 | 220 | 221 | def combine_partial_re_encryption_keys(partial_keys: List[PartialReEncryptionKey], 222 | old_public_key: PublicKey, 223 | new_public_key: PublicKey, 224 | old_threshold_params: ThresholdParameters, 225 | new_threshold_params: ThresholdParameters) -> ReEncryptionKey: 226 | """ 227 | Combine a number of partial re-encryption keys yielding the re-encryption key. 228 | 229 | :param partial_keys: The partial keys as provided by participants 230 | :param old_public_key: the public key of the old access structure 231 | :param new_public_key: the public key of the new access structure 232 | :param old_threshold_params: the threshold parameters of the old access structure 233 | :param new_threshold_params: the threshold parameters of the new access structure 234 | :return: the re-encryption key 235 | """ 236 | # TODO check threshold parameters 237 | 238 | max_t = max(old_threshold_params.t, new_threshold_params.t) 239 | 240 | if len(partial_keys) < max_t: 241 | raise ThresholdCryptoError("Not enough partial re-encryption keys given") 242 | 243 | curve_params = partial_keys[0].curve_params 244 | for partial_key in partial_keys: 245 | if partial_key.curve_params != curve_params: 246 | raise ThresholdCryptoError("Varying curve parameters found in partial re-encryption keys") 247 | 248 | re_key = sum([k.partial_key for k in partial_keys]) % curve_params.order 249 | 250 | # check that the proxy key is valid using the given public keys 251 | # proxy_key * P =?= new_pub - old_pub 252 | checkval1 = curve_params.P * re_key 253 | checkval2 = new_public_key.Q + (-old_public_key.Q) 254 | 255 | if checkval1 != checkval2: 256 | raise ThresholdCryptoError("The combined proxy key is invalid for given public keys.") 257 | 258 | return ReEncryptionKey(re_key, curve_params) 259 | 260 | 261 | def re_encrypt_message(em: EncryptedMessage, re_key: ReEncryptionKey) -> EncryptedMessage: 262 | """ 263 | Re-encrypts a message using the provided re-encryption key. 264 | 265 | :param em: the message 266 | :param re_key: the re-encryption key 267 | :return: 268 | """ 269 | re_enc_c = em.C2 + em.C1 * re_key.key 270 | 271 | return EncryptedMessage(em.C1, re_enc_c, em.ciphertext) 272 | -------------------------------------------------------------------------------- /eval/draw_run.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from collections import namedtuple 4 | 5 | import pandas 6 | import numpy as np 7 | import seaborn as sns 8 | import matplotlib.pyplot as plt 9 | from matplotlib.axes import Axes 10 | from matplotlib.figure import Figure 11 | 12 | FigureParameter = namedtuple('FigureParameter', ['tasks', 13 | 'title', 14 | 'filename', 15 | 'use_combined' 16 | ] 17 | ) 18 | 19 | 20 | def autolabel(rects, ax): 21 | """Attach a text label above each bar in *rects*, displaying its height.""" 22 | bar_color = rects[0].get_facecolor() 23 | 24 | for rect in rects: 25 | height = rect.get_height() 26 | ax.annotate('{:0.3f}'.format(height), 27 | xy=(rect.get_x() + rect.get_width() / 2, height), 28 | xytext=(0, 3), # 3 points vertical offset 29 | textcoords="offset points", 30 | rotation=90, 31 | ha='center', 32 | va='bottom', 33 | color=bar_color) 34 | 35 | 36 | def plot_bar(df, fig_params: FigureParameter): 37 | f, ax = plt.subplots() 38 | 39 | t_data = df.loc[df.task.isin(fig_params.tasks)] 40 | 41 | if len(t_data) == 0: 42 | print("No data for {}".format(fig_params.tasks)) 43 | return 44 | 45 | # plot = sns.barplot(data=t_data, x='combined', y='time', ax=ax, color='#1979a9') 46 | x_data = t_data.combined if fig_params.use_combined else t_data.parameters 47 | plot = ax.bar(x=x_data, height=t_data.time, color='#1979a9') 48 | 49 | ax.spines['top'].set_visible(False) 50 | ax.spines['right'].set_visible(False) 51 | ax.spines['left'].set_visible(False) 52 | ax.spines['bottom'].set_color('#DDDDDD') 53 | 54 | bar_color = plot[0].get_facecolor() 55 | 56 | # Add text annotations to the top of the bars. 57 | # Note, you'll have to adjust this slightly (the 0.3) 58 | # with different data. 59 | for bar in plot: 60 | ax.text( 61 | bar.get_x() + bar.get_width() / 2, 62 | bar.get_height() + 0.3, 63 | round(bar.get_height(), 1), 64 | horizontalalignment='center', 65 | color=bar_color, 66 | weight='bold' 67 | ) 68 | 69 | ax.tick_params(axis='x', rotation=90) 70 | 71 | ax.set_xlabel('Operation', labelpad=15, color='#333333') 72 | ax.set_ylabel('Time', labelpad=15, color='#333333') 73 | ax.set_title(fig_params.title, pad=15, color='#333333', weight='bold') 74 | 75 | f.tight_layout() 76 | print("Saving {}".format(fig_params.filename)) 77 | f.savefig(fig_params.filename) 78 | 79 | 80 | def main(): 81 | parser = argparse.ArgumentParser(description='draw a performance run') 82 | parser.add_argument('path', type=str, help='the file path for the performance evaluation output file') 83 | args = parser.parse_args() 84 | filepath = args.path 85 | 86 | df = pandas.read_csv(filepath, dtype={'parameters': str}, engine='c') 87 | df.parameters = df.parameters.fillna('') 88 | df["combined"] = df["task"].astype(str) + df["parameters"] 89 | 90 | print(df) 91 | 92 | # make dir for figures 93 | dirpath = filepath[:-4] 94 | if not os.path.exists(dirpath): 95 | os.mkdir(dirpath) 96 | 97 | def imgpath(imagefilename): 98 | return os.path.join(dirpath, imagefilename) 99 | 100 | # single figures 101 | 102 | dkg_task = ["DKG", "CKG"] 103 | decrypt_task = ["DecryptCombine"] 104 | diverse_tasks = [ 105 | "ReEncrypt", 106 | "PartialDecryption", 107 | "PartialReEncryptionKey", 108 | "ReEncryptionKeyCombination", 109 | ] 110 | 111 | figures = [ 112 | FigureParameter(dkg_task, "Distributed key generation", imgpath("dkg.png"), False), 113 | FigureParameter(decrypt_task, "Decryption", imgpath("dec.png"), False), 114 | FigureParameter(diverse_tasks, "Remaining operations", imgpath("divers.png"), True), 115 | ] 116 | 117 | # for fig_params in figures: 118 | # plot_bar(df, fig_params) 119 | 120 | """ 121 | performed on "daily business" or multiple times: 122 | - encryption: just depends on message size (slightly), not on AS 123 | - re-encryption: does not depend on msg size or AS 124 | 125 | performed on a one-by-one basis: 126 | - partial decryption: does not depend on msg size or AS 127 | - decrypt (combine): does depend on AS (huge impact) and msg size (low impact) 128 | 129 | performed once in a while: 130 | - dkg: huge AS impact 131 | - ckg: AS impact 132 | - prek: no AS dependency 133 | - rekc: irrelevant AS impact 134 | 135 | figures: 136 | - enc: msg size 137 | - ckg (1), dkg(1), dec(1000): AS impact 138 | 139 | - diverse: 140 | - partial decryption: ONE 141 | - prek: ONE 142 | - rekc: ONE 143 | - re-encrypt (relevant) 144 | 145 | ALTERNATIVE just relevant: 146 | - dkg: AS (just one run?), maybe include ckg 147 | - enc, re-encrypt 148 | missing: ckg(?), dec, pdec, prek, rekc 149 | """ 150 | 151 | f = draw_enc_dec(df) 152 | print("Saving {}".format(imgpath("enc_dec_line.png"))) 153 | f.savefig(imgpath("enc_dec_line.png")) 154 | 155 | f = draw_dkg_ckg_dec(df) 156 | print("Saving {}".format(imgpath("dkg_dec.png"))) 157 | f.savefig(imgpath("dkg_dec.png")) 158 | 159 | f = draw_dkg_or_dec(df, "DKG", "DKG") 160 | print("Saving {}".format(imgpath("dkg.png"))) 161 | f.savefig(imgpath("dkg.png")) 162 | 163 | f = draw_dkg_or_dec(df, "DecryptCombine", "Combine") 164 | print("Saving {}".format(imgpath("dec.png"))) 165 | f.savefig(imgpath("dec.png")) 166 | 167 | f = draw_enc_dec_re_pdec(df) 168 | print("Saving {}".format(imgpath("enc_dec_re_pdec.png"))) 169 | f.savefig(imgpath("enc_dec_re_pdec.png")) 170 | 171 | 172 | def draw_enc_dec(df): 173 | res = plt.subplots() 174 | f = res[0] 175 | ax: Axes = res[1] 176 | 177 | draw_enc_dec_on_ax(df, ax) 178 | f.tight_layout() 179 | 180 | return f 181 | 182 | 183 | def draw_enc_dec_on_ax(df, ax: Axes): 184 | enc_data = df.loc[df.task == 'Encrypt'] 185 | dec23_data = df.loc[df.task == 'Decrypt23'] 186 | dec35_data = df.loc[df.task == 'Decrypt35'] 187 | dec210_data = df.loc[df.task == 'Decrypt210'] 188 | 189 | plot1 = ax.plot(enc_data.parameters.astype(int), enc_data.time / enc_data.rounds, label="Encrypt") # , color='#FF0000') 190 | plot2 = ax.plot(enc_data.parameters.astype(int), dec23_data.time / dec23_data.rounds, label="Combine for (2,3)-scheme") # , color='#00FF00') 191 | plot3 = ax.plot(enc_data.parameters.astype(int), dec210_data.time / dec210_data.rounds, label="Combine for (2,10)-scheme") # , color='#00FF00') 192 | plot4 = ax.plot(enc_data.parameters.astype(int), dec35_data.time / dec35_data.rounds, label="Combine for (3,5)-scheme") # , color='#00FF00') 193 | 194 | ax.spines['top'].set_visible(False) 195 | ax.spines['right'].set_visible(False) 196 | ax.spines['left'].set_color('#DDDDDD') 197 | ax.spines['bottom'].set_color('#DDDDDD') 198 | 199 | ax.grid(axis='y', color="#DDDDDD") 200 | 201 | ax.tick_params(axis='both', color="#DDDDDD") # rotation=45, 202 | ax.ticklabel_format(style='plain') 203 | ax.set_ybound(lower=0, upper=ax.get_ybound()[1]) 204 | ax.set_xbound(lower=0, upper=ax.get_xbound()[1]) 205 | 206 | ax.set_xlabel('plaintext length [byte]', labelpad=15, color='#333333') 207 | ax.set_ylabel('time [s]', labelpad=15, color='#333333') 208 | 209 | ax.legend() 210 | 211 | 212 | def draw_dkg_or_dec(df, dkg_dec, label) -> Figure: 213 | f, ax = plt.subplots() 214 | draw_dkg_on_ax(df, ax, dkg_dec, label) 215 | f.tight_layout() 216 | return f 217 | 218 | 219 | def draw_dkg_on_ax(df, ax, dkg_dec, label): 220 | dkg_data = df.loc[df.task == dkg_dec] 221 | 222 | x = np.arange(len(dkg_data)) 223 | print(dkg_data.parameters) 224 | width = 0.5 225 | bar_dkg = ax.bar(x, dkg_data.time / dkg_data.rounds, width, label=label) 226 | 227 | # annotate bars with their respective values 228 | autolabel(bar_dkg, ax) 229 | 230 | # set graph "borders" 231 | ax.spines['top'].set_visible(False) 232 | ax.spines['right'].set_visible(False) 233 | ax.spines['left'].set_color('#DDDDDD') 234 | ax.spines['bottom'].set_color('#DDDDDD') 235 | 236 | # include grid lines behind bars 237 | ax.set_axisbelow(True) 238 | ax.grid(axis='y', color="#DDDDDD") 239 | 240 | # ticks and labels 241 | ax.tick_params(axis='x', rotation=90) 242 | ax.tick_params(axis='both', color="#DDDDDD") 243 | ax.set_xticks(x) 244 | ax.set_xticklabels(dkg_data.parameters) 245 | 246 | # axis labels and title 247 | ax.set_xlabel('used (t,n)-scheme', labelpad=15, color='#333333') 248 | ax.set_ylabel('time [s]', labelpad=15, color='#333333') 249 | 250 | # insert legend 251 | ax.legend() 252 | 253 | 254 | def draw_dkg_ckg_dec(df) -> Figure: 255 | f, ax = plt.subplots() 256 | draw_dkg_ckg_dec_on_ax(df, ax) 257 | f.tight_layout() 258 | return f 259 | 260 | 261 | def draw_dkg_ckg_dec_on_ax(df, ax): 262 | tasks = ["DKG", "CKG", "DecryptCombine"] 263 | dkg_data = df.loc[df.task == 'DKG'] 264 | ckg_data = df.loc[df.task == 'CKG'] 265 | dec_data = df.loc[df.task == 'DecryptCombine'] 266 | 267 | x = np.arange(len(dkg_data)) 268 | width = 0.2 269 | 270 | bar_ckg = ax.bar(x - 1.2 * width, ckg_data.time, width, label="CKG") 271 | bar_dkg = ax.bar(x, dkg_data.time, width, label="DKG") 272 | bar_dec = ax.bar(x + 1.2 * width, dec_data.time, width, label="DEC") 273 | 274 | # annotate bars with their respective values 275 | autolabel(bar_dkg, ax) 276 | autolabel(bar_ckg, ax) 277 | autolabel(bar_dec, ax) 278 | 279 | # set graph "borders" 280 | ax.spines['top'].set_visible(False) 281 | ax.spines['right'].set_visible(False) 282 | ax.spines['left'].set_color('#DDDDDD') 283 | ax.spines['bottom'].set_color('#DDDDDD') 284 | 285 | # include grid lines behind bars 286 | ax.set_axisbelow(True) 287 | ax.grid(axis='y', color="#DDDDDD") 288 | 289 | # ticks and labels 290 | ax.tick_params(axis='x', rotation=90) 291 | ax.tick_params(axis='both', color="#DDDDDD") 292 | ax.set_xticks(x) 293 | ax.set_xticklabels(dkg_data.parameters) 294 | 295 | # axis labels and title 296 | ax.set_xlabel('used (t,n)-scheme', labelpad=15, color='#333333') 297 | ax.set_ylabel('time [s]', labelpad=15, color='#333333') 298 | # ax.set_title("TITLE", pad=15, color='#333333', weight='bold') 299 | 300 | # insert legend 301 | ax.legend() 302 | 303 | 304 | def draw_re_pdec_on_ax(df, ax: Axes): 305 | diverse_tasks = [ 306 | "ReEncrypt", 307 | "PartialDecryption", 308 | #"PartialReEncryptionKey", 309 | #"ReEncryptionKeyCombination", 310 | ] 311 | t_data = df.loc[df.task.isin(diverse_tasks)] 312 | x_data = t_data.task 313 | x_data = pandas.Series(["PD", "RE"]) 314 | 315 | plot = ax.bar(x=x_data, height=t_data.time, width=0.2) # , color='#214355') 316 | 317 | ax.spines['top'].set_visible(False) 318 | ax.spines['right'].set_visible(False) 319 | ax.spines['left'].set_visible(False) 320 | ax.spines['bottom'].set_color('#DDDDDD') 321 | 322 | autolabel(plot, ax) 323 | 324 | # include grid lines behind bars 325 | ax.set_axisbelow(True) 326 | ax.grid(axis='y', color="#DDDDDD") 327 | 328 | # ticks and labels 329 | ax.tick_params(axis='x', rotation=90) 330 | ax.tick_params(axis='both', color="#DDDDDD") 331 | ax.yaxis.set_tick_params(labelleft=False) 332 | 333 | 334 | def draw_enc_dec_re_pdec(df): 335 | f: Figure = plt.figure(constrained_layout=True) 336 | widths = [9, 1] 337 | spec = f.add_gridspec(ncols=2, nrows=1, width_ratios=widths) 338 | ax0 = f.add_subplot(spec[0, 0]) 339 | ax1 = f.add_subplot(spec[0, 1], sharey=ax0) 340 | 341 | draw_enc_dec_on_ax(df, ax0) 342 | draw_re_pdec_on_ax(df, ax1) 343 | 344 | return f 345 | 346 | 347 | if __name__ == '__main__': 348 | main() 349 | -------------------------------------------------------------------------------- /threshold_crypto/participant.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | from Crypto.PublicKey import ECC 4 | from Crypto.Random import random 5 | from Crypto.Hash import SHA3_256 6 | 7 | from threshold_crypto.data import (EncryptedMessage, 8 | KeyShare, 9 | PartialDecryption, 10 | PartialReEncryptionKey, 11 | ThresholdCryptoError, 12 | CurveParameters, 13 | ThresholdParameters, 14 | LagrangeCoefficient, 15 | DkgClosedCommitment, 16 | DkgOpenCommitment, 17 | DkgSijValue, 18 | DkgFijValue, 19 | PublicKey) 20 | from threshold_crypto import number 21 | 22 | 23 | ParticipantId = int 24 | 25 | 26 | def compute_partial_decryption(encrypted_message: EncryptedMessage, key_share: KeyShare) -> PartialDecryption: 27 | """ 28 | Compute the partial decryption of an encrypted message using a key share. 29 | 30 | :param encrypted_message: the encrypted message 31 | :param key_share: the key share 32 | :return: a partial decryption 33 | """ 34 | yC1 = encrypted_message.C1 * key_share.y 35 | 36 | return PartialDecryption(key_share.x, yC1, key_share.curve_params) 37 | 38 | 39 | def compute_partial_re_encryption_key(old_share: KeyShare, old_lc: LagrangeCoefficient, new_share: KeyShare, new_lc: LagrangeCoefficient) -> PartialReEncryptionKey: 40 | """ 41 | Compute a partial re-encryption key from a participants old and new share. 42 | 43 | :param old_share: the participants "old" key share 44 | :param old_lc: "old" lagrange coefficient provided by the coordinating party 45 | :param new_share: the participants "new" key share 46 | :param new_lc: "new" lagrange coefficient provided by the coordinating party 47 | :return: the partial re-encryption key 48 | """ 49 | curve_params = old_share.curve_params 50 | 51 | if curve_params != new_share.curve_params: 52 | raise ThresholdCryptoError('Differing curves not supported for re-encryption!') 53 | 54 | if old_share.x != old_lc.participant_index: 55 | raise ThresholdCryptoError('Lagrange coefficient for OLD share was computed for other participant index') 56 | 57 | if new_share.x != new_lc.participant_index: 58 | raise ThresholdCryptoError('Lagrange coefficient for NEW share was computed for other participant index') 59 | 60 | partial_re_key = (new_share.y * new_lc.coefficient - old_share.y * old_lc.coefficient) % curve_params.order 61 | 62 | return PartialReEncryptionKey(partial_re_key, curve_params) 63 | 64 | 65 | class Participant: 66 | """ 67 | A Participant provides the interface for a participant in the distributed key generation (DKG) protocol of Pedersen91. 68 | Required values for other participants can be obtained and functionality to receive these values is offered. 69 | A multitude of checks are performed to prevent illegal state or actions during the protocol. However, several communication 70 | aspects as e.g., the secure transport of s_ij values to other participants, are not included and have to be assured 71 | by users of this class. 72 | """ 73 | 74 | _COMMITMENT_RANDOM_BITS = 256 75 | 76 | def __init__(self, own_id: ParticipantId, all_participant_ids: List[ParticipantId], curve_params: CurveParameters, threshold_params: ThresholdParameters): 77 | """ 78 | Initialize a participant. 79 | 80 | :param own_id: the id of this participant. 81 | As this id is used as the final shares x value, it's important that participants use distinct ids. 82 | :param all_participant_ids: a list of all 83 | :param curve_params: the curve parameters used 84 | :param threshold_params: the required threshold parameters 85 | """ 86 | if len(set(all_participant_ids)) != threshold_params.n: 87 | raise ThresholdCryptoError("List of distinct participant ids has length {} != {} = n".format(len(all_participant_ids), threshold_params.n)) 88 | 89 | if own_id not in all_participant_ids: 90 | raise ThresholdCryptoError("Own id must be contained in all participant ids") 91 | 92 | self.all_participant_ids: List[ParticipantId] = all_participant_ids 93 | self.id: ParticipantId = own_id 94 | self.curve_params: CurveParameters = curve_params 95 | self.threshold_params: ThresholdParameters = threshold_params 96 | 97 | self._x_i: int = number.random_in_range(0, curve_params.order) 98 | self._h_i: ECC.EccPoint = self._x_i * curve_params.P 99 | self._polynom: number.PolynomMod = number.PolynomMod.create_random_polynom(self._x_i, self.threshold_params.t - 1, curve_params.order) 100 | 101 | # calculate own F_ij values 102 | self._local_F_ij: List[ECC.EccPoint] = [] 103 | for coeff in self._polynom.coefficients: 104 | self._local_F_ij.append(coeff * curve_params.P) 105 | 106 | # calculate own s_ij values 107 | self._local_sij: Dict[ParticipantId, int] = {} 108 | for p_id in self.all_participant_ids: 109 | s_ij = self._polynom.evaluate(p_id) 110 | self._local_sij[p_id] = s_ij 111 | 112 | # random value for commitment of h_i 113 | rand_int = random.getrandbits(self._COMMITMENT_RANDOM_BITS) 114 | self._commitment_random: bytes = number.int_to_bytes(rand_int) 115 | self._commitment: bytes = self._compute_commitment(self._commitment_random, self._h_i) 116 | 117 | self._received_closed_commitments: Dict[ParticipantId, DkgClosedCommitment] = { 118 | self.id: self.closed_commmitment() 119 | } 120 | self._received_open_commitments: Dict[ParticipantId, DkgOpenCommitment] = { 121 | self.id: self._unchecked_open_commitment() 122 | } 123 | self._received_F: Dict[ParticipantId, DkgFijValue] = { 124 | self.id: self.F_ij_value() 125 | } 126 | self._received_sij: Dict[ParticipantId, DkgSijValue] = { 127 | self.id: self._unchecked_s_ij_value_for_participant(self.id) 128 | } 129 | 130 | self._s_i: Optional[int] = None 131 | self.key_share: Optional[KeyShare] = None 132 | 133 | @staticmethod 134 | def _compute_commitment(commitment_random: bytes, h_i: ECC.EccPoint): 135 | hash_fct = SHA3_256.new(commitment_random) 136 | hash_fct.update(number.int_to_bytes(int(h_i.x))) 137 | hash_fct.update(number.int_to_bytes(int(h_i.y))) 138 | return hash_fct.digest() 139 | 140 | def closed_commmitment(self) -> DkgClosedCommitment: 141 | """ 142 | The participants closed commitment to the "public key share" h_i. 143 | 144 | :return: the closed commitment 145 | """ 146 | return DkgClosedCommitment(self.id, self._commitment) 147 | 148 | def receive_closed_commitment(self, commitment: DkgClosedCommitment): 149 | """ 150 | Receive a closed commitment to the "public key share" h_i of another participant. 151 | 152 | :param commitment: the received commitment 153 | """ 154 | source_id = commitment.participant_id 155 | 156 | if source_id not in self.all_participant_ids: 157 | raise ThresholdCryptoError("Received closed commitment from unknown participant id {}".format(source_id)) 158 | 159 | if source_id == self.id: 160 | raise ThresholdCryptoError("Received own closed commitment - don't do this") 161 | 162 | if source_id not in self._received_closed_commitments: 163 | self._received_closed_commitments[source_id] = commitment 164 | else: 165 | raise ThresholdCryptoError("Closed commitment from participant {} already received".format(source_id)) 166 | 167 | def open_commitment(self) -> DkgOpenCommitment: 168 | """ 169 | The participants open commitment to the "public key share" h_i, which can be evaluated. 170 | 171 | :return: the open commitment 172 | """ 173 | if len(self._received_closed_commitments) != self.threshold_params.n: 174 | raise ThresholdCryptoError( 175 | "Open commitment is just accessible when all other closed commitments were received") 176 | 177 | return self._unchecked_open_commitment() 178 | 179 | def _unchecked_open_commitment(self) -> DkgOpenCommitment: 180 | # This method is provided so that it can be used for the own commitment computation in the __init__ 181 | # method without performing the check in open_commitment. 182 | return DkgOpenCommitment(self.id, self._commitment, self._h_i, self._commitment_random) 183 | 184 | def receive_open_commitment(self, open_commitment: DkgOpenCommitment): 185 | """ 186 | Receive an open (evaluatable) commitment to the "public key share" h_i of another participant. 187 | 188 | :param open_commitment: the received commitment 189 | """ 190 | source_id = open_commitment.participant_id 191 | 192 | if source_id not in self.all_participant_ids: 193 | raise ThresholdCryptoError("Received open commitment from unknown participant id {}".format(source_id)) 194 | 195 | if source_id == self.id: 196 | raise ThresholdCryptoError("Received own open commitment - don't do this") 197 | 198 | if source_id not in self._received_closed_commitments: 199 | raise ThresholdCryptoError("Received open commitment from participant id {} withput received closed commitment".format(source_id)) 200 | 201 | closed_commitment = self._received_closed_commitments[source_id] 202 | 203 | if closed_commitment.commitment != open_commitment.commitment: 204 | raise ThresholdCryptoError("Open and close commitment values differ for participant {}".format(source_id)) 205 | 206 | if self._compute_commitment(open_commitment.r, open_commitment.h_i) != closed_commitment.commitment: 207 | raise ThresholdCryptoError("Invalid commitment for participant {}".format(source_id)) 208 | 209 | if source_id not in self._received_open_commitments: 210 | self._received_open_commitments[source_id] = open_commitment 211 | else: 212 | raise ThresholdCryptoError("Open commitment from participant {} already received".format(source_id)) 213 | 214 | def compute_public_key(self) -> PublicKey: 215 | """ 216 | Compute the public key from received commitment values. 217 | 218 | :return: the public key 219 | """ 220 | if len(self._received_open_commitments) != self.threshold_params.n: 221 | raise ThresholdCryptoError("Not all commitments were received") 222 | 223 | participants_h_i = [c.h_i for c in self._received_open_commitments.values()] 224 | h = number.ecc_sum(participants_h_i) 225 | 226 | return PublicKey(h, self.curve_params) 227 | 228 | def F_ij_value(self) -> DkgFijValue: 229 | """ 230 | The F_ij value from Pedersens DKG protocol, which is used to evaluate the correctness of the s_ij 231 | values received in a later step. 232 | 233 | :return: The F_ij value 234 | """ 235 | return DkgFijValue(self.id, self._local_F_ij) 236 | 237 | def receive_F_ij_value(self, F_ij: DkgFijValue): 238 | """ 239 | Receive the F_ij value of another participant. 240 | 241 | :param F_ij: the received F_ij value 242 | """ 243 | # implicit check for successful receival of all commitments 244 | self.compute_public_key() 245 | 246 | source_id = F_ij.source_participant_id 247 | len_F_ij = len(F_ij.F_ij) 248 | 249 | if source_id not in self.all_participant_ids: 250 | raise ThresholdCryptoError("Received F_ij values from unknown participant id {}".format(source_id)) 251 | 252 | if source_id == self.id: 253 | raise ThresholdCryptoError("Received own F_ij values - don't do this") 254 | 255 | if len_F_ij != self.threshold_params.t: 256 | raise ThresholdCryptoError("List of F_ij values from participant {} has length {} != {} = t".format(source_id, len_F_ij, self.threshold_params.t)) 257 | 258 | if source_id not in self._received_F: 259 | self._received_F[source_id] = F_ij 260 | else: 261 | raise ThresholdCryptoError("F_ij values from participant {} already received".format(source_id)) 262 | 263 | def s_ij_value_for_participant(self, target_participant_id: ParticipantId) -> DkgSijValue: 264 | """ 265 | The s_ij value from Pedersens DKG protocol for ONE other particular participant. 266 | This value has to be sent SECRETLY to the target participant, which is not covered by this library for now. 267 | 268 | :param target_participant_id: the id of the target participant 269 | """ 270 | if len(self._received_F) != self.threshold_params.n: 271 | raise ThresholdCryptoError( 272 | "s_ij values are just accessible when all other F_ij values were received") 273 | 274 | return self._unchecked_s_ij_value_for_participant(target_participant_id) 275 | 276 | def _unchecked_s_ij_value_for_participant(self, target_participant_id: ParticipantId) -> DkgSijValue: 277 | # This method is provided so that it can be used for the own s_ij value computation in the __init__ 278 | # method without performing the check in s_ij_value_for_participant. 279 | if target_participant_id not in self.all_participant_ids: 280 | raise ThresholdCryptoError("Participant id {} not present in known participant ids".format(target_participant_id)) 281 | else: 282 | return DkgSijValue(self.id, target_participant_id, self._local_sij[target_participant_id]) 283 | 284 | def receive_sij(self, received_sij: DkgSijValue): 285 | """ 286 | Receive the s_ij value of another participant. 287 | 288 | :param received_sij: the received s_ij value 289 | """ 290 | source_id = received_sij.source_participant_id 291 | target_id = received_sij.target_participant_id 292 | sij = received_sij.s_ij 293 | 294 | if source_id not in self.all_participant_ids: 295 | raise ThresholdCryptoError("Received s_ij value from unknown participant id {}".format(source_id)) 296 | 297 | if source_id == self.id: 298 | raise ThresholdCryptoError("Received own s_ij value - don't do this") 299 | 300 | if target_id != self.id: 301 | raise ThresholdCryptoError("Received s_ij value for foreign participant (own id={}, target id={})".format(self.id, target_id)) 302 | 303 | if source_id not in self._received_sij: 304 | self._received_sij[source_id] = received_sij 305 | else: 306 | raise ThresholdCryptoError("s_ij value for participant {} already received".format(source_id)) 307 | 308 | # verify received F values 309 | s_ijP = sij * self.curve_params.P 310 | F_list = [(self.id ** l) * F_jl for l, F_jl in enumerate(self._received_F[source_id].F_ij)] 311 | F_sum = number.ecc_sum(F_list) 312 | 313 | if s_ijP != F_sum: 314 | raise ThresholdCryptoError("F verification failed for participant {}".format(source_id)) 315 | 316 | def compute_share(self) -> KeyShare: 317 | """ 318 | Compute the participants key share from values obtained during the DKG protocol. 319 | 320 | :return: the final key share after the DKG protocol 321 | """ 322 | if len(self._received_sij) != self.threshold_params.n: 323 | raise ThresholdCryptoError("Received less s_ij values than necessary: {} != {} = n".format(len(self._received_sij), self.threshold_params.n)) 324 | 325 | self._s_i = sum(rs.s_ij for rs in self._received_sij.values()) % self.curve_params.order 326 | self.key_share = KeyShare(self.id, self._s_i, self.curve_params) 327 | 328 | return self.key_share 329 | 330 | def __str__(self): 331 | return "Participant[id = {}, x_i = {}, h_i = {}, s_i = {}".format(self.id, self._x_i, self._h_i, self._s_i) -------------------------------------------------------------------------------- /threshold_crypto/data.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from collections.abc import Mapping 3 | import json 4 | from typing import Iterable, List, Any, Dict 5 | 6 | from Crypto.PublicKey import ECC 7 | 8 | 9 | class ThresholdCryptoError(Exception): 10 | pass 11 | 12 | 13 | # helper functions for serializing ECC.EccPoint 14 | 15 | def _ecc_point_to_serializable(p: ECC.EccPoint) -> Dict[str, Any]: 16 | return { 17 | "x": int(p.x), 18 | "y": int(p.y), 19 | "curve": p._curve_name, 20 | } 21 | 22 | 23 | def _is_ecc_point_list(value): 24 | if isinstance(value, list): 25 | return all(isinstance(item, ECC.EccPoint) for item in value) 26 | return False 27 | 28 | 29 | def _is_serialized_ecc_point(value): 30 | return isinstance(value, Mapping) and "x" in value and "y" in value and "curve" in value 31 | 32 | 33 | def _is_serialized_ecc_point_list(value): 34 | if isinstance(value, list): 35 | return all(_is_serialized_ecc_point(item) for item in value) 36 | return False 37 | 38 | 39 | class ThresholdDataClass: 40 | """ Baseclass for ThresholdCrypto data classes. """ 41 | BASE64_MAGIC = "BASE64|" 42 | CURVE_MAGIC = "ECURVE|" 43 | 44 | def __init__(self): 45 | raise NotImplementedError("Implement __init__ in subclass when using ThresholdDataClass") 46 | 47 | def to_json(self): 48 | """ Create json representation of object. Some special cases are already handled here. """ 49 | data_dict = self.__dict__.copy() 50 | 51 | for k, val in data_dict.items(): 52 | # special handling of bytes 53 | if isinstance(val, bytes): 54 | data_dict[k] = self.BASE64_MAGIC + base64.b64encode(val).decode('ascii') 55 | 56 | # special handling of curve parameters 57 | if isinstance(val, CurveParameters): 58 | data_dict[k] = self.CURVE_MAGIC + val._name 59 | 60 | # special handling of curve points 61 | if isinstance(val, ECC.EccPoint): 62 | data_dict[k] = _ecc_point_to_serializable(val) 63 | 64 | if _is_ecc_point_list(val): 65 | data_dict[k] = [_ecc_point_to_serializable(p) for p in val] 66 | 67 | return json.dumps(data_dict) 68 | 69 | @classmethod 70 | def from_json(cls, json_str: str): 71 | """ Create object from json representation. Some special cases are already handled here. """ 72 | dict = json.loads(json_str) 73 | 74 | for k, val in dict.items(): 75 | # special handling of bytes 76 | if isinstance(val, str) and val.startswith(cls.BASE64_MAGIC): 77 | dict[k] = base64.b64decode(val[len(cls.BASE64_MAGIC):].encode('ascii')) 78 | 79 | # special handling of curve parameters 80 | if isinstance(val, str) and val.startswith(cls.CURVE_MAGIC): 81 | dict[k] = CurveParameters(curve_name=val[len(cls.CURVE_MAGIC):]) 82 | 83 | # special handling of curve points 84 | if _is_serialized_ecc_point(val): 85 | dict[k] = ECC.EccPoint(**val) 86 | 87 | if _is_serialized_ecc_point_list(dict[k]): 88 | dict[k] = [ECC.EccPoint(**s_point) for s_point in val] 89 | 90 | return cls(**dict) 91 | 92 | 93 | class ThresholdParameters(ThresholdDataClass): 94 | """ 95 | Contains the parameters used for the threshold scheme: 96 | - t: number of share owners required to decrypt a message 97 | - n: number of share owners involved 98 | 99 | In other words: 100 | At least t out of overall n share owners must participate to decrypt an encrypted message. 101 | """ 102 | 103 | def __init__(self, t: int, n: int): 104 | """ 105 | Construct threshold parameter. Required: 106 | 0 < t <= n 107 | 108 | :param t: number of share owners required for decryption 109 | :param n: overall number of share owners 110 | """ 111 | if t > n: 112 | raise ThresholdCryptoError('threshold parameter t must be smaller than n') 113 | if t <= 0: 114 | raise ThresholdCryptoError('threshold parameter t must be greater than 0') 115 | 116 | self.t = t 117 | self.n = n 118 | 119 | def __eq__(self, other): 120 | return (isinstance(other, self.__class__) and 121 | self.t == other.t and 122 | self.n == other.n) 123 | 124 | def __str__(self): 125 | return 'ThresholdParameters ({}, {})'.format(self.t, self.n) 126 | 127 | 128 | class CurveParameters(ThresholdDataClass): 129 | """ 130 | Contains the curve parameters the scheme uses. Since PyCryptodome is used, only curves present there are available: 131 | https://pycryptodome.readthedocs.io/en/latest/src/public_key/ecc.html 132 | """ 133 | DEFAULT_CURVE = 'P-256' 134 | 135 | def __init__(self, curve_name: str = DEFAULT_CURVE): 136 | """ 137 | Construct the curve from a given curve name (according to curves present in PyCryptodome). 138 | 139 | :param curve_name: 140 | """ 141 | if curve_name not in ECC._curves: 142 | raise ThresholdCryptoError('Unsupported curve: ' + curve_name) 143 | 144 | self._name = curve_name 145 | self._curve = ECC._curves[curve_name] 146 | self.P = ECC.EccPoint(x=self._curve.Gx, y=self._curve.Gy, curve=curve_name) 147 | 148 | @property 149 | def order(self): 150 | return int(self._curve.order) 151 | 152 | def to_json(self): 153 | return json.dumps({'curve_name': self._name}) 154 | 155 | def __eq__(self, other): 156 | return (isinstance(other, self.__class__) and 157 | self._curve == other._curve) 158 | 159 | def __str__(self): 160 | return "Curve {} of order {} with generator point P = {}".format(self._name, self.order, self.P) 161 | 162 | 163 | class PublicKey(ThresholdDataClass): 164 | """ 165 | The public key point Q linked to the (implicit) secret key d of the scheme. 166 | """ 167 | 168 | def __init__(self, Q: ECC.EccPoint, curve_params: CurveParameters = CurveParameters()): 169 | """ 170 | Construct the public key. 171 | 172 | :param Q: the public key point Q = dP 173 | :param curve_params: the curve parameters used for constructing the key. 174 | """ 175 | self.Q = Q 176 | self.curve_params = curve_params 177 | 178 | def __eq__(self, other): 179 | return (isinstance(other, self.__class__) and 180 | self.curve_params == other.curve_params and 181 | self.Q == other.Q) 182 | 183 | def __str__(self): 184 | return 'Public key point Q = {} (on curve {})'.format(self.Q, self.curve_params._name) 185 | 186 | 187 | class KeyShare(ThresholdDataClass): 188 | """ 189 | A share (x_i, y_i) of the secret key d for share owner i. 190 | y_i is the evaluated polynom value of x_i in shamirs secret sharing. 191 | """ 192 | 193 | def __init__(self, x: int, y: int, curve_params: CurveParameters): 194 | """ 195 | Construct a share of the private key d. 196 | 197 | :param x: the x value of the share 198 | :param y: the y value of the share 199 | :param curve_params: the curve parameters used 200 | """ 201 | self.x = x 202 | self.y = y 203 | self.curve_params = curve_params 204 | 205 | def __eq__(self, other): 206 | return (isinstance(other, self.__class__) and 207 | self.curve_params == other.curve_params and 208 | self.x == other.x and 209 | self.y == other.y) 210 | 211 | def __str__(self): 212 | return 'KeyShare (x,y) = ({}, {}) (on curve {})'.format(self.x, self.y, self.curve_params._name) 213 | 214 | 215 | class EncryptedMessage(ThresholdDataClass): 216 | # TODO include curve_params? 217 | """ 218 | An encrypted message in the scheme. Because a hybrid approach is used it consists of three parts: 219 | 220 | - C1 = kP as in the ElGamal scheme 221 | - C2 = kQ + rP as in the ElGamal scheme with rP being the encrypted point for a random value r 222 | - ciphertext, the symmetrically encrypted message. 223 | 224 | The symmetric key is derived from the ElGamal encrypted point rP. 225 | 226 | Note: The ECIES approach for ECC 227 | - chooses a random r, 228 | - computes R=rP and S=rQ, 229 | - derives a symmetric key k from S, 230 | - uses R and the symmetric encryption of m as ciphertext. 231 | But to enable the re-encryption of ciphertexts, here the approach similar to regular ElGamal is used instead. 232 | """ 233 | 234 | def __init__(self, C1: ECC.EccPoint, C2: ECC.EccPoint, ciphertext: bytes): 235 | """ 236 | Construct a encrypted message. 237 | 238 | :param v: like in ElGamal scheme 239 | :param c: like in ElGamal scheme 240 | :param ciphertext: the symmetrically encrypted message 241 | """ 242 | self.C1 = C1 243 | self.C2 = C2 244 | self.ciphertext = ciphertext 245 | 246 | def __eq__(self, other): 247 | return (isinstance(other, self.__class__) and 248 | self.C1 == other.C1 and 249 | self.C2 == other.C2 and 250 | self.ciphertext == other.ciphertext) 251 | 252 | def __str__(self): 253 | return 'EncryptedMessage (C1, C2, ciphertext) = ({}, {}, {}))'.format(self.C1, self.C2, self.ciphertext) 254 | 255 | 256 | class LagrangeCoefficient(ThresholdDataClass): 257 | """ 258 | The Lagrange coefficient for a distinct participant used in partial decryption combination and partial re-encryption key combination. 259 | """ 260 | 261 | def __init__(self, participant_index: int, used_index_values: Iterable[int], coefficient: int): 262 | """ 263 | Construct the Lagrange coefficient 264 | 265 | :param participant_index: the index (=x value) for the participants share 266 | :param used_index_values: all used indices for reconstruction 267 | :param coefficient: the computed Lagrange coefficient for participant_index using used_index_values 268 | """ 269 | self.participant_index = participant_index 270 | self.used_index_values = set(used_index_values) 271 | self.coefficient = coefficient 272 | 273 | def __eq__(self, other): 274 | return (isinstance(other, self.__class__) and 275 | self.participant_index == other.participant_index and 276 | self.used_index_values == other.used_index_values and 277 | self.coefficient == other.coefficient) 278 | 279 | def __str__(self): 280 | return 'LagrangeCoefficient for participant with index {} in group {} : {}'.format(self.participant_index, list(self.used_index_values), self.coefficient) 281 | 282 | 283 | class PartialDecryption(ThresholdDataClass): 284 | """ 285 | A partial decryption of an encrypted message computed by a share owner using his share. 286 | """ 287 | 288 | def __init__(self, x: int, yC1: ECC.EccPoint, curve_params: CurveParameters): 289 | """ 290 | Construct the partial decryption. 291 | 292 | :param x: the shares x value 293 | :param yC1: the computed partial decryption value 294 | """ 295 | self.x = x 296 | self.yC1 = yC1 297 | self.curve_params = curve_params 298 | 299 | def __eq__(self, other): 300 | return (isinstance(other, self.__class__) and 301 | self.x == other.x and 302 | self.yC1 == other.yC1 and 303 | self.curve_params == other.curve_params) 304 | 305 | def __str__(self): 306 | return 'PartialDecryption (x, yC1) = ({}, {}) (on curve {})'.format(self.x, self.yC1, self.curve_params._name) 307 | 308 | 309 | # re-encryption data types 310 | 311 | 312 | class PartialReEncryptionKey(ThresholdDataClass): 313 | """ 314 | A partial re-encryption key, which can be combined with others to yield the final re-encryption key. 315 | """ 316 | 317 | def __init__(self, partial_key: int, curve_params: CurveParameters): 318 | """ 319 | Construct a partial re-encryption key. 320 | 321 | :param partial_key: The difference of (λ2_i * y2_i - λ1_i * y1_i) where *1 are the old and *2 the new components 322 | :param curve_params: The used curve parameters 323 | """ 324 | if partial_key < 0 or partial_key > curve_params.order: 325 | raise ThresholdCryptoError('Invalid partial key') 326 | 327 | self.partial_key = partial_key 328 | self.curve_params = curve_params 329 | 330 | def __eq__(self, other): 331 | return (isinstance(other, self.__class__) and 332 | self.partial_key == other.partial_key and 333 | self.curve_params == other.curve_params) 334 | 335 | def __str__(self): 336 | return 'PartialReEncryptionKey λ2_i * y2_i - λ1_i * y1_i = {} (for curve {})'.format(self.partial_key, self.curve_params._name) 337 | 338 | 339 | class ReEncryptionKey(ThresholdDataClass): 340 | """ 341 | The re-encryption key created from combined partial re-encryption keys. It can be used to re-encrypt ciphertexts 342 | encrypted for access structure A to ciphertexts decryptable by access structure B. 343 | """ 344 | 345 | def __init__(self, key: int, curve_params: CurveParameters): 346 | """ 347 | Construct the re-encryption key. 348 | 349 | :param key: the reencryption key (dB - dA) meaning new private key minus old private key obtained by combing partial re-encryption keys 350 | :param curve_params: The used curve parameters 351 | """ 352 | if key < 0 or key > curve_params.order: 353 | raise ThresholdCryptoError('Invalid re-encryption key') 354 | 355 | self.key = key 356 | self.curve_params = curve_params 357 | 358 | def __eq__(self, other): 359 | return (isinstance(other, self.__class__) and 360 | self.key == other.key and 361 | self.curve_params == other.curve_params) 362 | 363 | def __str__(self): 364 | return 'ReEncryptionKey dB - dA = {} (for curve {})'.format(self.key, self.curve_params._name) 365 | 366 | 367 | # DKG data types 368 | 369 | class DkgClosedCommitment(ThresholdDataClass): 370 | """ 371 | The closed commitment sent in the first step of Pedersens DKG protocol. 372 | """ 373 | 374 | def __init__(self, participant_id: int, commitment: bytes): 375 | """ 376 | Initialize the closed commitment. 377 | 378 | :param participant_id: the participant id 379 | :param commitment: the (closed) commitment value 380 | """ 381 | self.participant_id = participant_id 382 | self.commitment = commitment 383 | 384 | def __eq__(self, other): 385 | return (isinstance(other, self.__class__) and 386 | self.participant_id == other.participant_id and 387 | self.commitment == other.commitment) 388 | 389 | def __str__(self): 390 | return 'DkgClosedCommitment for participant {} = {}'.format(self.participant_id, self.commitment) 391 | 392 | 393 | class DkgOpenCommitment(ThresholdDataClass): 394 | """ 395 | The open commitment of Pedersens DKG protocol sent after each participant has received all closed commitments. 396 | """ 397 | 398 | def __init__(self, participant_id: int, commitment: bytes, h_i: ECC.EccPoint, r: bytes): 399 | """ 400 | Initialize the open commitment. 401 | 402 | :param participant_id: the participant id 403 | :param commitment: the (closed) commitment value 404 | :param h_i: the "public key share" (ecc point) the participant has commited to 405 | :param r: the random value used in commitment computation 406 | """ 407 | self.participant_id = participant_id 408 | self.commitment = commitment 409 | self.h_i = h_i 410 | self.r = r 411 | 412 | def __eq__(self, other): 413 | return (isinstance(other, self.__class__) and 414 | self.participant_id == other.participant_id and 415 | self.commitment == other.commitment and 416 | self.h_i == other.h_i and 417 | self.r == other.r) 418 | 419 | def __str__(self): 420 | return 'DkgOpenCommitment for participant {} = (c={}, h_i={}, r={})'.format(self.participant_id, self.commitment, self.h_i, self.r) 421 | 422 | 423 | class DkgFijValue(ThresholdDataClass): 424 | """ 425 | The F_ij values used in Pedersens DKG protocol used as check for the later sent s_ij values. 426 | """ 427 | 428 | def __init__(self, source_participant_id: int, F_ij: List[ECC.EccPoint]): 429 | """ 430 | Initialize the F_ij value. 431 | 432 | :param source_participant_id: the participant id 433 | :param F_ij: the F_ij values (ecc points in this implementation) 434 | """ 435 | self.source_participant_id = source_participant_id 436 | self.F_ij = F_ij 437 | 438 | def __eq__(self, other): 439 | return (isinstance(other, self.__class__) and 440 | self.source_participant_id == other.source_participant_id and 441 | self.F_ij == other.F_ij) 442 | 443 | def __str__(self): 444 | return 'DkgFijValue from participant {} = {}'.format(self.source_participant_id, self.F_ij) 445 | 446 | 447 | class DkgSijValue(ThresholdDataClass): 448 | """ 449 | The share of their secret value a participant sents to another participant SECRETLY. 450 | """ 451 | 452 | def __init__(self, source_participant_id: int, target_participant_id: int, s_ij: int): 453 | """ 454 | Initialize the s_ij value. 455 | 456 | :param source_participant_id: the source participant of this value 457 | :param target_participant_id: the target participant of this value 458 | :param s_ij: the s_ij value 459 | """ 460 | self.source_participant_id = source_participant_id 461 | self.target_participant_id = target_participant_id 462 | self.s_ij = s_ij 463 | 464 | def __eq__(self, other): 465 | return (isinstance(other, self.__class__) and 466 | self.source_participant_id == other.source_participant_id and 467 | self.target_participant_id == other.target_participant_id and 468 | self.s_ij == other.s_ij) 469 | 470 | def __str__(self): 471 | return 'DkgSijValue from participant {} to participant {} = {}'.format(self.source_participant_id, self.target_participant_id, self.s_ij) 472 | -------------------------------------------------------------------------------- /test/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from Crypto.Random import random 4 | 5 | from threshold_crypto.data import (ThresholdParameters, 6 | CurveParameters, 7 | ThresholdCryptoError, 8 | KeyShare, 9 | EncryptedMessage, 10 | PartialDecryption, 11 | PartialReEncryptionKey, 12 | ReEncryptionKey, 13 | PublicKey, 14 | DkgOpenCommitment, 15 | DkgSijValue, 16 | DkgClosedCommitment, 17 | DkgFijValue 18 | ) 19 | from threshold_crypto import number 20 | from threshold_crypto import central 21 | from threshold_crypto import participant 22 | 23 | 24 | class TCTestCase(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.tp = ThresholdParameters(3, 5) 28 | self.cp = CurveParameters() 29 | self.pk, self.shares = central.create_public_key_and_shares_centralized(self.cp, self.tp) 30 | self.message = 'Some secret message' 31 | self.em = central.encrypt_message(self.message, self.pk) 32 | self.reconstruct_shares = [self.shares[i] for i in [0, 2, 4]] # choose 3 of 5 key shares 33 | self.partial_decryptions = [participant.compute_partial_decryption(self.em, share) for share in self.reconstruct_shares] 34 | 35 | def tearDown(self): 36 | pass 37 | 38 | def test_valid_threshold_parameters(self): 39 | self.assertTrue(ThresholdParameters(3, 5)) 40 | 41 | def test_invalid_threshold_parameters(self): 42 | with self.assertRaises(ThresholdCryptoError): 43 | ThresholdParameters(5, 3) 44 | 45 | def test_threshold_parameter_json(self): 46 | t = ThresholdParameters(3, 5) 47 | t_j = ThresholdParameters.from_json(t.to_json()) 48 | 49 | self.assertEqual(t, t_j) 50 | 51 | def test_valid_curve_parameters(self): 52 | cp = CurveParameters() 53 | self.assertTrue(cp.order > 0) 54 | 55 | def test_invalid_curve_parameters_whole_group(self): 56 | with self.assertRaises(ThresholdCryptoError): 57 | CurveParameters(curve_name="invalid-curve") 58 | 59 | def test_curve_parameter_json(self): 60 | cp = CurveParameters() 61 | cp_j = CurveParameters.from_json(cp.to_json()) 62 | 63 | self.assertEqual(cp, cp_j) 64 | 65 | def test_central_key_generation(self): 66 | pk, shares = central.create_public_key_and_shares_centralized(self.cp, self.tp) 67 | 68 | self.assertEqual(len(shares), self.tp.n) 69 | 70 | def test_public_key_json(self): 71 | pk_j = PublicKey.from_json(self.pk.to_json()) 72 | 73 | self.assertEqual(self.pk, pk_j) 74 | 75 | def test_key_share_json(self): 76 | share = self.shares[0] 77 | share_j = KeyShare.from_json(share.to_json()) 78 | 79 | self.assertEqual(share, share_j) 80 | 81 | def test_message_encryption(self): 82 | em = central.encrypt_message(self.message, self.pk) 83 | 84 | self.assertTrue(em.C1) 85 | self.assertTrue(em.C2) 86 | self.assertTrue(em.ciphertext) 87 | 88 | def test_message_json(self): 89 | m_j = EncryptedMessage.from_json(self.em.to_json()) 90 | 91 | self.assertEqual(self.em, m_j) 92 | 93 | def test_partial_decryption_json(self): 94 | pd = self.partial_decryptions[0] 95 | pd_j = PartialDecryption.from_json(pd.to_json()) 96 | 97 | self.assertEqual(pd, pd_j) 98 | 99 | # TBD: further tests 100 | 101 | def test_polynom_creation(self): 102 | p = number.PolynomMod.create_random_polynom(17, 5, 41) 103 | 104 | self.assertTrue(p.degree == 5) 105 | self.assertTrue(p.evaluate(0) == 17) 106 | 107 | def test_key_encryption_decryption_with_enough_shares(self): 108 | r = number.random_in_range(2, self.cp.order) 109 | testkey_element = r * self.cp.P 110 | kP, c = central._encrypt_key_point(testkey_element, self.pk.Q, self.cp) 111 | em = EncryptedMessage(kP, c, b'') 112 | reconstruct_shares = [self.shares[i] for i in [0, 2, 4]] # choose 3 of 5 key shares 113 | partial_decryptions = [participant.compute_partial_decryption(em, share) for share in reconstruct_shares] 114 | rec_testkey_element = central._combine_shares(partial_decryptions, em, self.cp) 115 | 116 | self.assertEqual(testkey_element, rec_testkey_element) 117 | 118 | def test_key_encryption_decryption_without_enough_shares(self): 119 | r = number.random_in_range(2, self.cp.order) 120 | testkey_element = r * self.cp.P 121 | kP, c = central._encrypt_key_point(testkey_element, self.pk.Q, self.cp) 122 | em = EncryptedMessage(kP, c, b'') 123 | reconstruct_shares = [self.shares[i] for i in [0, 4]] # choose 2 of 5 key shares 124 | partial_decryptions = [participant.compute_partial_decryption(em, share) for share in reconstruct_shares] 125 | rec_testkey_element = central._combine_shares(partial_decryptions, em, self.cp) 126 | 127 | self.assertNotEqual(testkey_element, rec_testkey_element) 128 | 129 | def test_complete_process_with_enough_shares(self): 130 | curve_params = CurveParameters() 131 | thresh_params = ThresholdParameters(3, 5) 132 | 133 | pub_key, key_shares = central.create_public_key_and_shares_centralized(curve_params, thresh_params) 134 | 135 | message = 'Some secret message to be encrypted!' 136 | encrypted_message = central.encrypt_message(message, pub_key) 137 | 138 | reconstruct_shares = [key_shares[i] for i in [0, 2, 4]] # choose 3 of 5 key shares 139 | partial_decryptions = [participant.compute_partial_decryption(encrypted_message, share) for share in reconstruct_shares] 140 | decrypted_message = central.decrypt_message(partial_decryptions, encrypted_message, thresh_params) 141 | 142 | self.assertEqual(message, decrypted_message) 143 | 144 | def test_complete_process_without_enough_shares(self): 145 | curve_params = CurveParameters() 146 | thresh_params = ThresholdParameters(3, 5) 147 | 148 | pub_key, key_shares = central.create_public_key_and_shares_centralized(curve_params, thresh_params) 149 | 150 | message = 'Some secret message to be encrypted!' 151 | encrypted_message = central.encrypt_message(message, pub_key) 152 | 153 | reconstruct_shares = [key_shares[i] for i in [3, 4]] # choose 2 of 5 key shares 154 | partial_decryptions = [participant.compute_partial_decryption(encrypted_message, share) for share in reconstruct_shares] 155 | 156 | with self.assertRaises(ThresholdCryptoError): 157 | central.decrypt_message(partial_decryptions, encrypted_message, thresh_params) 158 | 159 | 160 | class PreTestCase(unittest.TestCase): 161 | """ 162 | Test cases for the proxy reencryption scheme. 163 | """ 164 | 165 | def setUp(self): 166 | self.tp = ThresholdParameters(3, 5) 167 | self.cp = CurveParameters() 168 | self.pk, self.shares = central.create_public_key_and_shares_centralized(self.cp, self.tp) 169 | self.message = 'Some secret message' 170 | self.em = central.encrypt_message(self.message, self.pk) 171 | self.reconstruct_shares = [self.shares[i] for i in [0, 2, 4]] # choose 3 of 5 key shares 172 | self.partial_decryptions = [participant.compute_partial_decryption(self.em, share) for share in 173 | self.reconstruct_shares] 174 | 175 | def tearDown(self): 176 | pass 177 | 178 | def test_partial_re_encryption_key_json(self): 179 | prek = PartialReEncryptionKey(partial_key=17, curve_params=self.cp) 180 | prek_j = PartialReEncryptionKey.from_json(prek.to_json()) 181 | 182 | self.assertEqual(prek, prek_j) 183 | 184 | def test_re_encryption_key_json(self): 185 | rek = ReEncryptionKey(key=42, curve_params=CurveParameters()) 186 | rek_j = ReEncryptionKey.from_json(rek.to_json()) 187 | 188 | self.assertEqual(rek, rek_j) 189 | 190 | def test_re_encryption_process_for_same_access_structures(self): 191 | self.parameterizable_re_encryption_process_test(self.tp.t, self.tp.n) 192 | 193 | def test_re_encryption_process_for_added_participant(self): 194 | self.parameterizable_re_encryption_process_test(self.tp.t, self.tp.n + 1) 195 | 196 | def test_re_encryption_process_for_removed_participant(self): 197 | self.parameterizable_re_encryption_process_test(self.tp.t, self.tp.n - 1) 198 | 199 | def test_re_encryption_process_for_smaller_threshold(self): 200 | self.parameterizable_re_encryption_process_test(self.tp.t - 1, self.tp.n) 201 | 202 | def test_re_encryption_process_for_larger_threshold(self): 203 | self.parameterizable_re_encryption_process_test(self.tp.t + 1, self.tp.n) 204 | 205 | def parameterizable_re_encryption_process_test(self, new_t: int, new_n: int): 206 | new_tp = ThresholdParameters(new_t, new_n) 207 | new_pk, new_shares = central.create_public_key_and_shares_centralized(self.cp, new_tp) 208 | 209 | assert new_pk != self.pk, "Public keys for new and old access structure are the same" 210 | 211 | # Without loss of generality we assume the lists to be ordered in a way, that remaining participants 212 | # are placed at the beginning of the list. 213 | # Choose t_max shares randomly from first min_n old and new shares as the shares of one distinct participant. 214 | max_t = max(self.tp.t, new_tp.t) 215 | min_n = min(self.tp.n, new_tp.n) 216 | t_old_shares = random.sample(self.shares[:min_n], k=max_t) 217 | t_old_shares_x = [share.x for share in t_old_shares] 218 | t_new_shares = random.sample(new_shares[:min_n], k=max_t) 219 | t_new_shares_x = [share.x for share in t_new_shares] 220 | 221 | partial_re_encrypt_keys = [] 222 | for i, (s_old, s_new) in enumerate(zip(t_old_shares, t_new_shares)): 223 | old_lambda = central.lagrange_coefficient_for_key_share_indices(t_old_shares_x, t_old_shares_x[i], self.cp) 224 | new_lambda = central.lagrange_coefficient_for_key_share_indices(t_new_shares_x, t_new_shares_x[i], self.cp) 225 | partial_key = participant.compute_partial_re_encryption_key(s_old, old_lambda, s_new, new_lambda) 226 | partial_re_encrypt_keys.append(partial_key) 227 | 228 | re_encrypt_key = central.combine_partial_re_encryption_keys(partial_re_encrypt_keys, 229 | self.pk, 230 | new_pk, 231 | self.tp, 232 | new_tp) 233 | re_em = central.re_encrypt_message(self.em, re_encrypt_key) 234 | 235 | self.assertNotEqual(self.em, re_em) 236 | 237 | # successful decryption with t shares 238 | new_reconstruct_shares = random.sample(new_shares, new_tp.t) 239 | new_partial_decryptions = [participant.compute_partial_decryption(re_em, share) for share in new_reconstruct_shares] 240 | 241 | decrypted_message = central.decrypt_message(new_partial_decryptions, re_em, new_tp) 242 | self.assertEqual(self.message, decrypted_message) 243 | 244 | # failing decryption with t - 1 shares 245 | with self.assertRaises(ThresholdCryptoError) as dec_exception_context: 246 | less_reconstruct_shares = random.sample(new_shares, new_tp.t - 1) 247 | new_partial_decryptions = [participant.compute_partial_decryption(re_em, share) for share in less_reconstruct_shares] 248 | central._decrypt_message(new_partial_decryptions, re_em) 249 | 250 | self.assertIn("Message decryption failed", str(dec_exception_context.exception)) 251 | 252 | 253 | class DkgTestCase(unittest.TestCase): 254 | """ 255 | Test cases for the distributed key generation. 256 | """ 257 | 258 | def setUp(self): 259 | self.tp = ThresholdParameters(3, 5) 260 | self.cp = CurveParameters() 261 | self.message = 'Some secret message' 262 | 263 | def tearDown(self): 264 | pass 265 | 266 | def test_closed_commitment_json(self): 267 | c = DkgClosedCommitment(1, random.getrandbits(10)) 268 | c_j = DkgClosedCommitment.from_json(c.to_json()) 269 | 270 | self.assertEqual(c, c_j) 271 | 272 | def test_open_commitment_json(self): 273 | c = DkgOpenCommitment(1, random.getrandbits(10), self.cp.P, random.getrandbits(10)) 274 | c_j = DkgOpenCommitment.from_json(c.to_json()) 275 | 276 | self.assertEqual(c, c_j) 277 | 278 | def test_F_ij_value_json(self): 279 | f = DkgFijValue(1, [self.cp.P, 2 * self.cp.P]) 280 | f_j = DkgFijValue.from_json(f.to_json()) 281 | 282 | self.assertEqual(f, f_j) 283 | 284 | def test_s_ij_value_json(self): 285 | f = DkgSijValue(1, 2, 42) 286 | f_j = DkgSijValue.from_json(f.to_json()) 287 | 288 | self.assertEqual(f, f_j) 289 | 290 | def test_distributed_key_generation(self): 291 | participant_ids = list(range(1, self.tp.n + 1)) 292 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 293 | 294 | # via broadcast 295 | for pi in participants: 296 | for pj in participants: 297 | if pj != pi: 298 | closed_commitment = pj.closed_commmitment() 299 | pi.receive_closed_commitment(closed_commitment) 300 | 301 | # via broadcast 302 | for pi in participants: 303 | for pj in participants: 304 | if pj != pi: 305 | open_commitment = pj.open_commitment() 306 | pi.receive_open_commitment(open_commitment) 307 | 308 | public_key = participants[0].compute_public_key() 309 | for pk in [p.compute_public_key() for p in participants[1:]]: 310 | self.assertEqual(public_key, pk) 311 | 312 | # via broadcast 313 | for pi in participants: 314 | for pj in participants: 315 | if pj != pi: 316 | F_ij = pj.F_ij_value() 317 | pi.receive_F_ij_value(F_ij) 318 | 319 | # SECRETLY from i to j 320 | for pi in participants: 321 | for pj in participants: 322 | if pj != pi: 323 | s_ij = pj.s_ij_value_for_participant(pi.id) 324 | pi.receive_sij(s_ij) 325 | 326 | shares = [p.compute_share() for p in participants] 327 | 328 | # test encryption/decryption 329 | 330 | em = central.encrypt_message(self.message, public_key) 331 | 332 | pdms = [participant.compute_partial_decryption(em, ks) for ks in shares[:self.tp.t]] 333 | dm = central.decrypt_message(pdms, em, self.tp) 334 | 335 | self.assertEqual(dm, self.message) 336 | 337 | def test_compromised_open_commitment(self): 338 | participant_ids = list(range(1, self.tp.n + 1)) 339 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 340 | 341 | # via broadcast 342 | for pi in participants: 343 | for pj in participants: 344 | if pj != pi: 345 | closed_commitment = pj.closed_commmitment() 346 | pi.receive_closed_commitment(closed_commitment) 347 | 348 | with self.assertRaises(ThresholdCryptoError): 349 | open_commitment = participants[0].open_commitment() 350 | 351 | # tamper with open commitment 352 | open_commitment.h_i = 2 * open_commitment.h_i 353 | 354 | participants[1].receive_open_commitment(open_commitment) 355 | 356 | def test_compromised_F_value(self): 357 | participant_ids = list(range(1, self.tp.n + 1)) 358 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 359 | 360 | # via broadcast 361 | for pi in participants: 362 | for pj in participants: 363 | if pj != pi: 364 | closed_commitment = pj.closed_commmitment() 365 | pi.receive_closed_commitment(closed_commitment) 366 | 367 | # via broadcast 368 | for pi in participants: 369 | for pj in participants: 370 | if pj != pi: 371 | open_commitment = pj.open_commitment() 372 | pi.receive_open_commitment(open_commitment) 373 | 374 | # via broadcast 375 | for pi in participants: 376 | for pj in participants: 377 | if pj != pi: 378 | F_ij = pj.F_ij_value() 379 | 380 | # tamper with one F_ij 381 | if pj.id == 1: 382 | F_ij.F_ij[0] = 2 * F_ij.F_ij[0] 383 | 384 | pi.receive_F_ij_value(F_ij) 385 | 386 | # SECRETLY from i to j 387 | with self.assertRaises(ThresholdCryptoError): 388 | s_ij = participants[0].s_ij_value_for_participant(participants[1].id) 389 | participants[1].receive_sij(s_ij) 390 | 391 | def test_compromised_s_ij_value(self): 392 | participant_ids = list(range(1, self.tp.n + 1)) 393 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 394 | 395 | # via broadcast 396 | for pi in participants: 397 | for pj in participants: 398 | if pj != pi: 399 | closed_commitment = pj.closed_commmitment() 400 | pi.receive_closed_commitment(closed_commitment) 401 | 402 | # via broadcast 403 | for pi in participants: 404 | for pj in participants: 405 | if pj != pi: 406 | open_commitment = pj.open_commitment() 407 | pi.receive_open_commitment(open_commitment) 408 | 409 | # via broadcast 410 | for pi in participants: 411 | for pj in participants: 412 | if pj != pi: 413 | F_ij = pj.F_ij_value() 414 | pi.receive_F_ij_value(F_ij) 415 | 416 | # SECRETLY from i to j 417 | with self.assertRaises(ThresholdCryptoError): 418 | s_ij = participants[0].s_ij_value_for_participant(participants[1].id) 419 | 420 | # tamper with s_ij 421 | s_ij.s_ij = 2 * s_ij.s_ij % self.cp.order 422 | 423 | participants[1].receive_sij(s_ij) 424 | 425 | def test_not_enough_open_commitments(self): 426 | participant_ids = list(range(1, self.tp.n + 1)) 427 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 428 | 429 | # via broadcast 430 | # participants[0] is missing participants[1]'s commitment 431 | for pj in participants[2:]: 432 | closed_commitment = pj.closed_commmitment() 433 | participants[0].receive_closed_commitment(closed_commitment) 434 | 435 | with self.assertRaises(ThresholdCryptoError): 436 | participants[0].open_commitment() 437 | 438 | def test_not_enough_closed_commitments(self): 439 | participant_ids = list(range(1, self.tp.n + 1)) 440 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 441 | 442 | # via broadcast 443 | for pi in participants: 444 | for pj in participants: 445 | if pj != pi: 446 | closed_commitment = pj.closed_commmitment() 447 | pi.receive_closed_commitment(closed_commitment) 448 | 449 | # via broadcast 450 | # participants[0] is missing participants[1]'s commitment 451 | for pj in participants[2:]: 452 | open_commitment = pj.open_commitment() 453 | participants[0].receive_open_commitment(open_commitment) 454 | 455 | with self.assertRaises(ThresholdCryptoError): 456 | participants[0].compute_public_key() 457 | 458 | def test_not_enough_F_ij_values(self): 459 | participant_ids = list(range(1, self.tp.n + 1)) 460 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 461 | 462 | # via broadcast 463 | for pi in participants: 464 | for pj in participants: 465 | if pj != pi: 466 | closed_commitment = pj.closed_commmitment() 467 | pi.receive_closed_commitment(closed_commitment) 468 | 469 | # via broadcast 470 | for pi in participants: 471 | for pj in participants: 472 | if pj != pi: 473 | open_commitment = pj.open_commitment() 474 | pi.receive_open_commitment(open_commitment) 475 | 476 | # via broadcast 477 | # participants[0] is missing participants[1]'s F_ij value 478 | for pj in participants[2:]: 479 | F_ij = pj.F_ij_value() 480 | participants[0].receive_F_ij_value(F_ij) 481 | 482 | with self.assertRaises(ThresholdCryptoError): 483 | participants[0].s_ij_value_for_participant(2) 484 | 485 | def test_not_enough_s_ij_values(self): 486 | participant_ids = list(range(1, self.tp.n + 1)) 487 | participants = [participant.Participant(id, participant_ids, self.cp, self.tp) for id in participant_ids] 488 | 489 | # via broadcast 490 | for pi in participants: 491 | for pj in participants: 492 | if pj != pi: 493 | closed_commitment = pj.closed_commmitment() 494 | pi.receive_closed_commitment(closed_commitment) 495 | 496 | # via broadcast 497 | for pi in participants: 498 | for pj in participants: 499 | if pj != pi: 500 | open_commitment = pj.open_commitment() 501 | pi.receive_open_commitment(open_commitment) 502 | 503 | public_key = participants[0].compute_public_key() 504 | for pk in [p.compute_public_key() for p in participants[1:]]: 505 | self.assertEqual(public_key, pk) 506 | 507 | # via broadcast 508 | for pi in participants: 509 | for pj in participants: 510 | if pj != pi: 511 | F_ij = pj.F_ij_value() 512 | pi.receive_F_ij_value(F_ij) 513 | 514 | # SECRETLY from i to j 515 | # participants[0] is missing participants[1]'s s_ij value 516 | for pj in participants[2:]: 517 | s_ij = pj.s_ij_value_for_participant(participants[0].id) 518 | participants[0].receive_sij(s_ij) 519 | 520 | with self.assertRaises(ThresholdCryptoError): 521 | participants[0].compute_share() 522 | --------------------------------------------------------------------------------