diff --git a/.github/workflows/run-benchmark-tests.yaml b/.github/workflows/run-benchmark-tests.yaml index 1bc357fa..624a8b57 100644 --- a/.github/workflows/run-benchmark-tests.yaml +++ b/.github/workflows/run-benchmark-tests.yaml @@ -1,6 +1,6 @@ -name: Run benchmark tests +name: run-benchmark-tests on: - pull_request_target: + pull_request: types: [ opened, synchronize, reopened, edited ] branches: - main diff --git a/.github/workflows/run-pytest-and-sonarcloud-scan.yaml b/.github/workflows/run-pytest-and-sonarcloud-scan.yaml index fb296698..f5789af4 100644 --- a/.github/workflows/run-pytest-and-sonarcloud-scan.yaml +++ b/.github/workflows/run-pytest-and-sonarcloud-scan.yaml @@ -4,7 +4,7 @@ on: push: branches: - '**' - pull_request_target: + pull_request: types: [opened, synchronize, reopened, edited] branches: - develop @@ -47,11 +47,10 @@ jobs: path: /home/runner/_work/claasp/coverage.xml run-code-coverage: - if: ${{ !github.event.repository.fork }} runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v2 with: persist-credentials: false fetch-depth: 0 @@ -73,4 +72,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - needs: run-pytest + needs: run-pytest \ No newline at end of file diff --git a/.github/workflows/update-changelog.yaml b/.github/workflows/update-changelog.yaml index cf3c7bd6..4a4fbcf3 100644 --- a/.github/workflows/update-changelog.yaml +++ b/.github/workflows/update-changelog.yaml @@ -31,7 +31,7 @@ jobs: uses: actions-js/push@master if: ${{ env.should_add_last_changes_to_master == 'true' }} with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.AUTHORIZATION_TOKEN }} message: 'Changelog version updated' tags: true force: true @@ -49,7 +49,7 @@ jobs: if: ${{ env.should_add_last_changes_to_master == 'true' }} uses: rickstaa/action-create-tag@v1 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.AUTHORIZATION_TOKEN }} tag: ${{ env.tag_name }} tag_exists_error: false message: ${{ env.release_message }} @@ -80,5 +80,5 @@ jobs: with: target: 'develop' source: 'main' - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.AUTHORIZATION_TOKEN }} strategy_options: 'ours' diff --git a/VERSION b/VERSION index 852700e1..a6316f06 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.1.0 \ No newline at end of file +v2.3.0 \ No newline at end of file diff --git a/claasp/cipher_modules/algebraic_tests.py b/claasp/cipher_modules/algebraic_tests.py index 5362c386..c021abc6 100644 --- a/claasp/cipher_modules/algebraic_tests.py +++ b/claasp/cipher_modules/algebraic_tests.py @@ -31,26 +31,26 @@ class AlgebraicTests: sage: toyspn = ToySPN1(number_of_rounds=2) sage: alg_test = AlgebraicTests(toyspn) sage: alg_test.algebraic_tests(timeout_in_seconds=10) - {'input_parameters': {'cipher.id': 'toyspn1_p6_k6_o6_r2', + {'input_parameters': {'cipher': toyspn1_p6_k6_o6_r2, 'timeout_in_seconds': 10, 'test_name': 'algebraic_tests'}, - 'test_results': {'number_of_variables': [66, 126], - 'number_of_equations': [76, 158], - 'number_of_monomials': [96, 186], + 'test_results': {'number_of_variables': [30, 48], + 'number_of_equations': [40, 80], + 'number_of_monomials': [60, 108], 'max_degree_of_equations': [2, 2], - 'test_passed': [False, True]}} + 'test_passed': [False, False]}} sage: from claasp.cipher_modules.algebraic_tests import AlgebraicTests sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(number_of_rounds=1) sage: alg_test = AlgebraicTests(speck) sage: alg_test.algebraic_tests(timeout_in_seconds=30) - {'input_parameters': {'cipher.id': 'speck_p32_k64_o32_r1', + {'input_parameters': {'cipher': speck_p32_k64_o32_r1, 'timeout_in_seconds': 30, 'test_name': 'algebraic_tests'}, - 'test_results': {'number_of_variables': [320], - 'number_of_equations': [272], - 'number_of_monomials': [365], + 'test_results': {'number_of_variables': [144], + 'number_of_equations': [96], + 'number_of_monomials': [189], 'max_degree_of_equations': [2], 'test_passed': [True]}} @@ -58,22 +58,23 @@ class AlgebraicTests: def __init__(self, cipher): self._cipher = cipher + self._algebraic_model = AlgebraicModel(cipher) def algebraic_tests(self, timeout_in_seconds=60): from sage.structure.sequence import Sequence nvars_up_to_round = [] - npolynomials_up_to_round = [] nmonomials_up_to_round = [] max_deg_of_equations_up_to_round = [] tests_up_to_round = [] F = [] - - algebraic_model = AlgebraicModel(self._cipher) + constant_vars = {} for round_number in range(self._cipher.number_of_rounds): - F += algebraic_model.polynomial_system_at_round(round_number) + \ - algebraic_model.connection_polynomials_at_round(round_number) + F += self._algebraic_model.polynomial_system_at_round(round_number, True) + constant_vars.update(self._algebraic_model._dict_constant_component_polynomials(round_number)) + if constant_vars is not None: + F = self._algebraic_model._remove_constant_polynomials(constant_vars, F) Fseq = Sequence(F) nvars_up_to_round.append(Fseq.nvariables()) npolynomials_up_to_round.append(len(Fseq)) diff --git a/claasp/cipher_modules/models/algebraic/algebraic_model.py b/claasp/cipher_modules/models/algebraic/algebraic_model.py index efdf4826..a9543d2e 100644 --- a/claasp/cipher_modules/models/algebraic/algebraic_model.py +++ b/claasp/cipher_modules/models/algebraic/algebraic_model.py @@ -1,4 +1,3 @@ - # **************************************************************************** # Copyright 2023 Technology Innovation Institute # @@ -78,25 +77,11 @@ def connection_polynomials_at_round(self, r): plaintext_y23 + sbox_0_5_x3] """ polynomials = [] - R = self.ring() for component in self._cipher.get_components_in_round(r): - if component.type == "constant": continue - - input_vars = [component.id + "_" + self.input_postfix + str(i) for i in range(component.input_bit_size)] - input_vars = list(map(R, input_vars)) - - input_links = component.input_id_links - input_positions = component.input_bit_positions - - prev_input_vars = [] - for k in range(len(input_links)): - prev_input_vars += [input_links[k] + "_" + self.output_postfix + str(i) for i in - input_positions[k]] - prev_input_vars = list(map(R, prev_input_vars)) - + input_vars, prev_input_vars = self._input_vars_previous_input_vars(component) polynomials += [x + y for (x, y) in zip(input_vars, prev_input_vars)] return polynomials @@ -107,15 +92,15 @@ def is_algebraically_secure(self, timeout): INPUT: - - ``timeout`` -- **integer**; the timeout for the Grobner basis computation in seconds + - ``timeout`` -- **integer**; the timeout for the Groebner basis computation in seconds EXAMPLES:: sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel - sage: from claasp.ciphers.block_ciphers.identity_block_cipher import IdentityBlockCipher - sage: identity = IdentityBlockCipher() - sage: algebraic = AlgebraicModel(identity) - sage: algebraic.is_algebraically_secure(120) + sage: from claasp.ciphers.toys.toyspn1 import ToySPN1 + sage: toyspn = ToySPN1() + sage: algebraic = AlgebraicModel(toyspn) + sage: algebraic.is_algebraically_secure(30) False """ from cysignals.alarm import alarm, cancel_alarm @@ -164,18 +149,48 @@ def polynomial_system(self): EXAMPLES:: + sage: from claasp.ciphers.toys.toyspn1 import ToySPN1 + sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel + sage: toyspn = ToySPN1() + sage: AlgebraicModel(toyspn).polynomial_system() + Polynomial Sequence with 80 Polynomials in 48 Variables + sage: from claasp.ciphers.block_ciphers.fancy_block_cipher import FancyBlockCipher sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel sage: fancy = FancyBlockCipher(number_of_rounds=1) - sage: AlgebraicModel(fancy).polynomial_system() # long time - Polynomial Sequence with 468 Polynomials in 384 Variables + sage: AlgebraicModel(fancy).polynomial_system() + Polynomial Sequence with 228 Polynomials in 144 Variables + + sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel + sage: speck = SpeckBlockCipher(number_of_rounds=2) + sage: AlgebraicModel(speck).polynomial_system() + Polynomial Sequence with 288 Polynomials in 352 Variables + + sage: from claasp.ciphers.block_ciphers.aes_block_cipher import AESBlockCipher + sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel + sage: aes = AESBlockCipher(word_size=4, state_size=2, number_of_rounds=1) + sage: AlgebraicModel(aes).polynomial_system() + Polynomial Sequence with 198 Polynomials in 128 Variables + + sage: from claasp.ciphers.block_ciphers.tea_block_cipher import TeaBlockCipher + sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel + sage: tea = TeaBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=1) + sage: AlgebraicModel(tea).polynomial_system() + Polynomial Sequence with 352 Polynomials in 448 Variables + """ - polynomials = sum([self.polynomial_system_at_round(r) for r in range(self._cipher.number_of_rounds)], []) - polynomials += self.connection_polynomials() + polynomials = [] + constant_vars = {} + for r in range(self._cipher.number_of_rounds): + polynomials += self.polynomial_system_at_round(r, True) + constant_vars.update(self._dict_constant_component_polynomials(r)) + if constant_vars is not None: + polynomials = self._remove_constant_polynomials(constant_vars, polynomials) return Sequence(polynomials) - def polynomial_system_at_round(self, r): + def polynomial_system_at_round(self, r, fun_call_flag=False): """ Return a polynomial system at round `r`. @@ -188,8 +203,8 @@ def polynomial_system_at_round(self, r): sage: from claasp.ciphers.block_ciphers.fancy_block_cipher import FancyBlockCipher sage: from claasp.cipher_modules.models.algebraic.algebraic_model import AlgebraicModel sage: fancy = FancyBlockCipher(number_of_rounds=1) - sage: AlgebraicModel(fancy).polynomial_system_at_round(0) # long time - Polynomial Sequence with 252 Polynomials in 288 Variables + sage: AlgebraicModel(fancy).polynomial_system_at_round(0) + Polynomial Sequence with 228 Polynomials in 144 Variables """ if not 0 <= r < self._cipher.number_of_rounds: raise ValueError(f"r must be in the range 0 <= r < {self._cipher.number_of_rounds}") @@ -209,8 +224,70 @@ def polynomial_system_at_round(self, r): operation in ['ROTATE_BY_VARIABLE_AMOUNT', 'SHIFT_BY_VARIABLE_AMOUNT']: raise ValueError(f"polynomial generation of {operation} operation is not supported at present") + polynomials = self._apply_connection_variable_mapping(Sequence(polynomials), r) + + if fun_call_flag is False: + constant_vars = self._dict_constant_component_polynomials(r) + if constant_vars is not None: + polynomials = self._remove_constant_polynomials(constant_vars, polynomials) + return Sequence(polynomials) + def _apply_connection_variable_mapping(self, polys, r): + + if not polys: + return polys + + variable_substitution_dict = {} + + for component in self._cipher.get_components_in_round(r): + if component.type == "constant": + continue + input_vars, prev_input_vars = self._input_vars_previous_input_vars(component) + if component.type != "cipher_output": + variable_substitution_dict.update({x: y for x, y in zip(input_vars, prev_input_vars)}) + else: + variable_substitution_dict.update({y: x for x, y in zip(input_vars, prev_input_vars)}) + + polys = polys.subs(variable_substitution_dict) + + return polys + + def _input_vars_previous_input_vars(self, component): + input_vars = [component.id + "_" + self.input_postfix + str(i) for i in range(component.input_bit_size)] + input_vars = list(map(self.ring(), input_vars)) + input_links = component.input_id_links + input_positions = component.input_bit_positions + + prev_input_vars = [] + for k in range(len(input_links)): + prev_input_vars += [input_links[k] + "_" + self.output_postfix + str(i) for i in + input_positions[k]] + prev_input_vars = list(map(self.ring(), prev_input_vars)) + return input_vars, prev_input_vars + + def _dict_constant_component_polynomials(self, round_number): + + constant_vars = {} + for component in self._cipher.get_components_in_round(round_number): + if component.type == "constant": + output_vars = [component.id + "_" + self.output_postfix + str(i) for i in + range(component.output_bit_size)] + else: + continue + output_vars = list(map(self.ring(), output_vars)) + constant = int(component.description[0], 16) + b = list(map(int, reversed(bin(constant)[2:]))) + b += [0] * (component.output_bit_size - len(b)) + constant_vars.update({x: y for x, y in zip(output_vars, b)}) + return constant_vars + + def _remove_constant_polynomials(self, constant_vars, polys): + + polys = Sequence(polys).subs(constant_vars) + polys = [p for p in polys if p != 0] + return polys + def ring(self): """ Return the polynomial ring for the system of equations. diff --git a/claasp/cipher_modules/models/milp/milp_model.py b/claasp/cipher_modules/models/milp/milp_model.py index f43e35c6..6b40439e 100644 --- a/claasp/cipher_modules/models/milp/milp_model.py +++ b/claasp/cipher_modules/models/milp/milp_model.py @@ -43,14 +43,11 @@ from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT, get_external_milp_solver_configuration +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT, MODEL_DEFAULT_PATH, MILP_SOLVERS_EXTERNAL, \ + MILP_SOLVERS_INTERNAL from claasp.cipher_modules.models.milp.utils.utils import _get_data, _parse_external_solver_output, _write_model_to_lp_file from claasp.cipher_modules.models.utils import convert_solver_solution_to_dictionary -verbose = 0 -verbose_print = print if verbose else lambda *a, **k: None - - def get_independent_input_output_variables(component): """ Return a list of 2 lists containing the name of each input/output bit. @@ -132,7 +129,7 @@ def get_input_output_variables(component): class MilpModel: """Build MILP models for ciphers using Cipher.""" - def __init__(self, cipher, n_window_heuristic=None): + def __init__(self, cipher, n_window_heuristic=None, verbose=False): self._cipher = cipher self._variables_list = [] self._model_constraints = [] @@ -143,6 +140,7 @@ def __init__(self, cipher, n_window_heuristic=None): self._non_linear_component_id = [] self._intermediate_output_names = [] self._number_of_trails_found = 0 + self._verbose_print = print if verbose else lambda *a, **k: None def fix_variables_value_constraints(self, fixed_variables=[]): """ @@ -259,6 +257,8 @@ def init_model_in_sage_milp_class(self, solver_name=SOLVER_DEFAULT): sage: milp._model Mixed Integer Program (no objective, 0 variables, 0 constraints) """ + if solver_name.upper().endswith("_EXT"): + solver_name = SOLVER_DEFAULT self._model = MixedIntegerLinearProgram(maximization=False, solver=solver_name) self._binary_variable = self._model.new_variable(binary=True) self._integer_variable = self._model.new_variable(integer=True) @@ -266,17 +266,21 @@ def init_model_in_sage_milp_class(self, solver_name=SOLVER_DEFAULT): def _solve_with_external_solver(self, model_type, model_path, solver_name=SOLVER_DEFAULT): - solvers_configuration = get_external_milp_solver_configuration(f'{model_path[:-3]}.sol') - if solver_name not in solvers_configuration: - raise ValueError(f"Invalid solver name: {solver_name}." - f"Currently supported solvers are 'Gurobi', 'cplex', 'scip' and 'glpk'.") - solver_specs = solvers_configuration[solver_name] - command = solver_specs['command'] - options = solver_specs['options'] - tracemalloc.start() + solver_specs = [specs for specs in MILP_SOLVERS_EXTERNAL if specs["solver_name"] == solver_name.upper()][0] + solution_file_path = f'{MODEL_DEFAULT_PATH}/{model_path[:-3]}.sol' - command += model_path + options + command = "" + for key in solver_specs['keywords']['command']['format']: + parameter = solver_specs['keywords']['command'][key] + if key == "input_file": + parameter += " " + model_path + elif key == "output_file": + parameter = parameter + solution_file_path if parameter.endswith('=') else parameter + " " + solution_file_path + elif key == "options": + parameter = " ".join(parameter) + command += " " + parameter + tracemalloc.start() solver_process = subprocess.run(command, capture_output=True, shell=True, text=True) milp_memory = tracemalloc.get_traced_memory()[1] / 10 ** 6 tracemalloc.stop() @@ -285,29 +289,29 @@ def _solve_with_external_solver(self, model_type, model_path, solver_name=SOLVER raise MIPSolverException("Make sure that the solver is correctly installed.") if 'memory' in solver_specs: - milp_memory = _get_data(solver_specs['memory'], str(solver_process)) + milp_memory = _get_data(solver_specs['keywords']['memory'], str(solver_process)) - return _parse_external_solver_output(self, solvers_configuration, model_type, solver_name, solver_process) + (milp_memory,) + return _parse_external_solver_output(self, solver_specs, model_type, solution_file_path, solver_process.stdout) + (milp_memory,) def _solve_with_internal_solver(self): mip = self._model - - verbose_print("Solving model in progress ...") + status = 'UNSATISFIABLE' + self._verbose_print("Solving model in progress ...") time_start = time.time() tracemalloc.start() try: mip.solve() - milp_memory = tracemalloc.get_traced_memory()[1] / 10 ** 6 - tracemalloc.stop() - time_end = time.time() - milp_time = time_end - time_start - verbose_print(f"Time for solving the model = {milp_time}") status = 'SATISFIABLE' except MIPSolverException as milp_exception: - status = 'UNSATISFIABLE' print(milp_exception) + finally: + milp_memory = tracemalloc.get_traced_memory()[1] / 10 ** 6 + tracemalloc.stop() + time_end = time.time() + milp_time = time_end - time_start + self._verbose_print(f"Time for solving the model = {milp_time}") return status, milp_time, milp_memory @@ -332,23 +336,44 @@ def solve(self, model_type, solver_name=SOLVER_DEFAULT, external_solver_name=Non ... sage: solution = milp.solve("xor_differential") # random """ - if external_solver_name: - solver_name_in_solution = external_solver_name + if external_solver_name or (solver_name.upper().endswith("_EXT")): + solver_choice = external_solver_name or solver_name + if solver_choice.upper() not in [specs["solver_name"] for specs in MILP_SOLVERS_EXTERNAL]: + raise ValueError(f"Invalid solver name: {solver_choice}.\n" + f"Please select a solver in the following list: {[specs['solver_name'] for specs in MILP_SOLVERS_EXTERNAL]}.") + + solver_name_in_solution = solver_choice model_path = _write_model_to_lp_file(self, model_type) solution_file_path, status, objective_value, components_values, milp_time, milp_memory = self._solve_with_external_solver( - model_type, model_path, external_solver_name) + model_type, model_path, solver_choice) os.remove(model_path) os.remove(f"{solution_file_path}") else: + objective_value = None + components_values = None solver_name_in_solution = solver_name status, milp_time, milp_memory = self._solve_with_internal_solver() - objective_value, components_values = self._parse_solver_output() + if status == 'SATISFIABLE': + objective_value, components_values = self._parse_solver_output() solution = convert_solver_solution_to_dictionary(self._cipher, model_type, solver_name_in_solution, milp_time, milp_memory, components_values, objective_value) solution['status'] = status return solution + def solver_names(self, verbose=False): + solver_names = [] + + keys = ['solver_brand_name', 'solver_name'] + for solver in MILP_SOLVERS_INTERNAL: + solver_names.append({key: solver[key] for key in keys}) + if verbose: + keys = ['solver_brand_name', 'solver_name', 'keywords'] + + for solver in MILP_SOLVERS_EXTERNAL: + solver_names.append({key: solver[key] for key in keys}) + return solver_names + @property def binary_variable(self): return self._binary_variable diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_deterministic_truncated_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_deterministic_truncated_xor_differential_model.py index 7dbc3de4..5eccd65e 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_deterministic_truncated_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_deterministic_truncated_xor_differential_model.py @@ -16,12 +16,12 @@ # **************************************************************************** import time -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_BITWISE_DETERMINISTIC_TRUNCATED, \ MILP_BACKWARD_SUFFIX, MILP_BUILDING_MESSAGE, MILP_TRUNCATED_XOR_DIFFERENTIAL_OBJECTIVE from claasp.cipher_modules.models.milp.utils.milp_truncated_utils import \ fix_variables_value_deterministic_truncated_xor_differential_constraints -from claasp.cipher_modules.models.milp.milp_model import MilpModel, verbose_print +from claasp.cipher_modules.models.milp.milp_model import MilpModel from claasp.cipher_modules.models.utils import set_component_solution from claasp.name_mappings import (CONSTANT, INTERMEDIATE_OUTPUT, CIPHER_OUTPUT, WORD_OPERATION, LINEAR_LAYER, SBOX, MIX_COLUMN) @@ -29,8 +29,8 @@ class MilpBitwiseDeterministicTruncatedXorDifferentialModel(MilpModel): - def __init__(self, cipher, n_window_heuristic=None): - super().__init__(cipher, n_window_heuristic) + def __init__(self, cipher, n_window_heuristic=None, verbose=False): + super().__init__(cipher, n_window_heuristic, verbose) self._trunc_binvar = None def init_model_in_sage_milp_class(self, solver_name=SOLVER_DEFAULT): @@ -80,7 +80,7 @@ def add_constraints_to_build_in_sage_milp_class(self, fixed_variables=[]): sage: milp.add_constraints_to_build_in_sage_milp_class() """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable @@ -305,7 +305,7 @@ def find_one_bitwise_deterministic_truncated_xor_differential_trail(self, fixed_ """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(fixed_values) @@ -344,7 +344,7 @@ def find_lowest_varied_patterns_bitwise_deterministic_truncated_xor_differential start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model p = self._integer_variable mip.set_objective(p[MILP_TRUNCATED_XOR_DIFFERENTIAL_OBJECTIVE]) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model.py index 93d85e4d..e9931a47 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model.py @@ -18,8 +18,7 @@ import time from claasp.cipher_modules.inverse_cipher import get_key_schedule_component_ids -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT -from claasp.cipher_modules.models.milp.milp_model import verbose_print +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT from claasp.cipher_modules.models.milp.milp_models.milp_bitwise_deterministic_truncated_xor_differential_model import \ MilpBitwiseDeterministicTruncatedXorDifferentialModel from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_BITWISE_IMPOSSIBLE, \ @@ -30,8 +29,8 @@ class MilpBitwiseImpossibleXorDifferentialModel(MilpBitwiseDeterministicTruncatedXorDifferentialModel): - def __init__(self, cipher, n_window_heuristic=None): - super().__init__(cipher, n_window_heuristic) + def __init__(self, cipher, n_window_heuristic=None, verbose=False): + super().__init__(cipher, n_window_heuristic, verbose) self._forward_cipher = None self._backward_cipher = None self._incompatible_components = None @@ -90,7 +89,7 @@ def add_constraints_to_build_in_sage_milp_class(self, middle_round=None, fixed_v sage: milp.add_constraints_to_build_in_sage_milp_class(1) """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable x_class = self._trunc_binvar @@ -161,7 +160,7 @@ def add_constraints_to_build_in_sage_milp_class_with_chosen_incompatible_compone """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable x_class = self._trunc_binvar @@ -254,7 +253,7 @@ def add_constraints_to_build_fully_automatic_model_in_sage_milp_class(self, fixe sage: milp.add_constraints_to_build_fully_automatic_model_in_sage_milp_class() """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable @@ -335,7 +334,7 @@ def find_one_bitwise_impossible_xor_differential_trail(self, middle_round, fixed """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(middle_round, fixed_values) @@ -389,7 +388,7 @@ def find_one_bitwise_impossible_xor_differential_trail_with_chosen_incompatible_ """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class_with_chosen_incompatible_components(component_id_list, fixed_values) @@ -442,7 +441,7 @@ def find_one_bitwise_impossible_xor_differential_trail_with_fully_automatic_mode """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_fully_automatic_model_in_sage_milp_class(fixed_variables=fixed_values, include_all_components=include_all_components) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_deterministic_truncated_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_deterministic_truncated_xor_differential_model.py index 4856ba6e..34348713 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_deterministic_truncated_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_deterministic_truncated_xor_differential_model.py @@ -16,8 +16,8 @@ # **************************************************************************** import time -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT -from claasp.cipher_modules.models.milp.milp_model import MilpModel, verbose_print +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT +from claasp.cipher_modules.models.milp.milp_model import MilpModel from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_WORDWISE_DETERMINISTIC_TRUNCATED, \ MILP_BUILDING_MESSAGE, MILP_TRUNCATED_XOR_DIFFERENTIAL_OBJECTIVE from claasp.cipher_modules.models.milp.utils.utils import espresso_pos_to_constraints, \ @@ -37,8 +37,8 @@ class MilpWordwiseDeterministicTruncatedXorDifferentialModel(MilpModel): - def __init__(self, cipher, n_window_heuristic=None): - super().__init__(cipher, n_window_heuristic) + def __init__(self, cipher, n_window_heuristic=None, verbose=False): + super().__init__(cipher, n_window_heuristic, verbose) self._trunc_wordvar = None self._word_size = 4 if self._cipher.is_spn(): @@ -96,7 +96,7 @@ def add_constraints_to_build_in_sage_milp_class(self, fixed_bits=[], fixed_words sage: milp.add_constraints_to_build_in_sage_milp_class() """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable @@ -323,7 +323,7 @@ def find_one_wordwise_deterministic_truncated_xor_differential_trail(self, fixed """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(fixed_bits, fixed_words) @@ -363,7 +363,7 @@ def find_lowest_varied_patterns_wordwise_deterministic_truncated_xor_differentia start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model p = self._integer_variable mip.set_objective(p[MILP_TRUNCATED_XOR_DIFFERENTIAL_OBJECTIVE]) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model.py index 7bc4f7b8..7c3a4a37 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model.py @@ -18,24 +18,18 @@ import time from claasp.cipher_modules.inverse_cipher import get_key_schedule_component_ids -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT -from claasp.cipher_modules.models.milp.milp_model import verbose_print +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT from claasp.cipher_modules.models.milp.milp_models.milp_wordwise_deterministic_truncated_xor_differential_model import MilpWordwiseDeterministicTruncatedXorDifferentialModel -from claasp.cipher_modules.models.milp.utils.generate_inequalities_for_wordwise_truncated_xor_with_n_input_bits import \ - update_dictionary_that_contains_wordwise_truncated_input_inequalities, \ - output_dictionary_that_contains_wordwise_truncated_input_inequalities from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_WORDWISE_IMPOSSIBLE_AUTO, \ MILP_WORDWISE_IMPOSSIBLE, MILP_BACKWARD_SUFFIX, MILP_BUILDING_MESSAGE -from claasp.cipher_modules.models.milp.utils.utils import espresso_pos_to_constraints -from claasp.cipher_modules.models.utils import set_component_solution from claasp.name_mappings import CIPHER_OUTPUT, INPUT_KEY from claasp.cipher_modules.models.milp.utils import utils as milp_utils, milp_truncated_utils class MilpWordwiseImpossibleXorDifferentialModel(MilpWordwiseDeterministicTruncatedXorDifferentialModel): - def __init__(self, cipher, n_window_heuristic=None): - super().__init__(cipher, n_window_heuristic) + def __init__(self, cipher, n_window_heuristic=None, verbose=False): + super().__init__(cipher, n_window_heuristic, verbose) self._forward_cipher = None self._backward_cipher = None self._incompatible_components = None @@ -97,7 +91,7 @@ def add_constraints_to_build_in_sage_milp_class(self, middle_round=None, fixed_b sage: milp.add_constraints_to_build_in_sage_milp_class(1, get_single_key_scenario_format_for_fixed_values(aes)) """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable @@ -177,7 +171,7 @@ def add_constraints_to_build_in_sage_milp_class_with_chosen_incompatible_compone sage: milp.add_constraints_to_build_in_sage_milp_class_with_fixed_components(["intermediate_output_0_37"], get_single_key_scenario_format_for_fixed_values(aes)) """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable @@ -282,7 +276,7 @@ def add_constraints_to_build_fully_automatic_model_in_sage_milp_class(self, fixe sage: milp.add_constraints_to_build_fully_automatic_model_in_sage_milp_class(get_single_key_scenario_format_for_fixed_values(aes)) """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) mip = self._model x = self._binary_variable @@ -337,7 +331,7 @@ def find_one_wordwise_impossible_xor_differential_trail(self, middle_round=None, """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(middle_round, fixed_bits, fixed_words) @@ -372,7 +366,7 @@ def find_one_wordwise_impossible_xor_differential_trail_with_chosen_components(s """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class_with_chosen_incompatible_components(component_id_list, @@ -409,7 +403,7 @@ def find_one_wordwise_impossible_xor_differential_trail_with_fully_automatic_mod """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_fully_automatic_model_in_sage_milp_class(fixed_bits, fixed_words, include_all_components=include_all_components) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py index 2c9e186e..81be9427 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py @@ -20,8 +20,8 @@ from bitstring import BitArray -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT -from claasp.cipher_modules.models.milp.milp_model import MilpModel, verbose_print +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT +from claasp.cipher_modules.models.milp.milp_model import MilpModel from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_XOR_DIFFERENTIAL, MILP_PROBABILITY_SUFFIX, \ MILP_BUILDING_MESSAGE, MILP_XOR_DIFFERENTIAL_OBJECTIVE from claasp.cipher_modules.models.milp.utils.utils import _string_to_hex, _get_variables_values_as_string, _filter_fixed_variables @@ -33,8 +33,8 @@ class MilpXorDifferentialModel(MilpModel): - def __init__(self, cipher, n_window_heuristic=None): - super().__init__(cipher, n_window_heuristic) + def __init__(self, cipher, n_window_heuristic=None, verbose=False): + super().__init__(cipher, n_window_heuristic, verbose) def add_constraints_to_build_in_sage_milp_class(self, weight=-1, fixed_variables=[]): """ @@ -65,7 +65,7 @@ def add_constraints_to_build_in_sage_milp_class(self, weight=-1, fixed_variables sage: mip.number_of_variables() 468 """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) self.build_xor_differential_trail_model(weight, fixed_variables) mip = self._model p = self._integer_variable @@ -171,7 +171,7 @@ def find_all_xor_differential_trails_with_fixed_weight(self, fixed_weight, fixed """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) @@ -202,7 +202,7 @@ def find_all_xor_differential_trails_with_fixed_weight(self, fixed_weight, fixed solution['building_time'] = building_time solution['test_name'] = "find_all_xor_differential_trails_with_fixed_weight" self._number_of_trails_found += 1 - verbose_print(f"trails found : {self._number_of_trails_found}") + self._verbose_print(f"trails found : {self._number_of_trails_found}") list_trails.append(solution) fixed_variables = self._get_fixed_variables_from_solution(fixed_values, inputs_ids, solution) @@ -374,7 +374,7 @@ def find_all_xor_differential_trails_with_weight_at_most(self, min_weight, max_w """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) @@ -403,7 +403,7 @@ def find_all_xor_differential_trails_with_weight_at_most(self, min_weight, max_w solution['building_time'] = building_time solution['test_name'] = "find_all_xor_differential_trails_with_weight_at_most" self._number_of_trails_found += 1 - verbose_print(f"trails found : {self._number_of_trails_found}") + self._verbose_print(f"trails found : {self._number_of_trails_found}") list_trails.append(solution) fixed_variables = self._get_fixed_variables_from_solution(fixed_values, inputs_ids, solution) @@ -463,7 +463,7 @@ def find_lowest_weight_xor_differential_trail(self, fixed_values=[], solver_name """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model p = self._integer_variable mip.set_objective(p[MILP_XOR_DIFFERENTIAL_OBJECTIVE]) @@ -496,7 +496,7 @@ def find_one_xor_differential_trail(self, fixed_values=[], solver_name=SOLVER_DE EXAMPLES:: # single-key setting - from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel sage: speck = SpeckBlockCipher(number_of_rounds=5) sage: milp = MilpXorDifferentialModel(speck) @@ -513,7 +513,7 @@ def find_one_xor_differential_trail(self, fixed_values=[], solver_name=SOLVER_DE """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) @@ -567,7 +567,7 @@ def find_one_xor_differential_trail_with_fixed_weight(self, fixed_weight, fixed_ """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index f2b96186..fea4b3a9 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -22,11 +22,11 @@ from bitstring import BitArray -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT from claasp.cipher_modules.models.milp.utils.generate_inequalities_for_xor_with_n_input_bits import \ update_dictionary_that_contains_xor_inequalities_between_n_input_bits, \ output_dictionary_that_contains_xor_inequalities -from claasp.cipher_modules.models.milp.milp_model import MilpModel, verbose_print +from claasp.cipher_modules.models.milp.milp_model import MilpModel from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_XOR_LINEAR, MILP_PROBABILITY_SUFFIX, \ MILP_BUILDING_MESSAGE, MILP_XOR_LINEAR_OBJECTIVE from claasp.cipher_modules.models.milp.utils.utils import _get_variables_values_as_string, _string_to_hex, \ @@ -38,8 +38,8 @@ class MilpXorLinearModel(MilpModel): - def __init__(self, cipher, n_window_heuristic=None): - super().__init__(cipher, n_window_heuristic) + def __init__(self, cipher, n_window_heuristic=None, verbose=False): + super().__init__(cipher, n_window_heuristic, verbose) self.bit_bindings, self.bit_bindings_for_intermediate_output = get_bit_bindings(cipher, '_'.join) def add_constraints_to_build_in_sage_milp_class(self, weight=-1, fixed_variables=[]): @@ -71,7 +71,7 @@ def add_constraints_to_build_in_sage_milp_class(self, weight=-1, fixed_variables sage: mip.number_of_variables() 1018 """ - verbose_print(MILP_BUILDING_MESSAGE) + self._verbose_print(MILP_BUILDING_MESSAGE) self.build_xor_linear_trail_model(weight, fixed_variables) mip = self._model p = self._integer_variable @@ -299,7 +299,7 @@ def find_all_xor_linear_trails_with_fixed_weight(self, fixed_weight, fixed_value """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) @@ -323,7 +323,7 @@ def find_all_xor_linear_trails_with_fixed_weight(self, fixed_weight, fixed_value solution['building_time'] = building_time solution['test_name'] = "find_all_xor_linear_trails_with_fixed_weight" self._number_of_trails_found += 1 - verbose_print(f"trails found : {self._number_of_trails_found}") + self._verbose_print(f"trails found : {self._number_of_trails_found}") list_trails.append(solution) fixed_variables = self._get_fixed_variables_from_solution(fixed_values, inputs_ids, solution) @@ -394,7 +394,7 @@ def find_all_xor_linear_trails_with_weight_at_most(self, min_weight, max_weight, """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) @@ -418,7 +418,7 @@ def find_all_xor_linear_trails_with_weight_at_most(self, min_weight, max_weight, solution['building_time'] = building_time solution['test_name'] = "find_all_xor_linear_trails_with_weight_at_most" self._number_of_trails_found += 1 - verbose_print(f"trails found : {self._number_of_trails_found}") + self._verbose_print(f"trails found : {self._number_of_trails_found}") list_trails.append(solution) fixed_variables = self._get_fixed_variables_from_solution(fixed_values, inputs_ids, solution) @@ -494,7 +494,7 @@ def find_lowest_weight_xor_linear_trail(self, fixed_values=[], solver_name=SOLVE """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model p = self._integer_variable mip.set_objective(p[MILP_XOR_LINEAR_OBJECTIVE]) @@ -542,7 +542,7 @@ def find_one_xor_linear_trail(self, fixed_values=[], solver_name=SOLVER_DEFAULT, """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) @@ -595,7 +595,7 @@ def find_one_xor_linear_trail_with_fixed_weight(self, fixed_weight, fixed_values """ start = time.time() self.init_model_in_sage_milp_class(solver_name) - verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") + self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, fixed_values) diff --git a/claasp/cipher_modules/models/milp/solvers.py b/claasp/cipher_modules/models/milp/solvers.py new file mode 100644 index 00000000..6f7650d3 --- /dev/null +++ b/claasp/cipher_modules/models/milp/solvers.py @@ -0,0 +1,120 @@ +import os + +# **************************************************************************** +# Copyright 2023 Technology Innovation Institute +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# **************************************************************************** + + +SOLVER_DEFAULT = "GLPK" +MODEL_DEFAULT_PATH = os.getcwd() + + + +MILP_SOLVERS_INTERNAL = [ + {"solver_brand_name": "GLPK (GNU Linear Programming Kit) (using Sage backend)", "solver_name": "GLPK"}, + {"solver_brand_name": "GLPK (GNU Linear Programming Kit) with simplex method based on exact arithmetic (using Sage backend)", "solver_name": "GLPK/exact"}, + {"solver_brand_name": "COIN-BC (COIN Branch and Cut) (using Sage backend)", "solver_name": "Coin"}, + {"solver_brand_name": "CVXOPT (Python Software for Convex Optimization) (using Sage backend)", "solver_name": "CVXOPT"}, + {"solver_brand_name": "Gurobi Optimizer (using Sage backend)", "solver_name": "Gurobi"}, + {"solver_brand_name": "PPL (Parma Polyhedra Library) (using Sage backend)", "solver_name": "PPL"}, + {"solver_brand_name": "InteractiveLP (using Sage backend)", "solver_name": "InteractiveLP"}, +] + +MILP_SOLVERS_EXTERNAL = [ + { + "solver_brand_name": "Gurobi Optimizer (external)", + "solver_name": "GUROBI_EXT", + "keywords": { + "command": { + "executable": "gurobi_cl", + "options": [], + "input_file": "", + "solve": "", + "output_file": "ResultFile=", + "end": "", + "format": ["executable", "output_file", "input_file"], + }, + "time": r"Explored \d+ nodes \(\d+ simplex iterations\) in ([0-9]*[.]?[0-9]+) seconds", + "unsat_condition": "Model is infeasible", + }, + }, + { + "solver_brand_name": "GLPK (GNU Linear Programming Kit) (external)", + "solver_name": "GLPK_EXT", + "keywords": { + "command": { + "executable": "glpsol", + "options": ["--lp"], + "input_file": "", + "solve": "", + "output_file": "--output ", + "end": "", + "format": ["executable", "options", "input_file", "output_file"], + }, + "time": r"Time used:[\s]+([0-9]*[.]?[0-9]+) secs", + "memory": r"Memory used:[\s]+([0-9]*[.]?[0-9]+) Mb", + "unsat_condition": r"PROBLEM HAS NO (\w+) FEASIBLE SOLUTION", + }, + }, + { + "solver_brand_name": "SCIP (Solving Constraint Integer Programs) (external)", + "solver_name": "SCIP_EXT", + "keywords": { + "command": { + "executable": "scip", + "options": ["-c", '"'], + "input_file": "read", + "solve": "optimize", + "output_file": "write solution", + "end": '"quit', + "format": [ + "executable", + "options", + "input_file", + "solve", + "output_file", + "end", + ], + }, + "time": r"Solving Time \(sec\)[\s]+:[\s]+([0-9]*[.]?[0-9]+)", + "unsat_condition": r"problem is solved \[infeasible\]", + }, + }, + { + "solver_brand_name": "IBM ILOG CPLEX Optimizer (external)", + "solver_name": "CPLEX_EXT", + "keywords": { + "command": { + "executable": "cplex", + "options": ["-c"], + "input_file": "read", + "solve": "optimize", + "output_file": "set logfile", + "end": "display solution variables -", + "format": [ + "executable", + "options", + "input_file", + "solve", + "output_file", + "end", + ], + }, + "time": r"Solution time =[\s]+([0-9]*[.]?[0-9]+) sec.", + "unsat_condition": "MIP - Integer infeasible.", + }, + }, +] diff --git a/claasp/cipher_modules/models/milp/utils/config.py b/claasp/cipher_modules/models/milp/utils/config.py deleted file mode 100644 index a620edaa..00000000 --- a/claasp/cipher_modules/models/milp/utils/config.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - -# **************************************************************************** -# Copyright 2023 Technology Innovation Institute -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# **************************************************************************** - - -SOLVER_DEFAULT = "GLPK" -MODEL_DEFAULT_PATH = os.getcwd() -SOLUTION_FILE_DEFAULT_NAME = "milp_model.sol" - -def get_external_milp_solver_configuration(solution_name=SOLUTION_FILE_DEFAULT_NAME): - - external_milp_solvers_configuration = { - 'Gurobi': { - 'command': f'gurobi_cl ResultFile={MODEL_DEFAULT_PATH}/{solution_name} ', - 'options': "", - 'time': r"Explored \d+ nodes \(\d+ simplex iterations\) in ([0-9]*[.]?[0-9]+) seconds", - 'unsat_condition': "Model is infeasible" - }, - 'scip': { - 'file_path': [], - 'command': 'scip -c \"read ', - 'options': f' opt write solution {MODEL_DEFAULT_PATH}/{solution_name} quit\"', - 'time': r"Solving Time \(sec\)[\s]+:[\s]+([0-9]*[.]?[0-9]+)", - 'unsat_condition': "problem is solved [infeasible]" - }, - 'glpk': { - 'command': 'glpsol --lp ', - 'options': f' --output {MODEL_DEFAULT_PATH}/{solution_name}', - 'time': r"Time used:[\s]+([0-9]*[.]?[0-9]+) secs", - 'memory': r"Memory used:[\s]+([0-9]*[.]?[0-9]+) Mb", - 'unsat_condition': 'PROBLEM HAS NO PRIMAL FEASIBLE SOLUTION' - }, - 'cplex': { - 'command': 'cplex -c \"read ', - 'options': f'\" \"optimize\" \"display solution variables *\" | tee {MODEL_DEFAULT_PATH}/{solution_name}', - 'time': r"Solution time =[\s]+([0-9]*[.]?[0-9]+) sec.", - 'unsat_condition': "MIP - Integer infeasible." - } - } - - for solver in external_milp_solvers_configuration: - external_milp_solvers_configuration[solver]['solution_path'] = f'{MODEL_DEFAULT_PATH}/{solution_name}' - - return external_milp_solvers_configuration diff --git a/claasp/cipher_modules/models/milp/utils/generate_inequalities_for_and_operation_2_input_bits.py b/claasp/cipher_modules/models/milp/utils/generate_inequalities_for_and_operation_2_input_bits.py index 5a720ddf..5af3f6af 100644 --- a/claasp/cipher_modules/models/milp/utils/generate_inequalities_for_and_operation_2_input_bits.py +++ b/claasp/cipher_modules/models/milp/utils/generate_inequalities_for_and_operation_2_input_bits.py @@ -18,7 +18,7 @@ """The target of this module is to generate MILP inequalities for a AND operation between 2 input bits.""" -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT def and_inequalities(): diff --git a/claasp/cipher_modules/models/milp/utils/generate_sbox_inequalities_for_trail_search.py b/claasp/cipher_modules/models/milp/utils/generate_sbox_inequalities_for_trail_search.py index ece6f1ef..8cfc2340 100644 --- a/claasp/cipher_modules/models/milp/utils/generate_sbox_inequalities_for_trail_search.py +++ b/claasp/cipher_modules/models/milp/utils/generate_sbox_inequalities_for_trail_search.py @@ -30,7 +30,7 @@ from claasp.cipher_modules.models.milp import MILP_AUXILIARY_FILE_PATH from sage.rings.integer_ring import ZZ -from claasp.cipher_modules.models.milp.utils.config import SOLVER_DEFAULT +from claasp.cipher_modules.models.milp.solvers import SOLVER_DEFAULT small_sbox_file_name = "dictionary_that_contains_inequalities_for_small_sboxes.obj" small_sbox_xor_linear_file_name = "dictionary_that_contains_inequalities_for_small_sboxes_xor_linear.obj" diff --git a/claasp/cipher_modules/models/milp/utils/utils.py b/claasp/cipher_modules/models/milp/utils/utils.py index 3101c25e..c4b0b77c 100644 --- a/claasp/cipher_modules/models/milp/utils/utils.py +++ b/claasp/cipher_modules/models/milp/utils/utils.py @@ -25,7 +25,6 @@ output_dictionary_that_contains_xor_inequalities, update_dictionary_that_contains_xor_inequalities_between_n_input_bits) -from claasp.cipher_modules.models.milp.utils.config import get_external_milp_solver_configuration from sage.numerical.mip import MIPSolverException from claasp.cipher_modules.models.milp.utils.milp_name_mappings import MILP_BITWISE_DETERMINISTIC_TRUNCATED, \ @@ -62,20 +61,16 @@ def _get_variables_value(internal_variables, read_file): return variables_value -def _parse_external_solver_output(model, solvers_configurations, model_type, solver_name, solver_process): - solver_specs = solvers_configurations[solver_name] - - solve_time = _get_data(solver_specs['time'], str(solver_process)) +def _parse_external_solver_output(model, solver_specs, model_type, solution_file_path, solver_process): + solve_time = _get_data(solver_specs['keywords']['time'], solver_process) status = 'UNSATISFIABLE' objective_value = None components_values = None - if solver_specs['unsat_condition'] not in str(solver_process): + if re.findall(solver_specs['keywords']['unsat_condition'], solver_process) == []: status = 'SATISFIABLE' - solution_file_path = solver_specs['solution_path'] - with open(solution_file_path, 'r') as lp_file: read_file = lp_file.read() diff --git a/claasp/cipher_modules/report.py b/claasp/cipher_modules/report.py index 345dad89..c2fa5b9d 100644 --- a/claasp/cipher_modules/report.py +++ b/claasp/cipher_modules/report.py @@ -4,11 +4,13 @@ from plotly import express as px import plotly.graph_objects as go import pandas as pd +import itertools import json import shutil from claasp.cipher_modules.statistical_tests.dieharder_statistical_tests import DieharderTests from claasp.cipher_modules.statistical_tests.nist_statistical_tests import NISTStatisticalTests from claasp.cipher_modules.component_analysis_tests import CipherComponentsAnalysis +from datetime import datetime def _print_colored_state(state, verbose, file): @@ -17,11 +19,11 @@ def _print_colored_state(state, verbose, file): for x in line: print(f'{x} ', end='', file=file) - occ = [i for i in range(len(line)) if line[i] != '_' and line[i] != '*'] + occ = [i for i in range(len(line)) if line[i] != '_' and line[i] != '*' and line[i] != '0'] if verbose: print(f'\tactive words positions = {occ}', file=file) else: - print('', file=file) + print('', end=' ', file=file) def _dict_to_latex_table(data_dict, header_list): @@ -151,7 +153,7 @@ def __init__(self, test_report): self.input_parameters = {} self.test_name = test_report['test_name'] if type(test_report) is dict else test_report[0]['test_name'] - def show(self, test_name=None, fixed_input='plaintext', fixed_output='round_output', + def show(self, show_as_hex=False, test_name=None, fixed_input='plaintext', fixed_output='round_output', fixed_input_difference='average', word_size=1, state_size=1, key_state_size=1, verbose=False, show_word_permutation=False, show_var_shift=False, show_var_rotate=False, show_theta_xoodoo=False, @@ -165,7 +167,10 @@ def show(self, test_name=None, fixed_input='plaintext', fixed_output='round_outp show_intermediate_output=True, show_cipher_output=True, show_input=True, show_output=True): if 'trail' in self.test_name: - self._print_trail(word_size, state_size, key_state_size, verbose, show_word_permutation, + if show_as_hex == True and (word_size / 4).is_integer() == False: + print("Incorrect word_size: if show_as_hex=True, word_size has to be a multiple of 4") + return + self._print_trail(show_as_hex, word_size, state_size, key_state_size, verbose, show_word_permutation, show_var_shift, show_var_rotate, show_theta_xoodoo, show_theta_keccak, show_shift_rows, show_sigma, show_reverse, show_permuation, show_multi_input_non_linear_logical_operator, @@ -192,7 +197,7 @@ def show(self, test_name=None, fixed_input='plaintext', fixed_output='round_outp 'input_difference_value' in x.keys()] if fixed_input_difference not in input_diff_values: print( - 'Error! Invalid input difference value. The report.show() function requires an input_difference_value input') + 'Error! Invalid input difference value. The report.show() function requires a fixed_input_difference input') print('input_difference_value has to be one of the following :', end='') print(input_diff_values) return @@ -202,14 +207,14 @@ def show(self, test_name=None, fixed_input='plaintext', fixed_output='round_outp 'neural_network_differential_distinguisher']] if fixed_input_difference not in input_diff_values: print( - 'Error! Invalid input difference value. The report.show() function requires an input_difference_value input') + 'Error! Invalid input difference value. The report.show() function requires a fixed_input_difference input') print('The input difference value has to be one of the following :', end='') print(input_diff_values) return self._produce_graph(show_graph=True, fixed_input=fixed_input, fixed_output=fixed_output, fixed_input_difference=fixed_input_difference, test_name=test_name) - def _export(self, file_format, output_dir, fixed_input = None, fixed_output=None, fixed_test = None): + def _export(self, file_format, output_dir, fixed_input=None, fixed_output=None, fixed_test=None): if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -290,7 +295,8 @@ def _export(self, file_format, output_dir, fixed_input = None, fixed_output=None os.makedirs(output_dir + '/' + self.cipher.id + '/' + self.test_report["input_parameters"][ "test_name"] + '_tables/' + it + '/' + out) - for test in self.test_report["test_results"][it][out].keys() if fixed_test == None else [fixed_test]: + for test in self.test_report["test_results"][it][out].keys() if fixed_test == None else [ + fixed_test]: if not os.path.exists( output_dir + '/' + self.cipher.id + '/' + self.test_report["input_parameters"][ "test_name"] + '_tables/' + it + '/' + out + '/' + test): @@ -395,16 +401,190 @@ def _export(self, file_format, output_dir, fixed_input = None, fixed_output=None print("Report saved in " + output_dir + '/' + self.cipher.id) - def save_as_DataFrame(self, output_dir=os.getcwd() + '/test_reports', fixed_input = None, fixed_output=None, fixed_test=None): - self._export(file_format='.csv', output_dir=output_dir, fixed_input=fixed_input, fixed_output=fixed_output, fixed_test = fixed_test) + def save_as_DataFrame(self, output_dir=os.getcwd() + '/test_reports', fixed_input=None, fixed_output=None, + fixed_test=None): + self._export(file_format='.csv', output_dir=output_dir, fixed_input=fixed_input, fixed_output=fixed_output, + fixed_test=fixed_test) + + def save_as_latex_table(self, output_dir=os.getcwd() + '/test_reports', fixed_input=None, fixed_output=None, + fixed_test=None): + self._export(file_format='.tex', output_dir=output_dir, fixed_input=fixed_input, fixed_output=fixed_output, + fixed_test=fixed_test) + + def save_as_json(self, output_dir=os.getcwd() + '/test_reports', fixed_input=None, fixed_output=None, + fixed_test=None): + self._export(file_format='.json', output_dir=output_dir, fixed_input=fixed_input, fixed_output=fixed_output, + fixed_test=fixed_test) + + def _get_component_types(self): + component_types = [] + show_key_flow = False + for comp in list(self.test_report['components_values'].keys()): + if 'key' in comp: + show_key_flow = True + if ('key' in comp or comp == 'plaintext') and comp not in component_types: + component_types.append(comp) + elif '_'.join(comp.split('_')[:-2]) not in component_types and comp[-2:] != "_i" and comp[-2:] != "_o": + component_types.append('_'.join(comp.split('_')[:-2])) + elif ('_'.join(comp.split('_')[:-3])) + '_' + ('_'.join(comp.split('_')[-1])) not in component_types and ( + comp[-2:] == "_i" or comp[-2:] == "_o"): + component_types.append(('_'.join(comp.split('_')[:-3])) + '_' + ('_'.join(comp.split('_')[-1]))) + + return component_types, show_key_flow + + def _get_show_components(self, component_types, show_output, show_input, show_key, input_comps, var_choices): + + show_components = {} + for comp, comp_choice in itertools.product(component_types, input_comps): + if 'show_' + comp == comp_choice: + show_components[comp] = var_choices[comp_choice] + elif 'show_' + comp == comp_choice + '_o' and show_output: + show_components[comp] = var_choices[comp_choice] + elif 'show_' + comp == comp_choice + '_i' and show_input: + show_components[comp] = var_choices[comp_choice] + + return show_components + + def _update_out_list(self, out_list, rel_prob, abs_prob, show_as_hex, comp_id, word_size, state_size, + key_state_size, key_flow, word_denominator): + + value = self.test_report['components_values'][comp_id]['value'] + bin_list = list(format(int(value, 16), 'b').zfill( + 4 * len(value) if value[:2] != '0x' else 4 * len(value[2:]))) if '*' not in value else list( + value[2:]) + + if show_as_hex == False: + word_list = ['*' if '*' in ''.join(bin_list[x:x + word_size]) else word_denominator if ''.join( + bin_list[x:x + word_size]).count('1') > 0 else '_' for x in + range(0, len(bin_list), word_size)] + else: + word_list = [ + '*' if '*' in ''.join(bin_list[x:x + word_size]) else hex(int(''.join(bin_list[x:x + word_size]), 2))[ + 2:].zfill(int(word_size/4)) for x + in range(0, len(bin_list), word_size)] + + if ('intermediate' in comp_id or 'cipher' in comp_id) and comp_id not in key_flow: + size = (state_size, len(word_list) // state_size) + + elif ('intermediate' in comp_id or 'cipher' in comp_id) and comp_id in key_flow and comp_id != 'key': + size = (key_state_size, len(word_list) // key_state_size) + + else: + size = (1, len(word_list)) + out_format = [[] for _ in range(size[0])] + for i, j in itertools.product(range(size[0]), range(size[1])): + if show_as_hex == False: + if word_list[j + i * size[1]] == word_denominator: + out_format[i].append(f'\033[31;4m{word_list[j + i * size[1]]}\033[0m') + else: + out_format[i].append(word_list[j + i * size[1]]) + else: + if word_list[j + i * size[1]] == '0': + out_format[i].append(word_list[j + i * size[1]]) + else: + out_format[i].append(f'\033[31;4m{word_list[j + i * size[1]]}\033[0m') - def save_as_latex_table(self, output_dir=os.getcwd() + '/test_reports', fixed_input = None, fixed_output=None, fixed_test=None): - self._export(file_format='.tex', output_dir=output_dir, fixed_input=fixed_input, fixed_output=fixed_output, fixed_test = fixed_test) + out_list[comp_id] = (out_format, rel_prob, abs_prob) if comp_id not in ["plaintext", "key"] else ( + out_format, 0, 0) - def save_as_json(self, output_dir=os.getcwd() + '/test_reports', fixed_input = None, fixed_output=None, fixed_test=None): - self._export(file_format='.json', output_dir=output_dir, fixed_input=fixed_input, fixed_output=fixed_output, fixed_test = fixed_test) + def _get_comp_value_and_key_flow(self, comp_id, key_flow): + + if comp_id[-2:] == "_i" or comp_id[-2:] == "_o": + input_links = self.cipher.get_component_from_id(comp_id[:-2]).input_id_links + comp_value = ('_'.join(comp_id.split('_')[:-3])) + '_' + ('_'.join(comp_id.split('_')[-1])) + else: + input_links = self.cipher.get_component_from_id(comp_id).input_id_links + comp_value = '_'.join(comp_id.split('_')[:-2]) + + if (all( + id_link in key_flow or 'constant' in id_link or id_link + '_o' in key_flow or id_link + '_i' in key_flow + for id_link in input_links) or ('key' in comp_id and comp_id != 'key')): + key_flow.append(comp_id) + if 'linear' in self.test_name: + constants_i = [constant_id + '_i' for constant_id in input_links if 'constant' in constant_id] + constants_o = [constant_id + '_o' for constant_id in input_links if 'constant' in constant_id] + key_flow += constants_i + constants_o + else: + constants = [constant_id for constant_id in input_links if 'constant' in constant_id] + key_flow += constants + return comp_value, key_flow + + def _get_show_key_flow(self, key_flow, word_size, word_denominator): + show_key_flow = False + for key_comp in key_flow: + key_value = self.test_report['components_values'][key_comp]['value'] + bin_list = list(format(int(key_value, 16), 'b').zfill( + 4 * len(key_value) if key_value[:2] != '0x' else 4 * len( + key_value[2:]))) if '*' not in key_value else list( + key_value[2:]) + word_list = ['*' if '*' in ''.join(bin_list[x:x + word_size]) else word_denominator if ''.join( + bin_list[x:x + word_size]).count('1') > 0 else '_' for x in + range(0, len(bin_list), word_size)] + + if word_list.count(word_denominator) > 0: + show_key_flow = True + break + return show_key_flow + + def _print_plaintext_flow(self, out_list, key_flow, verbose, file, save_fig): + for comp_id in [comp for comp in out_list.keys() if comp not in key_flow]: + if comp_id == 'plaintext': + if verbose == True: + print(f'{comp_id}\t', file=file) + _print_colored_state(out_list[comp_id][0], verbose, file) + else: + _print_colored_state(out_list[comp_id][0], verbose, file) + print(f' {comp_id}\t', file=file) + else: + if verbose: + print( + f'{comp_id} Input Links : {self.cipher.get_component_from_id(comp_id if comp_id[-2:] not in ["_i", "_o"] else comp_id[:-2]).input_id_links}', + file=file if save_fig else None) + _print_colored_state(out_list[comp_id][0], verbose, file) + else: + _print_colored_state(out_list[comp_id][0], verbose, file) + print(f' {comp_id}', file=file) + if verbose: + print('local weight = ' + str( + out_list[comp_id][1]) + '\t' + 'total weight = ' + str( + out_list[comp_id][2]), file=file) + print('', file=file) + print('', file=file) + print('total weight = ' + str(self.test_report['total_weight']), file=file) + + def _print_key_flow(self, key_flow, show_components, out_list, verbose, file): + + print('', file=file) + print("KEY FLOW", file=file) + print('', file=file) + + for comp_id in key_flow: + if comp_id[-2:] == "_i" or comp_id[-2:] == "_o": + comp_value = ('_'.join(comp_id.split('_')[:-3])) + '_' + ('_'.join(comp_id.split('_')[-1])) + else: + comp_value = '_'.join(comp_id.split('_')[:-2]) + if show_components[ + comp_value if (comp_id not in ['plaintext', 'cipher_output', 'cipher_output_o', 'cipher_output_i', + 'intermediate_output', 'intermediate_output_o', + 'intermediate_output_i'] and 'key' not in comp_id) else comp_id]: + if 'key' in comp_id: + _print_colored_state(out_list[comp_id][0], verbose, file) + print(f'\t{comp_id}\t', file=file) + else: + if verbose: + print( + f'{comp_id} Input Links : {self.cipher.get_component_from_id(comp_id if comp_id[-2:] not in ["_i", "_o"] else comp_id[:-2]).input_id_links}', + file=file) + _print_colored_state(out_list[comp_id][0], verbose, file) + else: + _print_colored_state(out_list[comp_id][0], verbose, file) + print(f'{comp_id}\t', file=file) + if verbose: print('local weight = ' + str( + out_list[comp_id][1]) + '\t' + 'total weight = ' + str( + out_list[comp_id][2]), file=file) + print('', file=file) - def _print_trail(self, word_size, state_size, key_state_size, verbose, show_word_permutation, + def _print_trail(self, show_as_hex, word_size, state_size, key_state_size, verbose, show_word_permutation, show_var_shift, show_var_rotate, show_theta_xoodoo, show_theta_keccak, show_shift_rows, show_sigma, show_reverse, show_permuation, show_multi_input_non_linear_logical_operator, @@ -429,33 +609,14 @@ def _print_trail(self, word_size, state_size, key_state_size, verbose, show_word file = None input_comps = list(locals().keys()) - component_types = [] - for comp in list(self.test_report['components_values'].keys()): - if 'key' in comp: - show_key_flow = True - if ('key' in comp or comp == 'plaintext') and comp not in component_types: - component_types.append(comp) - elif '_'.join(comp.split('_')[:-2]) not in component_types and comp[-2:] != "_i" and comp[-2:] != "_o": - component_types.append('_'.join(comp.split('_')[:-2])) - elif ('_'.join(comp.split('_')[:-3])) + '_' + ('_'.join(comp.split('_')[-1])) not in component_types and ( - comp[-2:] == "_i" or comp[-2:] == "_o"): - component_types.append(('_'.join(comp.split('_')[:-3])) + '_' + ('_'.join(comp.split('_')[-1]))) + component_types, show_key_flow = self._get_component_types() + show_components = self._get_show_components(component_types, show_output, show_input, show_key, input_comps, + locals()) - show_components = {} - for comp in component_types: - for comp_choice in input_comps: - if 'show_' + comp == comp_choice: - show_components[comp] = locals()[comp_choice] - if 'show_' + comp == comp_choice + '_o' and show_output: - show_components[comp] = locals()[comp_choice] - if 'show_' + comp == comp_choice + '_i' and show_input: - show_components[comp] = locals()[comp_choice] - if 'key' in comp and show_key == True: - show_components[comp] = True out_list = {} - key_flow = ['key'] + key_flow = [key for key in self.cipher.inputs if key == 'key'] abs_prob = 0 rel_prob = 0 @@ -468,121 +629,22 @@ def _print_trail(self, word_size, state_size, key_state_size, verbose, show_word abs_prob += rel_prob # Check input links - - if 'plaintext' not in comp_id and 'key' not in comp_id: - if comp_id[-2:] == "_i" or comp_id[-2:] == "_o": - input_links = self.cipher.get_component_from_id(comp_id[:-2]).input_id_links - comp_value = ('_'.join(comp_id.split('_')[:-3])) + '_' + ('_'.join(comp_id.split('_')[-1])) - else: - input_links = self.cipher.get_component_from_id(comp_id).input_id_links - comp_value = '_'.join(comp_id.split('_')[:-2]) - - if (all(( - id_link in key_flow or 'constant' in id_link or id_link + '_o' in key_flow or id_link + '_i' in key_flow) - for id_link in input_links) or ('key' in comp_id and 'comp_id' != 'key')): - key_flow.append(comp_id) - if 'linear' in self.test_name: - constants_i = [constant_id + '_i' for constant_id in input_links if 'constant' in constant_id] - constants_o = [constant_id + '_o' for constant_id in input_links if 'constant' in constant_id] - key_flow = key_flow + constants_i + constants_o - else: - constants = [constant_id for constant_id in input_links if 'constant' in constant_id] - key_flow = key_flow + constants + if comp_id != 'plaintext' and 'key' not in comp_id: + comp_value, key_flow = self._get_comp_value_and_key_flow(comp_id, key_flow) if show_components[ comp_value if (comp_id not in ['plaintext', 'cipher_output', 'cipher_output_o', 'cipher_output_i', 'intermediate_output', 'intermediate_output_o', 'intermediate_output_i'] and 'key' not in comp_id) else comp_id]: + self._update_out_list(out_list, rel_prob, abs_prob, show_as_hex, comp_id, word_size, state_size, + key_state_size, key_flow, word_denominator) - value = self.test_report['components_values'][comp_id]['value'] - - bin_list = list(format(int(value, 16), 'b').zfill( - 4 * len(value) if value[:2] != '0x' else 4 * len(value[2:]))) if '*' not in value else list( - value[2:]) - - word_list = ['*' if '*' in ''.join(bin_list[x:x + word_size]) else word_denominator if ''.join( - bin_list[x:x + word_size]).count('1') > 0 else '_' for x in - range(0, len(bin_list), word_size)] + self._print_plaintext_flow(out_list, key_flow, verbose, file, save_fig) - if ('intermediate' in comp_id or 'cipher' in comp_id) and comp_id not in key_flow: - size = (state_size, len(word_list) // state_size) - - elif ('intermediate' in comp_id or 'cipher' in comp_id) and comp_id in key_flow and comp_id != 'key': - size = (key_state_size, len(word_list) // key_state_size) - - else: - size = (1, len(word_list)) - out_format = [[] for _ in range(size[0])] - for i in range(size[0]): - for j in range(size[1]): - if word_list[j + i * size[1]] == word_denominator: - out_format[i].append(f'\033[31;4m{word_list[j + i * size[1]]}\033[0m') - else: - out_format[i].append(word_list[j + i * size[1]]) - - out_list[comp_id] = (out_format, rel_prob, abs_prob) if comp_id not in ["plaintext", "key"] else ( - out_format, 0, 0) - - for comp_id in out_list.keys(): - if comp_id not in key_flow: - if comp_id == 'plaintext' or 'key' in comp_id: - print(f'\t{comp_id}\t', file=file) - else: - if verbose: - print( - f' \t{comp_id} Input Links : {self.cipher.get_component_from_id(comp_id if comp_id[-2:] not in ["_i", "_o"] else comp_id[:-2]).input_id_links}', - file=file if save_fig else None) - else: - print(f' \t{comp_id}\t', file=file) - _print_colored_state(out_list[comp_id][0], verbose, file) - if verbose: print(' ' * len(out_list[comp_id][0][0]) + '\tlocal weight = ' + str( - out_list[comp_id][1]) + '\t' + 'total weight = ' + str( - out_list[comp_id][2]), file=file) - print('', file=file) - - print('', file=file) - print('total weight = ' + str(self.test_report['total_weight']), file=file) - show_key_flow = False - - for key_comp in key_flow: - if key_comp != 'key' and self.test_report['components_values'][key_comp]['weight'] != 0: - show_key_flow = True - break + show_key_flow = self._get_show_key_flow(key_flow, word_size, word_denominator) if show_key_flow: - print('', file=file) - print("KEY FLOW", file=file) - print('', file=file) - - for comp_id in key_flow: - if comp_id not in ['plaintext', 'key', 'cipher_output', - 'intermediate_output'] and 'key' not in comp_id and 'intermediate_output' not in comp_id and comp_id[ - -2:] not in [ - '_i', '_o']: - identifier = '_'.join(comp_id.split('_')[:-2]) - elif 'intermediate_output' in comp_id: - identifier = 'intermediate_output' + comp_id[-2:] - elif comp_id[-2:] == '_i' and show_input: - identifier = comp_id.split('_')[0] + '_i' - elif comp_id[-2:] == '_o' and show_output: - identifier = comp_id.split('_')[0] + '_o' - else: - identifier = comp_id - if show_components[identifier]: - if comp_id == 'plaintext' or 'key' in comp_id: - print(f'\t{comp_id}\t', file=file) - else: - if verbose: - print( - f' \t{comp_id} Input Links : {self.cipher.get_component_from_id(comp_id if comp_id[-2:] not in ["_i", "_o"] else comp_id[:-2]).input_id_links}', - file=file) - else: - print(f' \t{comp_id}\t', file=file) - _print_colored_state(out_list[comp_id][0], verbose, file) - if verbose: print(' ' * len(out_list[comp_id][0][0]) + '\tlocal weight = ' + str( - out_list[comp_id][1]) + '\t' + 'total weight = ' + str( - out_list[comp_id][2]), file=file) - print('', file=file) + self._print_key_flow(key_flow, show_components, out_list, verbose, file) def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_input=None, fixed_output=None, fixed_input_difference=None, test_name=None): @@ -624,19 +686,19 @@ def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_i if 'dieharder' in self.test_name: for dict in self.test_report['test_results']: DieharderTests._generate_chart_round(dict, - output_directory + '/' + self.cipher.id + '/' + self.test_name, + output_directory, show_graph=True) DieharderTests._generate_chart_all(self.test_report['test_results'], - output_directory + '/' + self.cipher.id + '/' + self.test_name, + output_directory, show_graph=True) elif 'nist' in self.test_name: for dict in self.test_report['test_results']: NISTStatisticalTests._generate_chart_round(dict, - output_directory + '/' + self.cipher.id + '/' + self.test_name, + output_directory, show_graph=True) NISTStatisticalTests._generate_chart_all(self.test_report['test_results'], - output_directory + '/' + self.cipher.id + '/' + self.test_name, + output_directory, show_graph=True) elif 'algebraic' in self.test_name: @@ -652,11 +714,10 @@ def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_i fig.update_traces(text=z_text, texttemplate="%{text}") fig.update(layout_coloraxis_showscale=False) fig.update_xaxes(side="top") - print(output_directory) if show_graph == False: print('saving image') - fig.write_image(output_directory + '/' + self.cipher.id + '/' + self.test_name + '/test_results.png') + fig.write_image(output_directory + '/test_results.png') print('image saved') if show_graph: fig.show(renderer='png') @@ -666,24 +727,24 @@ def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_i inputs = list(self.test_report['test_results'].keys()) for it in inputs if fixed_input == None else [fixed_input]: if not os.path.exists( - output_directory + '/' + self.cipher.id + '/' + self.test_name + '/' + it) and show_graph == False: - os.mkdir(output_directory + '/' + self.cipher.id + '/' + self.test_name + '/' + it) + output_directory + '/' + it) and show_graph == False: + os.mkdir(output_directory + '/' + it) outputs = list(self.test_report['test_results'][it].keys()) for out in outputs if fixed_input == None else [fixed_output]: if out == 'cipher_output': continue if not os.path.exists( - output_directory + '/' + self.cipher.id + '/' + self.test_name + '/' + it + '/' + out) and show_graph == False: + output_directory + '/' + it + '/' + out) and show_graph == False: os.mkdir( - output_directory + '/' + self.cipher.id + '/' + self.test_name + '/' + it + '/' + out) + output_directory + '/' + it + '/' + out) results = list(self.test_report['test_results'][it][out].keys()) for res in results if test_name == None else [test_name]: - if not os.path.exists(output_directory + '/' + - self.cipher.id + '/' + self.test_name + '/' + it + '/' + out + '/' + res) and show_graph == False: + if not os.path.exists( + output_directory + '/' + it + '/' + out + '/' + res) and show_graph == False: os.mkdir( - output_directory + '/' + self.cipher.id + '/' + self.test_name + '/' + it + '/' + out + '/' + res) + output_directory + '/' + it + '/' + out + '/' + res) ### Make Graphs @@ -742,8 +803,7 @@ def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_i }) if show_graph == False: - fig.write_image(output_directory + '/' + - self.cipher.id + '/' + self.test_name + '/' + it + '/' + out + '/' + res + '/' + str( + fig.write_image(output_directory + '/' + it + '/' + out + '/' + res + '/' + str( res) + '_' + str(case['input_difference_value']) + '.png', scale=4) else: fig.show(renderer='png') @@ -759,8 +819,7 @@ def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_i showlegend=False) if show_graph == False: - fig.write_image(output_directory + '/' + - self.cipher.id + '/' + self.test_name + '/' + it + '/' + out + '/' + res + + fig.write_image(output_directory + '/' + it + '/' + out + '/' + res + '/' + str(res) + '.png', scale=4) else: @@ -769,8 +828,9 @@ def _produce_graph(self, output_directory=os.getcwd(), show_graph=False, fixed_i fig.data = [] fig.layout = {} - def save_as_image(self, test_name=None, fixed_input=None, fixed_output=None, - fixed_input_difference=None, word_size=1, state_size=1, key_state_size=1, output_directory=os.getcwd() + '/test_reports', + def save_as_image(self, show_as_hex=False, test_name=None, fixed_input=None, fixed_output=None, + fixed_input_difference=None, word_size=1, state_size=1, key_state_size=1, + output_directory=os.getcwd() + '/test_reports', verbose=False, show_word_permutation=False, show_var_shift=False, show_var_rotate=False, show_theta_xoodoo=False, show_theta_keccak=False, show_shift_rows=False, show_sigma=False, show_reverse=False, @@ -804,12 +864,13 @@ def save_as_image(self, test_name=None, fixed_input=None, fixed_output=None, sage: report.save_as_image() """ - + time = '_date:' + 'time:'.join(str(datetime.now()).split(' ')) + test_directory = output_directory if 'component_analysis' in self.test_name: print('This method is not implemented yet for the component analysis test') return elif 'trail' in self.test_name: - self._print_trail(word_size, state_size, key_state_size, verbose, show_word_permutation, + self._print_trail(show_as_hex, word_size, state_size, key_state_size, verbose, show_word_permutation, show_var_shift, show_var_rotate, show_theta_xoodoo, show_theta_keccak, show_shift_rows, show_sigma, show_reverse, show_permuation, show_multi_input_non_linear_logical_operator, @@ -823,13 +884,16 @@ def save_as_image(self, test_name=None, fixed_input=None, fixed_output=None, if not os.path.exists(output_directory): os.mkdir(output_directory) - if not os.path.exists(output_directory + '/' + self.cipher.id): - os.mkdir(output_directory + '/' + self.cipher.id) + if not os.path.exists(output_directory + '/' + self.cipher.id + time): + os.mkdir(output_directory + '/' + self.cipher.id + time) + + if not os.path.exists(output_directory + '/' + self.cipher.id + time + '/' + self.test_name): + os.mkdir(output_directory + '/' + self.cipher.id + time + '/' + self.test_name) - if not os.path.exists(output_directory + '/' + self.cipher.id + '/' + self.test_name): - os.mkdir(output_directory + '/' + self.cipher.id + '/' + self.test_name) - self._produce_graph(output_directory, test_name=test_name, fixed_output=fixed_output, fixed_input_difference=fixed_input_difference, fixed_input=fixed_input) - print('Report saved in ' + output_directory) + test_directory = output_directory + '/' + self.cipher.id + time + '/' + self.test_name + self._produce_graph(output_directory=test_directory, test_name=test_name, fixed_output=fixed_output, + fixed_input_difference=fixed_input_difference, fixed_input=fixed_input) + print('Report saved in ' + test_directory) def clean_reports(self, output_dir=os.getcwd() + '/test_reports'): diff --git a/claasp/cipher_modules/statistical_tests/dieharder_statistical_tests.py b/claasp/cipher_modules/statistical_tests/dieharder_statistical_tests.py index 433fe010..a55ed398 100644 --- a/claasp/cipher_modules/statistical_tests/dieharder_statistical_tests.py +++ b/claasp/cipher_modules/statistical_tests/dieharder_statistical_tests.py @@ -19,7 +19,7 @@ import os import math import time -from datetime import timedelta +from datetime import timedelta, datetime import matplotlib.pyplot as plt from claasp.cipher_modules.statistical_tests.dataset_generator import DatasetGenerator, DatasetType @@ -455,8 +455,9 @@ def _generate_chart_all(report_dict_list, output_dir='', show_graph=False): print(f'Drawing chart for all rounds is in finished.') def _create_report_folder(self): + time_date = 'date:'+'time:'.join(str(datetime.now()).split(' ')) self.report_folder = os.path.join(self.folder_prefix, - f'{self._cipher_primitive}_{self.dataset_type_dieharder.name}_index{self.input_index}_{self.number_of_sequences_dieharder}lines_{self.bits_in_one_sequence_dieharder}bits') + f'{self._cipher_primitive}_{self.dataset_type_dieharder.name}_index{self.input_index}_{self.number_of_sequences_dieharder}lines_{self.bits_in_one_sequence_dieharder}bits_{time_date}time') try: os.makedirs(self.report_folder) except OSError: @@ -471,7 +472,7 @@ def _write_execution_time(self, execution_description, execution_time): print(f'Error: {e.strerror}') def _generate_dieharder_dicts(self, dataset, round_start, round_end, dieharder_test_option, FLAG_CHART=False): - dataset_folder = os.getcwd() + '/dataset' + dataset_folder = os.getcwd() + 'dieharder_dataset' dataset_filename = 'dieharder_input_' + self._cipher_primitive dataset_filename = os.path.join(dataset_folder, dataset_filename) dieharder_report_dicts = [] diff --git a/claasp/cipher_modules/statistical_tests/nist_statistical_tests.py b/claasp/cipher_modules/statistical_tests/nist_statistical_tests.py index 9b6d42e5..1b2abceb 100644 --- a/claasp/cipher_modules/statistical_tests/nist_statistical_tests.py +++ b/claasp/cipher_modules/statistical_tests/nist_statistical_tests.py @@ -21,12 +21,11 @@ import time import shutil import pathlib -from datetime import timedelta +from datetime import timedelta, datetime import matplotlib.pyplot as plt from claasp.cipher_modules.statistical_tests.dataset_generator import DatasetGenerator, DatasetType -reports_path = "test_reports/statistical_tests/nist_statistics_report" TEST_ID_TABLE = { @@ -95,6 +94,7 @@ def nist_statistical_tests(self, test_type, """ + time_date = 'date:'+'time:'.join(str(datetime.now()).split(' ')) nist_test = { 'input_parameters': { @@ -132,7 +132,7 @@ def nist_statistical_tests(self, test_type, self.number_of_samples = self.number_of_samples_in_one_sequence * (self.number_of_sequences + 1) self.bits_in_one_sequence = sample_size * self.number_of_samples_in_one_sequence - self._create_report_folder(statistical_test_option_list) + self._create_report_folder(time_date,statistical_test_option_list) dataset = self.data_generator.generate_avalanche_dataset(input_index=self.input_index, number_of_samples=self.number_of_samples) @@ -151,7 +151,7 @@ def nist_statistical_tests(self, test_type, self.number_of_sequences = number_of_sequences self.number_of_samples = self.number_of_sequences + 1 self.bits_in_one_sequence = number_of_blocks_in_one_sample * self.cipher.output_bit_size - self._create_report_folder(statistical_test_option_list) + self._create_report_folder(time_date,statistical_test_option_list) dataset = self.data_generator.generate_correlation_dataset(input_index=self.input_index, number_of_samples=self.number_of_samples, @@ -170,7 +170,7 @@ def nist_statistical_tests(self, test_type, self.number_of_sequences = number_of_sequences self.number_of_samples = self.number_of_sequences + 1 self.bits_in_one_sequence = number_of_blocks_in_one_sample * self.cipher.output_bit_size - self._create_report_folder(statistical_test_option_list) + self._create_report_folder(time_date,statistical_test_option_list) dataset = self.data_generator.generate_cbc_dataset(input_index=self.input_index, number_of_samples=self.number_of_samples, @@ -188,7 +188,7 @@ def nist_statistical_tests(self, test_type, self.number_of_sequences = number_of_sequences self.number_of_samples = self.number_of_sequences + 1 self.bits_in_one_sequence = self.number_of_blocks_in_one_sample * self.cipher.output_bit_size - self._create_report_folder(statistical_test_option_list) + self._create_report_folder(time_date,statistical_test_option_list) dataset = self.data_generator.generate_random_dataset(input_index=self.input_index, number_of_samples=self.number_of_samples, @@ -209,7 +209,7 @@ def nist_statistical_tests(self, test_type, ratio = min(1, (number_of_blocks_in_one_sample - 1 - n) / math.comb(n, 2)) self.number_of_blocks_in_one_sample = int(1 + n + math.ceil(math.comb(n, 2) * ratio)) self.bits_in_one_sequence = self.number_of_blocks_in_one_sample * self.cipher.output_bit_size - self._create_report_folder(statistical_test_option_list) + self._create_report_folder(time_date,statistical_test_option_list) dataset = self.data_generator.generate_low_density_dataset(input_index=self.input_index, number_of_samples=self.number_of_samples, @@ -229,7 +229,7 @@ def nist_statistical_tests(self, test_type, ratio = min(1, (number_of_blocks_in_one_sample - 1 - n) / math.comb(n, 2)) self.number_of_blocks_in_one_sample = int(1 + n + math.ceil(math.comb(n, 2) * ratio)) self.bits_in_one_sequence = self.number_of_blocks_in_one_sample * self.cipher.output_bit_size - self._create_report_folder(statistical_test_option_list) + self._create_report_folder(time_date,statistical_test_option_list) dataset = self.data_generator.generate_high_density_dataset(input_index=self.input_index, number_of_samples=self.number_of_samples, @@ -244,7 +244,7 @@ def nist_statistical_tests(self, test_type, if not dataset: return self._write_execution_time(f'Compute {self.dataset_type.value}', dataset_generate_time) - nist_test['test_results'] = self._generate_nist_dicts(dataset=dataset, round_start=round_start, + nist_test['test_results'] = self._generate_nist_dicts(time_date=time_date, dataset=dataset, round_start=round_start, round_end=round_end, statistical_test_option_list=statistical_test_option_list) nist_test['input_parameters']['bits_in_one_sequence'] = bits_in_one_sequence @@ -551,9 +551,9 @@ def _generate_chart_all(report_dict_list, report_folder="", show_graph=False): plt.close() print(f'Drawing chart for all rounds is in finished.') - def _create_report_folder(self,statistical_test_option_list): + def _create_report_folder(self,time_date,statistical_test_option_list): self.report_folder = os.path.join(self.folder_prefix, - f'{self._cipher_primitive}_{self.dataset_type.name}_index{self.input_index}_{self.number_of_sequences}lines_{self.bits_in_one_sequence}bits_{statistical_test_option_list}test_option_list') + f'{self._cipher_primitive}_{self.dataset_type.name}_index{self.input_index}_{self.number_of_sequences}lines_{self.bits_in_one_sequence}bits_{statistical_test_option_list}test_option_list_{time_date}time') try: os.makedirs(self.report_folder) except OSError: @@ -567,7 +567,7 @@ def _write_execution_time(self, execution_description, execution_time): except Exception as e: print(f'Error: {e.strerror}') - def _generate_nist_dicts(self, dataset, round_start, round_end, statistical_test_option_list='1' + 14 * '0'): + def _generate_nist_dicts(self,time_date, dataset, round_start, round_end, statistical_test_option_list='1' + 14 * '0'): # seems that the statistical tools cannot change the default folder 'experiments' nist_local_experiment_folder = f"/usr/local/bin/sts-2.1.2/experiments/" dataset_folder = 'dataset' @@ -591,7 +591,7 @@ def _generate_nist_dicts(self, dataset, round_start, round_end, statistical_test print(f'Error: {e.strerror}') return - report_folder_round = os.path.abspath(os.path.join(self.report_folder, f'round_{round_number}')) + report_folder_round = os.path.abspath(os.path.join(self.report_folder, f'round_{round_number}_{time_date}time')) dataset[round_number].tofile(dataset_filename) sts_execution_time = time.time() diff --git a/claasp/components/or_component.py b/claasp/components/or_component.py index b4d961c2..7a962103 100644 --- a/claasp/components/or_component.py +++ b/claasp/components/or_component.py @@ -1,4 +1,3 @@ - # **************************************************************************** # Copyright 2023 Technology Innovation Institute # @@ -44,14 +43,43 @@ def algebraic_polynomials(self, model): sage: or_component = gift.get_component_from_id("or_0_4") sage: algebraic = AlgebraicModel(gift) sage: or_component.algebraic_polynomials(algebraic) - [or_0_4_y0 + 1, - or_0_4_y1 + 1, - ... - or_0_4_y30 + 1, - or_0_4_y31 + 1] + [or_0_4_x0*or_0_4_x32 + or_0_4_y0 + or_0_4_x32 + or_0_4_x0, + or_0_4_x1*or_0_4_x33 + or_0_4_y1 + or_0_4_x33 + or_0_4_x1, + or_0_4_x2*or_0_4_x34 + or_0_4_y2 + or_0_4_x34 + or_0_4_x2, + or_0_4_x3*or_0_4_x35 + or_0_4_y3 + or_0_4_x35 + or_0_4_x3, + or_0_4_x4*or_0_4_x36 + or_0_4_y4 + or_0_4_x36 + or_0_4_x4, + or_0_4_x5*or_0_4_x37 + or_0_4_y5 + or_0_4_x37 + or_0_4_x5, + or_0_4_x6*or_0_4_x38 + or_0_4_y6 + or_0_4_x38 + or_0_4_x6, + or_0_4_x7*or_0_4_x39 + or_0_4_y7 + or_0_4_x39 + or_0_4_x7, + or_0_4_x8*or_0_4_x40 + or_0_4_y8 + or_0_4_x40 + or_0_4_x8, + or_0_4_x9*or_0_4_x41 + or_0_4_y9 + or_0_4_x41 + or_0_4_x9, + or_0_4_x10*or_0_4_x42 + or_0_4_y10 + or_0_4_x42 + or_0_4_x10, + or_0_4_x11*or_0_4_x43 + or_0_4_y11 + or_0_4_x43 + or_0_4_x11, + or_0_4_x12*or_0_4_x44 + or_0_4_y12 + or_0_4_x44 + or_0_4_x12, + or_0_4_x13*or_0_4_x45 + or_0_4_y13 + or_0_4_x45 + or_0_4_x13, + or_0_4_x14*or_0_4_x46 + or_0_4_y14 + or_0_4_x46 + or_0_4_x14, + or_0_4_x15*or_0_4_x47 + or_0_4_y15 + or_0_4_x47 + or_0_4_x15, + or_0_4_x16*or_0_4_x48 + or_0_4_y16 + or_0_4_x48 + or_0_4_x16, + or_0_4_x17*or_0_4_x49 + or_0_4_y17 + or_0_4_x49 + or_0_4_x17, + or_0_4_x18*or_0_4_x50 + or_0_4_y18 + or_0_4_x50 + or_0_4_x18, + or_0_4_x19*or_0_4_x51 + or_0_4_y19 + or_0_4_x51 + or_0_4_x19, + or_0_4_x20*or_0_4_x52 + or_0_4_y20 + or_0_4_x52 + or_0_4_x20, + or_0_4_x21*or_0_4_x53 + or_0_4_y21 + or_0_4_x53 + or_0_4_x21, + or_0_4_x22*or_0_4_x54 + or_0_4_y22 + or_0_4_x54 + or_0_4_x22, + or_0_4_x23*or_0_4_x55 + or_0_4_y23 + or_0_4_x55 + or_0_4_x23, + or_0_4_x24*or_0_4_x56 + or_0_4_y24 + or_0_4_x56 + or_0_4_x24, + or_0_4_x25*or_0_4_x57 + or_0_4_y25 + or_0_4_x57 + or_0_4_x25, + or_0_4_x26*or_0_4_x58 + or_0_4_y26 + or_0_4_x58 + or_0_4_x26, + or_0_4_x27*or_0_4_x59 + or_0_4_y27 + or_0_4_x59 + or_0_4_x27, + or_0_4_x28*or_0_4_x60 + or_0_4_y28 + or_0_4_x60 + or_0_4_x28, + or_0_4_x29*or_0_4_x61 + or_0_4_y29 + or_0_4_x61 + or_0_4_x29, + or_0_4_x30*or_0_4_x62 + or_0_4_y30 + or_0_4_x62 + or_0_4_x30, + or_0_4_x31*or_0_4_x63 + or_0_4_y31 + or_0_4_x63 + or_0_4_x31] + """ ninputs = self.input_bit_size noutputs = self.output_bit_size + ors_number = self.description[1] - 1 word_size = noutputs ring_R = model.ring() input_vars = [self.id + "_" + model.input_postfix + str(i) for i in range(ninputs)] @@ -61,10 +89,11 @@ def algebraic_polynomials(self, model): def or_polynomial(x0, x1): return x0 * x1 + x0 + x1 - x = [ring_R.one() for _ in range(noutputs)] - for word_vars in words_vars: + x = [words_vars[0][_] for _ in range(noutputs)] + for or_itr in range(ors_number): for i in range(noutputs): - x[i] = or_polynomial(x[i], word_vars[i]) + x[i] = or_polynomial(x[i], words_vars[or_itr + 1][i]) + y = list(map(ring_R, output_vars)) polynomials = [y[i] + x[i] for i in range(noutputs)] diff --git a/docker/Dockerfile b/docker/Dockerfile index b35ad1bd..0658479a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -303,6 +303,8 @@ RUN sage -pip install plotly -U kaleido COPY required_dependencies/sage_numerical_backends_gurobi-9.3.1.tar.gz /opt/ RUN cd /opt/ && sage -pip install sage_numerical_backends_gurobi-9.3.1.tar.gz +RUN apt-get install -y coinor-cbc coinor-libcbc-dev +RUN sage -python -m pip install sage-numerical-backends-coin==9.0b12 WORKDIR /home/sage/tii-claasp diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 228b1d84..841c757c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,66 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - 2024-03-22 + +### Added + +- Create new speedy cipher. +- Create the a5/2 stream cipher. +- Create a test function for nist statistical tests and dieharder statistical tests that produces a parsed result, standardized for the report class. +- Gaston permutation with sbox component. +- Create qarmav2 with mixcolumn component. +- Support inversion of tweakable primitives and inversion of mixcolumn operation with a non irreducible polynomial. +- Adding bct mzn model for arx ciphers. +- Add option to start chacha permutation from a bottom lower round. +- Adding minizinc boomerang model. + +### Changed + +- Location of files related to milp inequalities for non linear components or large xors moved to userspace. +- Location of files related to milp external solvers change to current working directory. +- Continuous_tests to class. + +### Fixed + +- Add timestamp to milp external files. +- File path in cp module changed to absolute path. +- Consider whole solution when searching xor linear trails. +- Refactoring of algebraic tests to an object. +- Create claasp base image for test. +- Fix sonarcloud github action so forks can be analyzed on pr. + +## [2.2.0] - 2024-03-07 + +### Added + +- Create new Speedy cipher +- Create the A5/2 stream cipher +- Create the grain128 stream cipher +- Create a test function for nist statistical tests and dieharder statistical tests that produces a parsed result, standardized for the Report class +- SAT wordwise deterministic truncated XOR differential trail model +- Gaston permutation with sbox component +- Create Qarmav2 with MixColumn component +- Support inversion of tweakable primitives and inversion of MixColumn operation with a non irreducible polynomial +- Add option to start Chacha permutation from a bottom lower round +- Create bitwise impossible XOR differential trail search for SAT + +### Changed + +- Location of files related to MILP inequalities for non linear components or large xors moved to userspace +- Location of files related to MILP external solvers change to current working directory +- Continuous_tests to class + +### Fixed + +- Add timestamp to MILP external files +- File path in CP module changed to absolute path +- Consider whole solution when searching XOR linear trails +- Refactored algebraic tests to an object and added some tests +- Refactoring of algebraic tests to an object +- Create CLAASP base image for test +- Fix SonarCloud GitHub Action so Forks can be analyzed on PR + ## [2.1.0] - 2024-01-30 ### Added @@ -89,6 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Everything! First public release. +[2.3.0]: https://github.com/Crypto-TII/claasp/compare/v2.3.0..v2.1.0 [2.1.0]: https://github.com/Crypto-TII/claasp/compare/v2.1.0..v2.0.0 [1.1.0]: https://github.com/Crypto-TII/claasp/releases/tag/v1.1.0 [1.0.0]: https://github.com/Crypto-TII/claasp/releases/tag/v1.0.0 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2cc1bf73..cc4ef604 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -41,12 +41,13 @@ To contribute to this project, please, follow the following conventions. # GitHub collaboration -In order to collaborate with the project, you need to fork this projects. +In order to collaborate with the project, you need to fill this [Google form](https://forms.gle/rYMKW76fCF15Lnxm6) to +be added as a collaboratior in [CLAASP GitHub repository](https://github.com/Crypto-TII/claasp). ## Pull requests -Pull requests are the way to contribute to the project. Pull requests coming from forks will be reviewed and need to -have all the checks passing green. +Pull requests are the way to contribute to the project. Only collaborators can create pull requests, so pull requests +coming from forks will be rejected. # Development environment diff --git a/tests/unit/cipher_modules/models/algebraic/algebraic_model_test.py b/tests/unit/cipher_modules/models/algebraic/algebraic_model_test.py index d6d8399e..42502e92 100644 --- a/tests/unit/cipher_modules/models/algebraic/algebraic_model_test.py +++ b/tests/unit/cipher_modules/models/algebraic/algebraic_model_test.py @@ -28,13 +28,13 @@ def test_nvars(): def test_polynomial_system(): fancy = FancyBlockCipher(number_of_rounds=1) assert str(AlgebraicModel(fancy).polynomial_system()) == \ - 'Polynomial Sequence with 468 Polynomials in 384 Variables' + 'Polynomial Sequence with 228 Polynomials in 144 Variables' def test_polynomial_system_at_round(): fancy = FancyBlockCipher(number_of_rounds=1) assert str(AlgebraicModel(fancy).polynomial_system_at_round(0)) == \ - 'Polynomial Sequence with 252 Polynomials in 288 Variables' + 'Polynomial Sequence with 228 Polynomials in 144 Variables' def test_ring(): diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model_test.py index 27d35a78..8d205674 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_bitwise_impossible_xor_differential_model_test.py @@ -128,7 +128,7 @@ def test_find_one_bitwise_impossible_xor_differential_trail_model_with_external_ key = set_fixed_variables(component_id='key', constraint_type='equal', bit_positions=range(64), bit_values=[0] * 64) ciphertext = set_fixed_variables(component_id='cipher_output_10_13', constraint_type='equal', bit_positions=range(32), bit_values=[0] * 6 + [2, 0, 2] + [0] * 23) - trail = milp.find_one_bitwise_impossible_xor_differential_trail(6, fixed_values=[plaintext, key, ciphertext], external_solver_name='glpk') + trail = milp.find_one_bitwise_impossible_xor_differential_trail(6, fixed_values=[plaintext, key, ciphertext], external_solver_name='glpk_ext') assert trail['status'] == 'SATISFIABLE' assert trail['components_values']['intermediate_output_5_12']['value'] == '????????????????0??????1??????0?' assert trail['components_values']['intermediate_output_5_12_backward']['value'] == SIMON_INCOMPATIBLE_ROUND_OUTPUT @@ -145,7 +145,7 @@ def test_find_one_bitwise_impossible_xor_differential_trail_with_fully_automatic ciphertext_backward = set_fixed_variables(component_id='cipher_output_10_13_backward', constraint_type='equal', bit_positions=range(32), bit_values=[0] * 6 + [2, 0, 2] + [0] * 23) trail = milp.find_one_bitwise_impossible_xor_differential_trail_with_fully_automatic_model( - fixed_values=[plaintext, key, key_backward, ciphertext_backward], external_solver_name='glpk') + fixed_values=[plaintext, key, key_backward, ciphertext_backward], external_solver_name='glpk_ext') assert trail['status'] == 'SATISFIABLE' assert trail['components_values']['plaintext']['value'] == '00000000000000000000000000000001' assert trail['components_values']['intermediate_output_5_12_backward']['value'] == SIMON_INCOMPATIBLE_ROUND_OUTPUT diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model_test.py index 8ba9cce4..3f0b8cd8 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_wordwise_impossible_xor_differential_model_test.py @@ -93,7 +93,7 @@ def test_find_one_wordwise_impossible_xor_differential_trail_model_with_external ciphertext = set_fixed_variables(component_id='cipher_output_1_32', constraint_type='equal', bit_positions=range(16), bit_values=[1] + [0]*15) trail = milp.find_one_wordwise_impossible_xor_differential_trail(1, fixed_bits=[key], - fixed_words=[plaintext, ciphertext], external_solver_name='glpk') + fixed_words=[plaintext, ciphertext], external_solver_name='glpk_ext') assert trail['status'] == 'SATISFIABLE' assert trail['components_values']['plaintext']['value'] == '1003000000000000' assert trail['components_values']['key']['value'] == '0000000000000000' @@ -113,7 +113,7 @@ def test_find_one_wordwise_impossible_xor_differential_trail_with_fully_automati ciphertext_backward = set_fixed_variables(component_id='cipher_output_1_32_backward', constraint_type='equal', bit_positions=range(16), bit_values=[1] + [0]*15) trail = milp.find_one_wordwise_impossible_xor_differential_trail_with_fully_automatic_model(fixed_bits=[key, key_backward], - fixed_words=[plaintext, ciphertext_backward], external_solver_name='glpk') + fixed_words=[plaintext, ciphertext_backward], external_solver_name='glpk_ext') assert trail['status'] == 'SATISFIABLE' assert trail['components_values']['plaintext']['value'] == '1003000000000000' assert trail['components_values']['key']['value'] == '0000000000000000' diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py index 9b4875cb..a37799b7 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py @@ -114,7 +114,7 @@ def test_find_one_xor_linear_trail_with_fixed_weight(): def test_find_one_xor_linear_trail_with_fixed_weight_with_external_solver(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="glpk") + trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="glpk_ext") assert len(trail) == 10 assert trail["total_weight"] == 1.0 @@ -123,14 +123,14 @@ def test_find_one_xor_linear_trail_with_fixed_weight_with_supported_but_not_inst with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="cplex") + trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="cplex_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_installed_external_solver_but_missing_license(): with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="Gurobi") + trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="Gurobi_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_unsupported_external_solver(): with pytest.raises(Exception) as e_info: diff --git a/tests/unit/cipher_modules/report_test.py b/tests/unit/cipher_modules/report_test.py index f2bb9060..18c1b067 100644 --- a/tests/unit/cipher_modules/report_test.py +++ b/tests/unit/cipher_modules/report_test.py @@ -6,6 +6,7 @@ from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher from claasp.ciphers.block_ciphers.simon_block_cipher import SimonBlockCipher from claasp.cipher_modules.report import Report +from claasp.ciphers.block_ciphers.present_block_cipher import PresentBlockCipher from claasp.cipher_modules.statistical_tests.dieharder_statistical_tests import DieharderTests from claasp.cipher_modules.statistical_tests.nist_statistical_tests import NISTStatisticalTests from claasp.cipher_modules.neural_network_tests import NeuralNetworkTests @@ -74,8 +75,8 @@ def test_save_as_latex_table(): trail_report = Report(trail) trail_report.save_as_latex_table() - nist = NISTStatisticalTests(simon) - report_sts = Report(nist.nist_statistical_tests('avalanche')) + dieharder=DieharderTests(simon) + report_sts = Report(dieharder.dieharder_statistical_tests('avalanche', dieharder_test_option=100)) report_sts.save_as_latex_table() def test_save_as_DataFrame(): @@ -100,8 +101,8 @@ def test_save_as_DataFrame(): trail_report = Report(trail) trail_report.save_as_DataFrame() - nist = NISTStatisticalTests(speck) - report_sts = Report(nist.nist_statistical_tests('avalanche')) + dieharder = DieharderTests(speck) + report_sts = Report(dieharder.dieharder_statistical_tests('avalanche', dieharder_test_option=100)) report_sts.save_as_DataFrame() @@ -112,24 +113,23 @@ def test_save_as_json(): simon).neural_network_blackbox_distinguisher_tests() blackbox_report = Report(neural_network_blackbox_distinguisher_tests_results) blackbox_report.save_as_json(fixed_input='plaintext',fixed_output='round_output') - nist = NISTStatisticalTests(simon) - report_sts = Report(nist.nist_statistical_tests('avalanche')) + dieharder = DieharderTests(simon) + report_sts = Report(dieharder.dieharder_statistical_tests('avalanche', dieharder_test_option=100)) report_sts.save_as_json() - milp = MilpXorDifferentialModel(simon) - plaintext = set_fixed_variables( - component_id='plaintext', - constraint_type='not_equal', - bit_positions=range(32), - bit_values=(0,) * 32) - key = set_fixed_variables( - component_id='key', - constraint_type='equal', - bit_positions=range(64), - bit_values=(0,) * 64) - trail = milp.find_lowest_weight_xor_differential_trail(fixed_values=[plaintext, key]) + present = PresentBlockCipher(number_of_rounds=2) + sat = SatXorDifferentialModel(present) + related_key_setting = [ + set_fixed_variables(component_id='key', constraint_type='not_equal', bit_positions=list(range(80)), + bit_values=[0] * 80), + set_fixed_variables(component_id='plaintext', constraint_type='equal', bit_positions=list(range(64)), + bit_values=[0] * 64) + ] + trail = sat.find_one_xor_differential_trail_with_fixed_weight(fixed_weight=16, fixed_values=related_key_setting, + solver_name='kissat') trail_report = Report(trail) - trail_report.save_as_json() + trail_report.show() + avalanche_results = AvalancheTests(simon).avalanche_tests() avalanche_report = Report(avalanche_results) avalanche_report.save_as_json(fixed_input='plaintext',fixed_output='round_output',fixed_test='avalanche_weight_vectors') @@ -157,24 +157,18 @@ def test_show(): avalanche_report.show(test_name='avalanche_weight_vectors', fixed_input_difference=None) avalanche_report.show(test_name='avalanche_weight_vectors', fixed_input_difference='average') - milp = MilpXorDifferentialModel(speck) - plaintext = set_fixed_variables( - component_id='plaintext', - constraint_type='not_equal', - bit_positions=range(32), - bit_values=(0,) * 32) - key = set_fixed_variables( - component_id='key', - constraint_type='equal', - bit_positions=range(64), - bit_values=(0,) * 64) - - trail = milp.find_one_xor_differential_trail(fixed_values=[plaintext, key]) + present = PresentBlockCipher(number_of_rounds=4) + sat = SatXorDifferentialModel(present) + related_key_setting = [ + set_fixed_variables(component_id='key', constraint_type='not_equal', bit_positions=list(range(80)), + bit_values=[0] * 80)] + trail = sat.find_one_xor_differential_trail_with_fixed_weight(fixed_weight=16, fixed_values=related_key_setting, + solver_name='kissat') trail_report = Report(trail) trail_report.show() - nist = NISTStatisticalTests(speck) - report_sts = Report(nist.nist_statistical_tests('avalanche')) + dieharder = DieharderTests(speck) + report_sts = Report(dieharder.dieharder_statistical_tests('avalanche', dieharder_test_option=100)) report_sts.show() neural_network_tests = NeuralNetworkTests(speck).neural_network_differential_distinguisher_tests() diff --git a/tests/unit/cipher_test.py b/tests/unit/cipher_test.py index 06d03617..5a053ea1 100644 --- a/tests/unit/cipher_test.py +++ b/tests/unit/cipher_test.py @@ -53,33 +53,33 @@ def test_algebraic_tests(): d = AlgebraicTests(toyspn).algebraic_tests(10) assert d == { 'input_parameters': {'cipher': toyspn, 'timeout_in_seconds': 10, 'test_name': 'algebraic_tests'}, - 'test_results': {'number_of_variables': [66, 126], - 'number_of_equations': [76, 158], - 'number_of_monomials': [96, 186], + 'test_results': {'number_of_variables': [30, 48], + 'number_of_equations': [40, 80], + 'number_of_monomials': [60, 108], 'max_degree_of_equations': [2, 2], - 'test_passed': [False, True]}} + 'test_passed': [False, False]}} speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=1) d = AlgebraicTests(speck).algebraic_tests(1) assert d == {'input_parameters': {'cipher': speck, - 'timeout_in_seconds': 1, - 'test_name': 'algebraic_tests'}, - 'test_results': {'number_of_variables': [320], - 'number_of_equations': [272], - 'number_of_monomials': [365], + 'timeout_in_seconds': 1, + 'test_name': 'algebraic_tests'}, + 'test_results': {'number_of_variables': [144], + 'number_of_equations': [96], + 'number_of_monomials': [189], 'max_degree_of_equations': [2], 'test_passed': [True]}} aes = AESBlockCipher(word_size=4, state_size=2, number_of_rounds=1) d = AlgebraicTests(aes).algebraic_tests(5) compare_result = {'input_parameters': {'cipher': aes, - 'timeout_in_seconds': 5, - 'test_name': 'algebraic_tests'}, - 'test_results': {'number_of_variables': [320], - 'number_of_equations': [390], - 'number_of_monomials': [488], - 'max_degree_of_equations': [2], - 'test_passed': [False]}} + 'timeout_in_seconds': 5, + 'test_name': 'algebraic_tests'}, + 'test_results': {'number_of_variables': [128], + 'number_of_equations': [198], + 'number_of_monomials': [296], + 'max_degree_of_equations': [2], + 'test_passed': [False]}} assert d == compare_result @@ -202,8 +202,8 @@ def test_impossible_differential_search(): def test_is_algebraically_secure(): - identity = IdentityBlockCipher() - assert identity.is_algebraically_secure(120) is False + aes = AESBlockCipher(word_size=4, state_size=2, number_of_rounds = 1) + assert aes.is_algebraically_secure(20) is False def test_is_andrx(): @@ -233,12 +233,13 @@ def test_is_spn(): def test_polynomial_system(): - assert str(IdentityBlockCipher().polynomial_system()) == 'Polynomial Sequence with 128 Polynomials in 256 Variables' + tea = TeaBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=1) + assert str(tea.polynomial_system()) == 'Polynomial Sequence with 352 Polynomials in 448 Variables' def test_polynomial_system_at_round(): assert str(FancyBlockCipher(number_of_rounds=1).polynomial_system_at_round(0)) == \ - 'Polynomial Sequence with 252 Polynomials in 288 Variables' + 'Polynomial Sequence with 228 Polynomials in 144 Variables' def test_print(): diff --git a/tests/unit/components/or_component_test.py b/tests/unit/components/or_component_test.py index d10d56b1..204a3530 100644 --- a/tests/unit/components/or_component_test.py +++ b/tests/unit/components/or_component_test.py @@ -10,10 +10,10 @@ def test_algebraic_polynomials(): algebraic = AlgebraicModel(gift) algebraic_polynomials = or_component.algebraic_polynomials(algebraic) - assert str(algebraic_polynomials[0]) == "or_0_4_y0 + 1" - assert str(algebraic_polynomials[1]) == "or_0_4_y1 + 1" - assert str(algebraic_polynomials[-2]) == "or_0_4_y30 + 1" - assert str(algebraic_polynomials[-1]) == "or_0_4_y31 + 1" + assert str(algebraic_polynomials[0]) == "or_0_4_x0*or_0_4_x32 + or_0_4_y0 + or_0_4_x32 + or_0_4_x0" + assert str(algebraic_polynomials[1]) == "or_0_4_x1*or_0_4_x33 + or_0_4_y1 + or_0_4_x33 + or_0_4_x1" + assert str(algebraic_polynomials[-2]) == "or_0_4_x30*or_0_4_x62 + or_0_4_y30 + or_0_4_x62 + or_0_4_x30" + assert str(algebraic_polynomials[-1]) == "or_0_4_x31*or_0_4_x63 + or_0_4_y31 + or_0_4_x63 + or_0_4_x31" def test_cp_constraints():