├── .gitmodules ├── logger.py ├── .gitignore ├── coppersmith_common.py ├── contextclass.py ├── coppersmith_onevariable.py ├── Dockerfile ├── Dockerfile_devpkgnoinstall ├── Dockerfile_devpkginstall ├── Dockerfile_sageonly ├── coppersmith_linear.py ├── coppersmith_multivariate_heuristic.py ├── README.md ├── LICENSE ├── example.py ├── rootfind_ZZ.py ├── lll.py └── somenote_on_hackmd.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fplll"] 2 | path = fplll 3 | url = https://github.com/fplll/fplll 4 | [submodule "flatter"] 5 | path = flatter 6 | url = https://github.com/keeganryan/flatter/ 7 | -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL, basicConfig, getLogger 2 | 3 | basicConfig(encoding='utf-8') 4 | logger = getLogger(__name__) 5 | 6 | ## default setting (change it) 7 | logger.setLevel(INFO) 8 | #logger.setLevel(DEBUG) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /coppersmith_common.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | import time 4 | 5 | from contextclass import context 6 | from logger import * 7 | 8 | from lll import do_lattice_reduction, FPLLL, FPLLL_BKZ, FLATTER, NTL, NTL_BKZ 9 | 10 | # for large N, precision is problematic 11 | RRh = RealField(prec=4096) 12 | 13 | 14 | ### common function for Coppersmith method variants 15 | 16 | 17 | def shiftpoly(basepoly, baseidx, Nidx, varsidx_lst): 18 | N = basepoly.parent().characteristic() 19 | basepoly_ZZ = basepoly.change_ring(ZZ) 20 | vars_ZZ = basepoly_ZZ.parent().gens() 21 | if len(vars_ZZ) != len(varsidx_lst): 22 | raise ValueError("varsidx_lst len is invalid (on shiftpoly)") 23 | return (basepoly_ZZ ** baseidx) * (N ** Nidx) * prod([v ** i for v, i in zip(vars_ZZ, varsidx_lst)]) 24 | 25 | 26 | def monomialset(fis): 27 | m_set = set() 28 | for fi in fis: 29 | m_set = m_set.union(set(fi.monomials())) 30 | return m_set 31 | 32 | 33 | def genmatrix_from_shiftpolys(shiftpolys, bounds): 34 | m_lst = list(monomialset(shiftpolys)) 35 | vars_ZZ = shiftpolys[0].parent().gens() 36 | if len(vars_ZZ) != len(bounds): 37 | raise ValueError("bounds len is invalid (on genmatrix_from_shiftpolys)") 38 | matele = [] 39 | for sftpol in shiftpolys: 40 | sftpol_sub_bound = sftpol.subs({vars_ZZ[i]: vars_ZZ[i]*bounds[i] for i in range(len(vars_ZZ))}) 41 | matele += [sftpol_sub_bound.monomial_coefficient(m_lst[i]) for i in range(len(m_lst))] 42 | mat = matrix(ZZ, len(matele)//len(m_lst), len(m_lst), matele) 43 | return mat, m_lst 44 | 45 | 46 | def do_LLL(mat): 47 | lll, trans = do_lattice_reduction(mat, **context.lllopt) 48 | 49 | return lll, trans 50 | 51 | 52 | def filter_LLLresult_coppersmith(basepoly, beta, t, m_lst, lll, bounds): 53 | vars_ZZ = m_lst[0].parent().gens() 54 | N = basepoly.parent().characteristic() 55 | howgrave_bound = (RRh(N)**RRh(beta))**RRh(t) 56 | if len(m_lst) != lll.ncols(): 57 | raise ValueError("lll or trans result is invalid (on filter_LLLresult_coppersmith)") 58 | # use vector (not use matrix norm, but vector norm) 59 | lll_vec = lll.rows() 60 | 61 | m_lst_bound = [m_lstele.subs({vars_ZZ[i]: bounds[i] for i in range(len(vars_ZZ))}) for m_lstele in m_lst] 62 | 63 | result = [] 64 | for lll_vecele in lll_vec: 65 | if all([int(lll_vecele_ele) == 0 for lll_vecele_ele in lll_vecele]): 66 | continue 67 | lll_l1norm = lll_vecele.norm(p=1) 68 | if lll_l1norm >= howgrave_bound: 69 | continue 70 | howgrave_ratio = int(((lll_l1norm/howgrave_bound)*(10**15))*(0.1**15)) 71 | logger.debug("lll_l1norm/howgrave_bound: %s", str(howgrave_ratio) ) 72 | pol = 0 73 | for j, m_lstele_bound in enumerate(m_lst_bound): 74 | #assert int(lll_vecele[j]) % int(m_lstele_bound) == 0 75 | pol += (int(lll_vecele[j]) // int(m_lstele_bound)) * m_lst[j] 76 | result.append(pol) 77 | return result 78 | -------------------------------------------------------------------------------- /contextclass.py: -------------------------------------------------------------------------------- 1 | from rootfind_ZZ import JACOBIAN, HENSEL, TRIANGULATE, GROEBNER, LINEAR_SIMPLE, LINEAR_NEAR_BOUNDS 2 | from lll import do_lattice_reduction, FPLLL, FPLLL_BKZ, FLATTER, NTL, NTL_BKZ, WRAPPER, HEURISTIC 3 | from logger import * 4 | 5 | 6 | class ContextClass(): 7 | def __init__(self): 8 | self.__rootfindZZopt = {} 9 | self.__lllopt = {} 10 | 11 | ## TODO: custom check for lllopt 12 | @property 13 | def lllopt(self): 14 | return self.__lllopt 15 | 16 | @lllopt.setter 17 | def lllopt(self, newlllopt): 18 | self.__lllopt = newlllopt 19 | 20 | ## TODO: custom check for rootfindZZopt 21 | @property 22 | def rootfindZZopt(self): 23 | return self.__rootfindZZopt 24 | 25 | @rootfindZZopt.setter 26 | def rootfindZZopt(self, newrootfindZZopt): 27 | self.__rootfindZZopt = newrootfindZZopt 28 | 29 | 30 | context = ContextClass() 31 | 32 | 33 | # NOTE: not depend on contextclass for specific coppersmith libraries 34 | # (cause these libraries itself would be reused for other objectives) 35 | # instead, we add options here, and call functions in these libraries with context options 36 | 37 | 38 | ## lll 39 | def register_options_lll(_context): 40 | lllopt = {} 41 | 42 | ## general 43 | lllopt['algorithm'] = FPLLL # for coppersmith(if need, use FLATTER or FPLLL_BKZ) 44 | lllopt['transformation'] = False # for coppersmith (normally, True) 45 | 46 | ## BKZ(FPLLL, NTL) 47 | lllopt['blocksize'] = 10 48 | 49 | ## FPLLL 50 | lllopt['use_siegel'] = True 51 | lllopt['fplll_version'] = WRAPPER 52 | lllopt['early_reduction'] = True 53 | 54 | ## FPLLL_BKZ 55 | lllopt['bkzautoabort'] = True 56 | 57 | ## FLATTER 58 | lllopt['use_pari_kernel'] = True 59 | lllopt['use_pari_matsol'] = False 60 | 61 | ## NTL 62 | # (none) 63 | 64 | ## NTL_BKZ 65 | lllopt['prune'] = 0 66 | 67 | 68 | _context.lllopt = lllopt 69 | 70 | 71 | ## rootfind_ZZ 72 | def register_options_rootfind_ZZ(_context): 73 | rootfindZZopt = {} 74 | # NOTE: we use dict for each libraries options (instead of property) 75 | # (cause avoiding too complicating debug and leave readability) 76 | 77 | ## general 78 | rootfindZZopt['algorithms'] = (JACOBIAN, HENSEL, TRIANGULATE) 79 | rootfindZZopt['monomial_order_for_variety'] = 'degrevlex' 80 | 81 | ## JACOBIAN 82 | rootfindZZopt['maxiternum'] = 1024 83 | rootfindZZopt['select_subpollst_loopnum'] = 10 84 | rootfindZZopt['search_near_positive_bounds_only'] = False 85 | rootfindZZopt['filter_small_solution_minbound'] = 2**16 86 | 87 | ## HENSEL 88 | rootfindZZopt['smallps'] = (2, 3, 5) 89 | rootfindZZopt['maxcands'] = 800 90 | 91 | ## TRIANGULATE 92 | rootfindZZopt['lllopt_symbolic_linear'] = {'algorithm': FPLLL_BKZ} 93 | rootfindZZopt['symbolic_linear_algorithm'] = LINEAR_NEAR_BOUNDS 94 | 95 | 96 | _context.rootfindZZopt = rootfindZZopt 97 | 98 | 99 | register_options_lll(context) 100 | register_options_rootfind_ZZ(context) 101 | -------------------------------------------------------------------------------- /coppersmith_onevariable.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | import time 4 | import traceback 5 | 6 | from coppersmith_common import RRh, shiftpoly, genmatrix_from_shiftpolys, do_LLL, filter_LLLresult_coppersmith 7 | from rootfind_ZZ import rootfind_ZZ 8 | from contextclass import context 9 | from logger import * 10 | 11 | 12 | ### one variable coppersmith 13 | def coppersmith_one_var_core(basepoly, bounds, beta, t, u, delta): 14 | logger.info("trying param: beta=%f, t=%d, u=%d, delta=%d", beta, t, u, delta) 15 | basepoly_vars = basepoly.parent().gens() 16 | try: 17 | basepoly = basepoly / basepoly.monomial_coefficient(basepoly_vars[0] ** delta) 18 | except: 19 | traceback.print_exc() 20 | logger.warning(f"maybe, facing an non-invertible monomial coefficient. still continuing...") 21 | return [] 22 | 23 | shiftpolys = [] 24 | for i in range(u-1, -1, -1): 25 | # x^i * f(x)^t 26 | shiftpolys.append(shiftpoly(basepoly, t, 0, [i])) 27 | for i in range(1, t+1, 1): 28 | for j in range(delta-1, -1, -1): 29 | # x^j * f(x)^(t-i) * N^i 30 | shiftpolys.append(shiftpoly(basepoly, t-i, i, [j])) 31 | 32 | mat, m_lst = genmatrix_from_shiftpolys(shiftpolys, bounds) 33 | lll, _ = do_LLL(mat) 34 | result = filter_LLLresult_coppersmith(basepoly, beta, t, m_lst, lll, bounds) 35 | return result 36 | 37 | 38 | def coppersmith_onevariable(basepoly, bounds, beta, maxmatsize=100, maxu=8): 39 | if type(bounds) not in [list, tuple]: 40 | bounds = [bounds] 41 | 42 | N = basepoly.parent().characteristic() 43 | 44 | basepoly_vars = basepoly.parent().gens() 45 | if len(basepoly_vars) != 1: 46 | raise ValueError("not one variable poly") 47 | try: 48 | delta = basepoly.weighted_degree([1]) 49 | except: 50 | delta = basepoly.degree() 51 | 52 | log_N_X = RRh(log(bounds[0], N)) 53 | if log_N_X >= RRh(beta)**2/delta: 54 | raise ValueError("too much large bound") 55 | 56 | testimate = int(1/(((RRh(beta)**2)/delta)/log_N_X - 1))//2 57 | 58 | logger.debug("testimate: %d", testimate) 59 | t = min([maxmatsize//delta, max(testimate, 1)]) 60 | 61 | whole_st = time.time() 62 | 63 | curfoundpols = [] 64 | while True: 65 | if t*delta > maxmatsize: 66 | raise ValueError(f"maxmatsize exceeded: {t*delta}") 67 | u0 = max([int((t+1)/RRh(beta) - t*delta), 0]) 68 | for u_diff in range(0, maxu+1): 69 | u = u0 + u_diff 70 | if t*delta + u > maxmatsize: 71 | break 72 | foundpols = coppersmith_one_var_core(basepoly, bounds, beta, t, u, delta) 73 | if len(foundpols) == 0: 74 | continue 75 | 76 | curfoundpols += foundpols 77 | curfoundpols = list(set(curfoundpols)) 78 | sol = rootfind_ZZ(curfoundpols, bounds, **context.rootfindZZopt) 79 | if sol != [] and sol is not None: 80 | whole_ed = time.time() 81 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 82 | return sol 83 | elif len(curfoundpols) >= 2: 84 | whole_ed = time.time() 85 | logger.warning(f"failed. maybe, wrong pol was passed.") 86 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 87 | return [] 88 | t += 1 89 | # never reached here 90 | return None 91 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sagemath/sagemath:latest 2 | 3 | RUN sudo apt-get update && sudo apt-get install -y tzdata # avoid select timezone 4 | RUN sudo apt-get update && sudo apt-get upgrade -y 5 | 6 | RUN sudo apt-get install -y \ 7 | vim \ 8 | less \ 9 | git \ 10 | tmux \ 11 | netcat 12 | 13 | ### for building fplll, flatter 14 | RUN sudo apt-get update \ 15 | && sudo apt-get install -y \ 16 | cmake \ 17 | libtool \ 18 | fplll-tools \ 19 | libfplll-dev \ 20 | libgmp-dev \ 21 | libmpfr-dev \ 22 | libeigen3-dev \ 23 | libblas-dev \ 24 | liblapack-dev 25 | 26 | USER sage 27 | 28 | RUN sage --pip install --no-cache-dir \ 29 | pwntools \ 30 | pycryptodome \ 31 | z3-solver \ 32 | tqdm 33 | 34 | ### prepare build fplll, flatter 35 | RUN mkdir /home/sage/coppersmith 36 | COPY --chown=sage:sage *.py /home/sage/coppersmith/ 37 | COPY --chown=sage:sage fplll /home/sage/coppersmith/fplll/ 38 | COPY --chown=sage:sage flatter /home/sage/coppersmith/flatter/ 39 | 40 | ### build fplll 41 | WORKDIR /home/sage/coppersmith/fplll 42 | RUN ./autogen.sh 43 | RUN ./configure 44 | RUN make -j4 45 | 46 | ### build flatter 47 | WORKDIR /home/sage/coppersmith/flatter 48 | RUN mkdir build 49 | WORKDIR /home/sage/coppersmith/flatter/build 50 | RUN cmake .. 51 | RUN make -j4 52 | 53 | 54 | #### other lattice library download 55 | RUN mkdir /home/sage/collection_lattice_tools 56 | WORKDIR /home/sage/collection_lattice_tools 57 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/:$PYTHONPATH 58 | 59 | ### defund/coppersmith 60 | RUN git clone https://github.com/defund/coppersmith 61 | RUN ln -s /home/sage/collection_lattice_tools/coppersmith/coppersmith.sage /home/sage/collection_lattice_tools/defund_coppersmith.sage 62 | ## load("/home/sage/collection_lattice_tools/defund_coppersmith.sage") 63 | 64 | ### josephsurin/lattice-based-cryptanalysis 65 | RUN git clone https://github.com/josephsurin/lattice-based-cryptanalysis 66 | RUN sed -i "s/algorithm='msolve', //g" /home/sage/collection_lattice_tools/lattice-based-cryptanalysis/lbc_toolkit/common/systems_solvers.sage 67 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/lattice-based-cryptanalysis/:$PYTHONPATH 68 | 69 | ### jvdsn/crypto-attacks 70 | RUN git clone https://github.com/jvdsn/crypto-attacks/ 71 | RUN mv crypto-attacks crypto_attacks 72 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/crypto_attacks/:$PYTHONPATH 73 | 74 | ### rkm0959/Inequality_Solving_with_CVP 75 | RUN git clone https://github.com/rkm0959/Inequality_Solving_with_CVP 76 | RUN ln -s /home/sage/collection_lattice_tools/Inequality_Solving_with_CVP/solver.sage /home/sage/collection_lattice_tools/inequ_cvp_solve.sage 77 | ## load("/home/sage/collection_lattice_tools/inequ_cvp_solve.sage") 78 | 79 | ### nneonneo/pwn-stuff (including math/solvelinmod.py) 80 | RUN git clone https://github.com/nneonneo/pwn-stuff 81 | RUN ln -s /home/sage/collection_lattice_tools/pwn-stuff/math/solvelinmod.py /home/sage/collection_lattice_tools/solvelinmod.py 82 | 83 | ### maple3142/lll_cvp 84 | RUN git clone https://github.com/maple3142/lll_cvp 85 | RUN ln -s /home/sage/collection_lattice_tools/lll_cvp/lll_cvp.py /home/sage/collection_lattice_tools/lll_cvp.py 86 | 87 | ### TheBlupper/linineq 88 | RUN git clone https://github.com/TheBlupper/linineq 89 | RUN ln -s /home/sage/collection_lattice_tools/linineq/linineq.py /home/sage/collection_lattice_tools/linineq.py 90 | 91 | 92 | ### coppersmith package config 93 | WORKDIR /home/sage/ 94 | ENV PYTHONPATH=/home/sage/coppersmith/:$PYTHONPATH 95 | ENV COPPERSMITHFPLLLPATH=/home/sage/coppersmith/fplll/fplll/ 96 | ENV COPPERSMITHFLATTERPATH=/home/sage/coppersmith/flatter/build/bin/ 97 | 98 | 99 | ### follow cryptohack-docker (https://github.com/cryptohack/cryptohack-docker) 100 | ENV PWNLIB_NOTERM=true 101 | 102 | ENV BLUE='\033[0;34m' 103 | ENV YELLOW='\033[1;33m' 104 | ENV RED='\e[91m' 105 | ENV NOCOLOR='\033[0m' 106 | 107 | ENV BANNER=" \n\ 108 | ${BLUE}----------------------------------${NOCOLOR} \n\ 109 | ${YELLOW}CryptoHack Docker Container${NOCOLOR} \n\ 110 | \n\ 111 | ${RED}After Jupyter starts, visit http://127.0.0.1:8888${NOCOLOR} \n\ 112 | ${BLUE}----------------------------------${NOCOLOR} \n\ 113 | " 114 | 115 | #CMD ["echo -e $BANNER && sage -n jupyter --NotebookApp.token='' --no-browser --ip='0.0.0.0' --port=8888"] 116 | CMD ["/home/sage/sage/sage", "--python", "coppersmith/example.py"] 117 | -------------------------------------------------------------------------------- /Dockerfile_devpkgnoinstall: -------------------------------------------------------------------------------- 1 | FROM sagemath/sagemath:latest 2 | 3 | RUN sudo apt-get update && sudo apt-get install -y tzdata # avoid select timezone 4 | RUN sudo apt-get update && sudo apt-get upgrade -y 5 | 6 | RUN sudo apt-get install -y \ 7 | vim \ 8 | less \ 9 | git \ 10 | tmux \ 11 | netcat 12 | 13 | ### for building fplll, flatter 14 | RUN sudo apt-get update \ 15 | && sudo apt-get install -y \ 16 | cmake \ 17 | libtool \ 18 | fplll-tools \ 19 | libfplll-dev \ 20 | libgmp-dev \ 21 | libmpfr-dev \ 22 | libeigen3-dev \ 23 | libblas-dev \ 24 | liblapack-dev 25 | 26 | USER sage 27 | 28 | RUN sage --pip install --no-cache-dir \ 29 | pwntools \ 30 | pycryptodome \ 31 | z3-solver \ 32 | tqdm 33 | 34 | ### prepare build fplll, flatter 35 | RUN mkdir /home/sage/coppersmith 36 | COPY --chown=sage:sage *.py /home/sage/coppersmith/ 37 | COPY --chown=sage:sage fplll /home/sage/coppersmith/fplll/ 38 | COPY --chown=sage:sage flatter /home/sage/coppersmith/flatter/ 39 | 40 | ### build fplll 41 | WORKDIR /home/sage/coppersmith/fplll 42 | RUN ./autogen.sh 43 | RUN ./configure 44 | RUN make -j4 45 | 46 | ### build flatter 47 | WORKDIR /home/sage/coppersmith/flatter 48 | RUN mkdir build 49 | WORKDIR /home/sage/coppersmith/flatter/build 50 | RUN cmake .. 51 | RUN make -j4 52 | 53 | 54 | #### other lattice library download 55 | RUN mkdir /home/sage/collection_lattice_tools 56 | WORKDIR /home/sage/collection_lattice_tools 57 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/:$PYTHONPATH 58 | 59 | ### defund/coppersmith 60 | RUN git clone https://github.com/defund/coppersmith 61 | RUN ln -s /home/sage/collection_lattice_tools/coppersmith/coppersmith.sage /home/sage/collection_lattice_tools/defund_coppersmith.sage 62 | ## load("/home/sage/collection_lattice_tools/defund_coppersmith.sage") 63 | 64 | ### josephsurin/lattice-based-cryptanalysis 65 | RUN git clone https://github.com/josephsurin/lattice-based-cryptanalysis 66 | RUN sed -i "s/algorithm='msolve', //g" /home/sage/collection_lattice_tools/lattice-based-cryptanalysis/lbc_toolkit/common/systems_solvers.sage 67 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/lattice-based-cryptanalysis/:$PYTHONPATH 68 | 69 | ### jvdsn/crypto-attacks 70 | RUN git clone https://github.com/jvdsn/crypto-attacks/ 71 | RUN mv crypto-attacks crypto_attacks 72 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/crypto_attacks/:$PYTHONPATH 73 | 74 | ### rkm0959/Inequality_Solving_with_CVP 75 | RUN git clone https://github.com/rkm0959/Inequality_Solving_with_CVP 76 | RUN ln -s /home/sage/collection_lattice_tools/Inequality_Solving_with_CVP/solver.sage /home/sage/collection_lattice_tools/inequ_cvp_solve.sage 77 | ## load("/home/sage/collection_lattice_tools/inequ_cvp_solve.sage") 78 | 79 | ### nneonneo/pwn-stuff (including math/solvelinmod.py) 80 | RUN git clone https://github.com/nneonneo/pwn-stuff 81 | RUN ln -s /home/sage/collection_lattice_tools/pwn-stuff/math/solvelinmod.py /home/sage/collection_lattice_tools/solvelinmod.py 82 | 83 | ### maple3142/lll_cvp 84 | RUN git clone https://github.com/maple3142/lll_cvp 85 | RUN ln -s /home/sage/collection_lattice_tools/lll_cvp/lll_cvp.py /home/sage/collection_lattice_tools/lll_cvp.py 86 | 87 | ### TheBlupper/linineq 88 | RUN git clone https://github.com/TheBlupper/linineq 89 | RUN ln -s /home/sage/collection_lattice_tools/linineq/linineq.py /home/sage/collection_lattice_tools/linineq.py 90 | 91 | 92 | ### coppersmith package config 93 | WORKDIR /home/sage/ 94 | ENV PYTHONPATH=/home/sage/coppersmith/:$PYTHONPATH 95 | ENV COPPERSMITHFPLLLPATH=/home/sage/coppersmith/fplll/fplll/ 96 | ENV COPPERSMITHFLATTERPATH=/home/sage/coppersmith/flatter/build/bin/ 97 | 98 | 99 | ### follow cryptohack-docker (https://github.com/cryptohack/cryptohack-docker) 100 | ENV PWNLIB_NOTERM=true 101 | 102 | ENV BLUE='\033[0;34m' 103 | ENV YELLOW='\033[1;33m' 104 | ENV RED='\e[91m' 105 | ENV NOCOLOR='\033[0m' 106 | 107 | ENV BANNER=" \n\ 108 | ${BLUE}----------------------------------${NOCOLOR} \n\ 109 | ${YELLOW}CryptoHack Docker Container${NOCOLOR} \n\ 110 | \n\ 111 | ${RED}After Jupyter starts, visit http://127.0.0.1:8888${NOCOLOR} \n\ 112 | ${BLUE}----------------------------------${NOCOLOR} \n\ 113 | " 114 | 115 | #CMD ["echo -e $BANNER && sage -n jupyter --NotebookApp.token='' --no-browser --ip='0.0.0.0' --port=8888"] 116 | CMD ["/home/sage/sage/sage", "--python", "coppersmith/example.py"] 117 | -------------------------------------------------------------------------------- /Dockerfile_devpkginstall: -------------------------------------------------------------------------------- 1 | FROM sagemath/sagemath:latest 2 | 3 | RUN sudo apt-get update && sudo apt-get install -y tzdata # avoid select timezone 4 | RUN sudo apt-get update && sudo apt-get upgrade -y 5 | 6 | RUN sudo apt-get install -y \ 7 | vim \ 8 | less \ 9 | git \ 10 | tmux \ 11 | netcat 12 | 13 | ### for building fplll, flatter 14 | RUN sudo apt-get update \ 15 | && sudo apt-get install -y \ 16 | cmake \ 17 | libtool \ 18 | libgmp-dev \ 19 | libmpfr-dev \ 20 | libeigen3-dev \ 21 | libblas-dev \ 22 | liblapack-dev 23 | 24 | USER sage 25 | 26 | RUN sage --pip install --no-cache-dir \ 27 | pwntools \ 28 | pycryptodome \ 29 | z3-solver \ 30 | tqdm 31 | 32 | ### prepare build fplll, flatter 33 | RUN mkdir /home/sage/coppersmith 34 | COPY --chown=sage:sage *.py /home/sage/coppersmith/ 35 | COPY --chown=sage:sage flatter /home/sage/coppersmith/flatter/ 36 | 37 | ### install fplll 38 | USER root 39 | RUN apt-get install -y fplll-tools libfplll-dev 40 | USER sage 41 | 42 | ### install flatter 43 | WORKDIR /home/sage/coppersmith/flatter 44 | RUN mkdir build 45 | WORKDIR /home/sage/coppersmith/flatter/build 46 | RUN cmake .. 47 | RUN make -j4 48 | 49 | USER root 50 | RUN make install 51 | WORKDIR /home/sage/coppersmith 52 | RUN rm -rf flatter 53 | RUN apt-get remove --purge -y cmake 54 | RUN apt-get autoremove --purge -y 55 | USER sage 56 | 57 | 58 | #### other lattice library download 59 | RUN mkdir /home/sage/collection_lattice_tools 60 | WORKDIR /home/sage/collection_lattice_tools 61 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/:$PYTHONPATH 62 | 63 | ### defund/coppersmith 64 | RUN git clone https://github.com/defund/coppersmith 65 | RUN ln -s /home/sage/collection_lattice_tools/coppersmith/coppersmith.sage /home/sage/collection_lattice_tools/defund_coppersmith.sage 66 | ## load("/home/sage/collection_lattice_tools/defund_coppersmith.sage") 67 | 68 | ### josephsurin/lattice-based-cryptanalysis 69 | RUN git clone https://github.com/josephsurin/lattice-based-cryptanalysis 70 | RUN sed -i "s/algorithm='msolve', //g" /home/sage/collection_lattice_tools/lattice-based-cryptanalysis/lbc_toolkit/common/systems_solvers.sage 71 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/lattice-based-cryptanalysis/:$PYTHONPATH 72 | 73 | ### jvdsn/crypto-attacks 74 | RUN git clone https://github.com/jvdsn/crypto-attacks/ 75 | RUN mv crypto-attacks crypto_attacks 76 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/crypto_attacks/:$PYTHONPATH 77 | 78 | ### rkm0959/Inequality_Solving_with_CVP 79 | RUN git clone https://github.com/rkm0959/Inequality_Solving_with_CVP 80 | RUN ln -s /home/sage/collection_lattice_tools/Inequality_Solving_with_CVP/solver.sage /home/sage/collection_lattice_tools/inequ_cvp_solve.sage 81 | ## load("/home/sage/collection_lattice_tools/inequ_cvp_solve.sage") 82 | 83 | ### nneonneo/pwn-stuff (including math/solvelinmod.py) 84 | RUN git clone https://github.com/nneonneo/pwn-stuff 85 | RUN ln -s /home/sage/collection_lattice_tools/pwn-stuff/math/solvelinmod.py /home/sage/collection_lattice_tools/solvelinmod.py 86 | 87 | ### maple3142/lll_cvp 88 | RUN git clone https://github.com/maple3142/lll_cvp 89 | RUN ln -s /home/sage/collection_lattice_tools/lll_cvp/lll_cvp.py /home/sage/collection_lattice_tools/lll_cvp.py 90 | 91 | ### TheBlupper/linineq 92 | RUN git clone https://github.com/TheBlupper/linineq 93 | RUN ln -s /home/sage/collection_lattice_tools/linineq/linineq.py /home/sage/collection_lattice_tools/linineq.py 94 | 95 | 96 | ### coppersmith package config 97 | WORKDIR /home/sage/ 98 | ENV PYTHONPATH=/home/sage/coppersmith/:$PYTHONPATH 99 | ENV COPPERSMITHFPLLLPATH=/usr/bin/ 100 | ENV COPPERSMITHFLATTERPATH=/usr/local/bin/ 101 | 102 | 103 | ### follow cryptohack-docker (https://github.com/cryptohack/cryptohack-docker) 104 | ENV PWNLIB_NOTERM=true 105 | 106 | ENV BLUE='\033[0;34m' 107 | ENV YELLOW='\033[1;33m' 108 | ENV RED='\e[91m' 109 | ENV NOCOLOR='\033[0m' 110 | 111 | ENV BANNER=" \n\ 112 | ${BLUE}----------------------------------${NOCOLOR} \n\ 113 | ${YELLOW}CryptoHack Docker Container${NOCOLOR} \n\ 114 | \n\ 115 | ${RED}After Jupyter starts, visit http://127.0.0.1:8888${NOCOLOR} \n\ 116 | ${BLUE}----------------------------------${NOCOLOR} \n\ 117 | " 118 | 119 | #CMD ["echo -e $BANNER && sage -n jupyter --NotebookApp.token='' --no-browser --ip='0.0.0.0' --port=8888"] 120 | CMD ["/home/sage/sage/sage", "--python", "coppersmith/example.py"] 121 | -------------------------------------------------------------------------------- /Dockerfile_sageonly: -------------------------------------------------------------------------------- 1 | FROM sagemath/sagemath:latest 2 | 3 | RUN sudo apt-get update && sudo apt-get install -y tzdata # avoid select timezone 4 | RUN sudo apt-get update && sudo apt-get upgrade -y 5 | 6 | RUN sudo apt-get install -y \ 7 | vim \ 8 | less \ 9 | git \ 10 | tmux \ 11 | netcat 12 | 13 | ### for building flatter 14 | #RUN sudo apt-get update \ 15 | # && sudo apt-get install -y \ 16 | # cmake \ 17 | # libtool \ 18 | # fplll-tools \ 19 | # libfplll-dev \ 20 | # libgmp-dev \ 21 | # libmpfr-dev \ 22 | # libeigen3-dev \ 23 | # libblas-dev \ 24 | # liblapack-dev 25 | 26 | USER sage 27 | 28 | RUN sage --pip install --no-cache-dir \ 29 | pwntools \ 30 | pycryptodome \ 31 | z3-solver \ 32 | tqdm 33 | 34 | ### prepare build flatter 35 | RUN mkdir /home/sage/coppersmith 36 | COPY --chown=sage:sage *.py /home/sage/coppersmith/ 37 | #COPY --chown=sage:sage flatter /home/sage/coppersmith/flatter/ 38 | 39 | 40 | ### install flatter (On current release, flatter are not included in Sagemath) 41 | #WORKDIR /home/sage/coppersmith/flatter 42 | #RUN mkdir build 43 | #WORKDIR /home/sage/coppersmith/flatter/build 44 | #RUN cmake .. 45 | #RUN make -j4 46 | 47 | #USER root 48 | #RUN make install 49 | #WORKDIR /home/sage/coppersmith 50 | #RUN rm -rf flatter 51 | #RUN apt-get remove --purge -y cmake 52 | #RUN apt-get autoremove --purge -y 53 | #USER sage 54 | 55 | 56 | #### other lattice library download 57 | RUN mkdir /home/sage/collection_lattice_tools 58 | WORKDIR /home/sage/collection_lattice_tools 59 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/:$PYTHONPATH 60 | 61 | ### defund/coppersmith 62 | RUN git clone https://github.com/defund/coppersmith 63 | RUN ln -s /home/sage/collection_lattice_tools/coppersmith/coppersmith.sage /home/sage/collection_lattice_tools/defund_coppersmith.sage 64 | ## load("/home/sage/collection_lattice_tools/defund_coppersmith.sage") 65 | 66 | ### josephsurin/lattice-based-cryptanalysis 67 | RUN git clone https://github.com/josephsurin/lattice-based-cryptanalysis 68 | RUN sed -i "s/algorithm='msolve', //g" /home/sage/collection_lattice_tools/lattice-based-cryptanalysis/lbc_toolkit/common/systems_solvers.sage 69 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/lattice-based-cryptanalysis/:$PYTHONPATH 70 | 71 | ### jvdsn/crypto-attacks 72 | RUN git clone https://github.com/jvdsn/crypto-attacks/ 73 | RUN mv crypto-attacks crypto_attacks 74 | ENV PYTHONPATH=/home/sage/collection_lattice_tools/crypto_attacks/:$PYTHONPATH 75 | 76 | ### rkm0959/Inequality_Solving_with_CVP 77 | RUN git clone https://github.com/rkm0959/Inequality_Solving_with_CVP 78 | RUN ln -s /home/sage/collection_lattice_tools/Inequality_Solving_with_CVP/solver.sage /home/sage/collection_lattice_tools/inequ_cvp_solve.sage 79 | ## load("/home/sage/collection_lattice_tools/inequ_cvp_solve.sage") 80 | 81 | ### nneonneo/pwn-stuff (including math/solvelinmod.py) 82 | RUN git clone https://github.com/nneonneo/pwn-stuff 83 | RUN ln -s /home/sage/collection_lattice_tools/pwn-stuff/math/solvelinmod.py /home/sage/collection_lattice_tools/solvelinmod.py 84 | 85 | ### maple3142/lll_cvp 86 | RUN git clone https://github.com/maple3142/lll_cvp 87 | RUN ln -s /home/sage/collection_lattice_tools/lll_cvp/lll_cvp.py /home/sage/collection_lattice_tools/lll_cvp.py 88 | 89 | ### TheBlupper/linineq 90 | RUN git clone https://github.com/TheBlupper/linineq 91 | RUN ln -s /home/sage/collection_lattice_tools/linineq/linineq.py /home/sage/collection_lattice_tools/linineq.py 92 | 93 | 94 | ### coppersmith package config 95 | WORKDIR /home/sage/ 96 | ENV PYTHONPATH=/home/sage/coppersmith/:$PYTHONPATH 97 | ENV COPPERSMITHFPLLLPATH=/home/sage/sage/local/bin/ 98 | #ENV COPPERSMITHFLATTERPATH=/usr/local/bin/ 99 | ### change default algorithm to FPLLL instead of FLATTER 100 | RUN sed -i 's| = FLATTER| = FPLLL|g' /home/sage/coppersmith/lll.py 101 | 102 | 103 | ### follow cryptohack-docker (https://github.com/cryptohack/cryptohack-docker) 104 | ENV PWNLIB_NOTERM=true 105 | 106 | ENV BLUE='\033[0;34m' 107 | ENV YELLOW='\033[1;33m' 108 | ENV RED='\e[91m' 109 | ENV NOCOLOR='\033[0m' 110 | 111 | ENV BANNER=" \n\ 112 | ${BLUE}----------------------------------${NOCOLOR} \n\ 113 | ${YELLOW}CryptoHack Docker Container${NOCOLOR} \n\ 114 | \n\ 115 | ${RED}After Jupyter starts, visit http://127.0.0.1:8888${NOCOLOR} \n\ 116 | ${BLUE}----------------------------------${NOCOLOR} \n\ 117 | " 118 | 119 | #CMD ["echo -e $BANNER && sage -n jupyter --NotebookApp.token='' --no-browser --ip='0.0.0.0' --port=8888"] 120 | CMD ["/home/sage/sage/sage", "--python", "coppersmith/example.py"] 121 | -------------------------------------------------------------------------------- /coppersmith_linear.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | import time 4 | import itertools 5 | import traceback 6 | 7 | from coppersmith_common import RRh, shiftpoly, genmatrix_from_shiftpolys, do_LLL, filter_LLLresult_coppersmith 8 | from rootfind_ZZ import rootfind_ZZ 9 | from contextclass import context 10 | from logger import * 11 | 12 | 13 | ### multivariate linear coppersmith (herrmann-may) 14 | def coppersmith_linear_core(basepoly, bounds, beta, t, m): 15 | logger.info("trying param: beta=%f, t=%d, m=%d", beta, t, m) 16 | basepoly_vars = basepoly.parent().gens() 17 | n = len(basepoly_vars) 18 | 19 | shiftpolys = [] 20 | for i, basepoly_var in enumerate(basepoly_vars): 21 | try: 22 | # NOTE: for working not integral domain, write as basepoly * (1/val) (do not write as basepoly / val) 23 | basepoly_i = basepoly * (1 / basepoly.monomial_coefficient(basepoly_var)) 24 | except: 25 | traceback.print_exc() 26 | logger.warning(f"maybe, facing an non-invertible monomial coefficient({basepoly_var}). still continuing...") 27 | ## TODO: continue procedures with hoping some monomial coefficients for at least one variable is invertible 28 | continue 29 | 30 | for k in range(m+1): 31 | for j in range(m-k+1): 32 | for xi_idx_sub in itertools.combinations_with_replacement(range(n-1), j): 33 | xi_idx = [xi_idx_sub.count(l) for l in range(n-1)] 34 | assert sum(xi_idx) == j 35 | xi_idx.insert(i, 0) 36 | # x2^i2 * ... * xn^in * f^k * N^max(t-k,0) 37 | shiftpolys.append(shiftpoly(basepoly_i, k, max(t-k, 0), xi_idx)) 38 | 39 | mat, m_lst = genmatrix_from_shiftpolys(shiftpolys, bounds) 40 | lll, _ = do_LLL(mat) 41 | result = filter_LLLresult_coppersmith(basepoly, beta, t, m_lst, lll, bounds) 42 | return result 43 | 44 | 45 | def coppersmith_linear(basepoly, bounds, beta, maxmatsize=100, maxm=8): 46 | if type(bounds) not in [list, tuple]: 47 | raise ValueError("bounds should be list or tuple") 48 | 49 | if beta >= 1.0: 50 | raise ValueError("beta is invalid. (for beta=1.0, use normal lattice reduction method directly.)") 51 | 52 | N = basepoly.parent().characteristic() 53 | 54 | basepoly_vars = basepoly.parent().gens() 55 | n = len(basepoly_vars) 56 | if n == 1: 57 | raise ValueError("one variable poly") 58 | 59 | if not set(basepoly.monomials()).issubset(set(list(basepoly_vars)+[1])): 60 | raise ValueError("non linear poly") 61 | 62 | log_N_X = RRh(log(product(bounds), N)) 63 | log_N_X_bound = 1-(1-RRh(beta))**(RRh(n+1)/n) - (n+1)*(1-(1-RRh(beta))**(RRh(1)/n)) * (1-RRh(beta)) 64 | 65 | if log_N_X >= log_N_X_bound: 66 | raise ValueError("too much large bound") 67 | 68 | mestimate = (n*(-RRh(beta)*ln(1-beta) + ((1-RRh(beta))**(-0.278465))/pi)/(log_N_X_bound - log_N_X))/(n+1.5) 69 | tau = 1 - (1-RRh(beta))**(RRh(1)/n) 70 | testimate = int(mestimate * tau + 0.5) 71 | 72 | logger.debug("testimate: %d", testimate) 73 | t = max(testimate, 1) 74 | 75 | while True: 76 | if t == 1: 77 | break 78 | m = int(t/tau+0.5) 79 | if binomial(n+1+m-1, m) <= maxmatsize: 80 | break 81 | t -= 1 82 | 83 | whole_st = time.time() 84 | 85 | curfoundpols = [] 86 | while True: 87 | m0 = int(t/tau+0.5) 88 | if binomial(n+1+m0-1, m0) > maxmatsize: 89 | raise ValueError(f"maxmatsize exceeded: {binomial(n+1+m0-1, m0)}") 90 | for m_diff in range(0, maxm+1): 91 | m = m0 + m_diff 92 | if binomial(n+1+m-1, m) > maxmatsize: 93 | break 94 | foundpols = coppersmith_linear_core(basepoly, bounds, beta, t, m) 95 | if len(foundpols) == 0: 96 | continue 97 | 98 | curfoundpols += foundpols 99 | curfoundpols = list(set(curfoundpols)) 100 | sol = rootfind_ZZ(curfoundpols, bounds, **context.rootfindZZopt) 101 | if sol != [] and sol is not None: 102 | whole_ed = time.time() 103 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 104 | return sol 105 | 106 | polrate = (1.0 * len(curfoundpols))/n 107 | if polrate > 1.0: 108 | logger.warning(f"polrate is over 1.0 (you might have inputted wrong pol): {polrate}") 109 | whole_ed = time.time() 110 | logger.info("whole elapsed time (not ended): %f", whole_ed-whole_st) 111 | t += 1 112 | # never reached here 113 | return None 114 | -------------------------------------------------------------------------------- /coppersmith_multivariate_heuristic.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | import time 4 | import itertools 5 | import traceback 6 | 7 | from coppersmith_common import RRh, shiftpoly, genmatrix_from_shiftpolys, do_LLL, filter_LLLresult_coppersmith 8 | from rootfind_ZZ import rootfind_ZZ 9 | from contextclass import context 10 | from logger import * 11 | 12 | 13 | def gen_set_leading_monomials(basepoly): 14 | if basepoly.is_constant(): 15 | return [basepoly.parent()(1)] 16 | 17 | lmset = [basepoly.parent()(1)] 18 | for monomial in basepoly.monomials(): 19 | newlmfound = True 20 | newlmset = [] 21 | for lmsetele in lmset: 22 | if monomial % lmsetele != 0: 23 | newlmset.append(lmsetele) 24 | if lmsetele % monomial == 0: 25 | newlmfound = False 26 | if newlmfound: 27 | newlmset.append(monomial) 28 | lmset = newlmset[:] 29 | return lmset 30 | 31 | 32 | def generate_M_with_ExtendedStrategy(basepoly, lm, t, d): 33 | basepoly_vars = basepoly.parent().gens() 34 | n = len(basepoly_vars) 35 | 36 | M = {} 37 | basepoly_pow_monos = (basepoly ** t).monomials() 38 | for k in range(t+1): 39 | M[k] = set() 40 | basepoly_powk_monos = (basepoly ** (t - k)).monomials() 41 | for monos in basepoly_pow_monos: 42 | if monos // (lm ** k) in basepoly_powk_monos: 43 | for extra in itertools.product(range(d), repeat=n): 44 | g = monos * prod([v ** i for v, i in zip(basepoly_vars, extra)]) 45 | M[k].add(g) 46 | M[t+1] = set() 47 | return M 48 | 49 | 50 | ### multivariate coppersmith with some heuristic (jochemsz-may) 51 | def coppersmith_multivariate_heuristic_core(basepoly, bounds, beta, t, d, lm, maxmatsize=100): 52 | logger.info("trying param: beta=%f, t=%d, d=%d, lm=%s", beta, t, d, str(lm)) 53 | basepoly_vars = basepoly.parent().gens() 54 | n = len(basepoly_vars) 55 | 56 | try: 57 | # NOTE: for working not integral domain, write as basepoly * (1/val) (do not write as basepoly / val) 58 | basepoly_i = basepoly * (1 / basepoly.monomial_coefficient(lm)) 59 | except: 60 | traceback.print_exc() 61 | logger.warning(f"maybe, facing an non-invertible monomial coefficient({lm}). still continuing...") 62 | return [] 63 | 64 | M = generate_M_with_ExtendedStrategy(basepoly_i, lm, t, d) 65 | shiftpolys = [] 66 | for k in range(t+1): 67 | for mono in M[k] - M[k+1]: 68 | curmono = (mono // (lm ** k)) 69 | xi_idx = curmono.exponents()[0] 70 | shiftpolys.append(shiftpoly(basepoly_i, k, t - k, xi_idx)) 71 | 72 | mat, m_lst = genmatrix_from_shiftpolys(shiftpolys, bounds) 73 | if mat.ncols() > maxmatsize: 74 | logger.warning("maxmatsize exceeded: %d", mat.ncols()) 75 | return [] 76 | 77 | lll, _ = do_LLL(mat) 78 | result = filter_LLLresult_coppersmith(basepoly, beta, t, m_lst, lll, bounds) 79 | return result 80 | 81 | 82 | def coppersmith_multivariate_heuristic(basepoly, bounds, beta, maxmatsize=100, maxd=8): 83 | if type(bounds) not in [list, tuple]: 84 | raise ValueError("bounds should be list or tuple") 85 | 86 | N = basepoly.parent().characteristic() 87 | 88 | basepoly_vars = basepoly.parent().gens() 89 | n = len(basepoly_vars) 90 | if n == 1: 91 | raise ValueError("one variable poly") 92 | 93 | # dealing with all candidates of leading monomials 94 | lms = gen_set_leading_monomials(basepoly) 95 | 96 | basepoly_ZZ_vars = basepoly.change_ring(ZZ).parent().gens() 97 | estimated_bound_size = prod(lms).change_ring(ZZ).subs({basepoly_ZZ_vars[i]: bounds[i] for i in range(len(basepoly_ZZ_vars))}) 98 | estimated_bound_rate = RRh(estimated_bound_size) / (RRh(N)**RRh(beta)) 99 | if estimated_bound_rate > RRh(1.0): 100 | logger.warning("It seems estimated bound rate is large (%f) (heuristic estimation, so go ahead)", float(estimated_bound_rate)) 101 | 102 | t = 2 103 | 104 | whole_st = time.time() 105 | 106 | curfoundpols = [] 107 | while True: 108 | d0 = t 109 | for d_diff in range(0, maxd+1): 110 | d = d0 + d_diff 111 | for lm in lms: 112 | foundpols = coppersmith_multivariate_heuristic_core(basepoly, bounds, beta, t, d, lm, maxmatsize=maxmatsize) 113 | if len(foundpols) == 0: 114 | continue 115 | curfoundpols += foundpols 116 | curfoundpols = list(set(curfoundpols)) 117 | sol = rootfind_ZZ(curfoundpols, bounds, **context.rootfindZZopt) 118 | if sol != [] and sol is not None: 119 | whole_ed = time.time() 120 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 121 | return sol 122 | 123 | polrate = (1.0 * len(curfoundpols))/n 124 | if polrate > 1.0: 125 | logger.warning(f"polrate is over 1.0 (you might have inputted wrong pol): {polrate}") 126 | whole_ed = time.time() 127 | logger.info("whole elapsed time (not ended): %f", whole_ed-whole_st) 128 | t += 1 129 | # never reached here 130 | return None 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coppersmith small roots 2 | 3 | Coppersmith small roots method (solving polynomial equation over composite modulus on small bounds) 4 | 5 | ## Features 6 | 7 | - multiple coppersmith equation support 8 | - univariate polynomial 9 | - multivariate linear polynomial (Herrmann-May method) 10 | - multivariate polynomial (Jochemsz-May heuristic method) 11 | 12 | Mainly, dealing with theoretically established Coppersmith method applicable equation. We recommend using univariate or linear (instead of heuristic) if you know the type of equation. 13 | 14 | - automated parameter tuning 15 | 16 | Firstly choose suitable parameters based on bounds and $\beta$, and then increment parameters until finishing to find an integer equation. 17 | 18 | - selectable internal LLL/BKZ method 19 | - [fplll](https://github.com/fplll/fplll) (LLL/BKZ) 20 | - [flatter](https://github.com/keeganryan/flatter/) 21 | - NTL (via Sagemath LLL/BKZ) 22 | 23 | - selectable solving integer equation method 24 | - jacobian Newton method (numerical) 25 | - Hensel lifting 26 | - triangulation (Groebner basis + dealing with non-zero dimensional ideal partially) 27 | 28 | ## Installation 29 | 30 | ```bash 31 | $ git clone https://github.com/kionactf/coppersmith --sync 32 | ``` 33 | 34 | If you use fplll or flatter, need to build these packages. if you already installed them, you can use this by setting these install paths to fplll_path or flatter_path on lll.py. 35 | 36 | Or you can create a docker image. It is based on sagemath/sagemath:latest image and also downloads well-known lattice libraries. (If you know good other libraries, let us know for including these.) 37 | 38 | ```bash 39 | $ docker build -t coppersmith . 40 | $ docker run --rm -it coppersmith /bin/bash 41 | ``` 42 | 43 | ## Usage 44 | 45 | Call coppersmith_onevariable.coppersmith_onevariable or coppersmith_linear.coppersmith_linear with Sagemath PolynomialRing over Zmod(N), bounds, beta. 46 | 47 | See `example.py`. 48 | 49 | Also, you can use only `LLL `by calling lll.do_lattice_reduction with Sagemath matrix over ZZ and some optimization parameters. And lll.babai for CVP solver. 50 | 51 | For `integer equations solver`, use rootfind_ZZ.rootfind_ZZ with a list of Sagemath polynomial over ZZ and bounds. 52 | 53 | ## Custom Configuration 54 | 55 | (The feature was introduced on 2024/3/8.) 56 | 57 | Although we set up built-in default settings based on our some experiments, some users say the setting does not fit on their environments. 58 | 59 | So we prepares a configuration interface `context` for `lll.py` and `rootfind_ZZ.py`. 60 | 61 | You can use it by importing `context` just as the following example: 62 | 63 | ```python 64 | from coppersmith_linear import * # including context 65 | from lll import FLATTER 66 | from rootfind_ZZ import JACOBIAN, TRIANGULATE 67 | 68 | # use flatter 69 | context.lllopt = {'algorithm':FLATTER, 'use_pari_kernel':True} 70 | # use jacobian method and triangulate in order, 71 | # and for jacobian method set the number of iteration(loop) as 32 (much less comparing the default value 1024) 72 | context.rootfindZZopt = {'algorithm':(JACOBIAN, TRIANGULATE), 'maxiternum':32} 73 | 74 | # debug output enable 75 | logger.setLevel(DEBUG) 76 | 77 | P = PolynomialRing(...) 78 | f = ... 79 | beta = ... 80 | bounds = [...] 81 | coppersmith_linear(f, bounds, beta) 82 | ```` 83 | 84 | You can check the list of options and their default values at `register_options_lll` and `register_options_rootfind_ZZ` at `contextclass.py`. 85 | 86 | 87 | ## Note (use_pari_kernel) 88 | 89 | For computing LLL, we use pari.matker for eliminating linearly dependent vectors for defining lattice. The process needs to use flatter. Though pari.matker is fast and outputs correct results in many cases, it sometimes outputs `wrong` results. (You can check this by running lll.test().) You may disable to use pari.matker by setting the option `use_pari_kernel=False`, where it forces using Sagemath kernel (which do internally run computing hermite normal form (HNF).) Note that HNF tends to produce large elements, so it causes LLL process makes super slow. 90 | 91 | ## Background 92 | 93 | See [Why we could not solve chronophobia… Analysis for Coppersmith method more](https://hackmd.io/@kiona/rywwKMYjj). (it might be an old article, though.) 94 | 95 | ## How to choose parameters? 96 | Coppersmith small root method is to find a root of the following type equation: 97 | 98 | $f(r_1,\ldots,r_n)=0 \pmod{b}$, where $| r_i | < X_i\ (i=1,\ldots,n)$, 99 | for known polynomial $f(r)$ and known $N$ such that $b\ |\ N$. 100 | 101 | The package function requires the following parameters. 102 | 103 | - basepoly: the polynomial $f(r)$ over Zmod($N$) 104 | - bounds: the list $[X_1,\ldots,X_n]$ whose positive integers $X_1,\ldots,X_n$ 105 | - beta: a positive floating point number $\beta$ such that $b \ge N^\beta$ 106 | 107 | For determining $\beta$, we recommend the following guideline. 108 | 109 | - If $b$ is known, then $\beta=\log_{N}(b)$ 110 | - If $b$ is unknown but bitsize of $b$ is known, then $\beta=(\text{bitsize}(b)-1)/(\text{bitsize}(N))$ 111 | 112 | For example, $N=pq$ and $\text{bitsize}(p)=\text{bitsize}(q)$, then $\beta\simeq 0.499$. 113 | 114 | ## Completeness of the Package 115 | 116 | Q: Can you solve many modulus multivariate polynomial systems by the package? 117 | 118 | A: Maybe no. It seems to be hard to create general modulus multivariate polynomial solver. It depends on monomials (such as \[ $f=ax^2 + by + c$ \] `v.s.` \[ $g=ax^2 + bxy + cy + d$ \]), coefficients (such as \[ $f=ax^2-by+c$ \] `v.s.` \[ $g=ax^2-ay+c$ \]), and bounds (such as \[ $X \simeq Y$ \] `v.s.` \[ $X \simeq Y^2$ \]). Many papers handle each specific cases. 119 | 120 | Especially, we do not recommend to use heuristic method without understanding the polynomial. Heuristic method does not estimate bound condition (unlike univariate case or linear case), so you would be confused that the solver did not output as what you expected. 121 | 122 | Alternatively, you may use linearization strategy and/or apply rootfind_ZZ directly. For example, if you want to solve $f=ax^2+by \pmod{N}$, first define ${f_\mathbb{Z}}=ax'+by+kN \in \mathbb{Z}[x',y,k]$, then apply rootfind_ZZ (or other integer polynomial solver) with bound $[X^2, Y]$ (we search $k$ with bruteforce). If we can assume $|k|$ is small, the system will be solved. Note that Coppersmith methods does not necessarily find algebraically independent polynomial sets. So you might have to solve same polynomial system $f_\mathbb{Z}$ even if you forced to apply Coppersmith method. rootfind_ZZ.solve_root_triangulate tries to solve non-zero dimensional linear system. 123 | 124 | Note that rootfind_ZZ does not necessarily find all roots, but only a few roots. Finding roots over integer is not easy, so you should not use the package for multiple roots included system. You can devise some methods for avoiding multiple roots. Some method might be narrowing bounds range by converting variables. Some rootfind_ZZ internal functions assume that a root exist near bounds. 125 | 126 | ## Contribution 127 | 128 | The package must not be perfect, we want you to be reviewed and improved this. Welcome to post any issues, pull requests and forks. And let us know `test cases` from any CTF or other resources. Failed test cases may make us to improve the package. 129 | 130 | ## Reference 131 | 132 | Some of our codes are based on the following libraries. 133 | 134 | - defund/coppersmith (https://github.com/defund/coppersmith) 135 | 136 | multivariate coppersmith method: `coppersmith.sage` 137 | 138 | - josephsurinlattice-based-cryptanalysis (https://github.com/josephsurin/lattice-based-cryptanalysis) 139 | 140 | multivariate coppersmith method: `lbc_toolkit/problems/small_roots.sage`. Some solvers for integer polynomial roots are at `lbc_toolkit/common/systems_solvers.sage`. 141 | 142 | - jvdsn/crypto-attacks (https://github.com/jvdsn/crypto-attacks) 143 | 144 | various coppersmith methods (including Herrmann-May, Blomer-May, Boneh-Durfee, Jochemsz-May, etc.): `shared/small_roots/*.py`. Some solvers for integer polynomial roots are at `shared/small_roots/__init__.py`. 145 | 146 | jvdsn/crypto-attacks is distributed under: 147 | 148 | >MIT license (https://github.com/jvdsn/crypto-attacks/blob/master/LICENSE) 149 | > 150 | >(c) 2020 Joachim Vandersmissen. 151 | 152 | ## Copyright 153 | 154 | This library is distributed under Apache 2.0 License. See LICENSE. 155 | 156 | >(C) 2023 kiona 157 | > 158 | >https://github.com/kionactf/coppersmith 159 | 160 | For redistribution, just say that it has been changed and note the link for our files. 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | import time 4 | from Crypto.Util.number import * 5 | from random import randrange, randint, choices as random_choices 6 | import string 7 | import sys 8 | 9 | from coppersmith_onevariable import coppersmith_onevariable 10 | from coppersmith_linear import coppersmith_linear 11 | from coppersmith_multivariate_heuristic import coppersmith_multivariate_heuristic 12 | from lll import * 13 | from contextclass import context 14 | from logger import * 15 | 16 | 17 | sys.set_int_max_str_digits(8000) 18 | 19 | 20 | #logger.setLevel(DEBUG) 21 | 22 | 23 | lllopt = {} 24 | #lllopt = {'algorithm':FPLLL} 25 | #lllopt = {'algorithm':FLATTER, 'use_pari_kernel':True} 26 | #lllopt = {'algorithm':FPLLL_BKZ, 'blocksize':3} 27 | 28 | context.lllopt = lllopt 29 | 30 | 31 | def example_onevariable_linear(): 32 | our_small_roots = lambda f, X: coppersmith_onevariable(f, [X], beta) 33 | bitsize = 2048 34 | while True: 35 | p = getPrime(bitsize//2) 36 | q = getPrime(bitsize//2) 37 | if p != q: 38 | break 39 | N = p*q 40 | 41 | beta = (1.0*(bitsize//2-1))/bitsize # (1024-1)/2048 >= 0.4995 (also 511/1024 >=0.499) 42 | 43 | # in my exp, worked for 500, but took much time... 44 | discardbitsizelst = [40, 256, 488] 45 | for discardbitsize in discardbitsizelst: 46 | print(discardbitsize) 47 | p0 = p>>discardbitsize 48 | P = PolynomialRing(Zmod(N), 'x') 49 | ## following P works for our_small_roots, but not Sagemath small_roots 50 | #P = PolynomialRing(Zmod(N), 1, 'x') 51 | x = P.gens()[0] 52 | f = (p0 << discardbitsize) + x 53 | result = our_small_roots(f, 2**discardbitsize) 54 | print(f"result:{result}, real:{p % (2**discardbitsize)}") 55 | 56 | # check output of sage small_roots 57 | if discardbitsize >= 488: 58 | epsilon = 0.02 59 | else: 60 | epsilon = 0.05 61 | 62 | sage_st = time.time() 63 | print(f"sage result:{f.small_roots(X=2**discardbitsize, beta=beta, epsilon=epsilon)}") 64 | sage_ed = time.time() 65 | logger.debug("sage comp elapsed time: %f", sage_ed - sage_st) 66 | 67 | # sometimes works with small beta (but 488 not works) 68 | print(f"sage result (small beta):{f.small_roots(X=2**discardbitsize, beta=0.1)}") 69 | 70 | 71 | def example_twovariable_linear(): 72 | our_small_roots = lambda f, bounds: coppersmith_linear(f, bounds, beta) 73 | bitsize = 2048 74 | while True: 75 | p = getPrime(bitsize//2) 76 | q = getPrime(bitsize//2) 77 | if p != q: 78 | break 79 | N = p*q 80 | 81 | beta = (1.0*(bitsize//2-1))/bitsize # (1024-1)/2048 >= 0.4995 (also 511/1024 >=0.499) 82 | 83 | # it seems severe for over (160, 160) (t=3 needs much time) 84 | discardbitpointlst = [[(756, 20),(256, 20)], [(756, 135), (256, 135)], [(756, 160), (256, 160)]] 85 | for discardbitpoint in discardbitpointlst: 86 | print(discardbitpoint) 87 | p0 = 0 88 | real = [] 89 | for i, ele in enumerate(discardbitpoint): 90 | if i == 0: 91 | p0 += (p>>ele[0]) << ele[0] 92 | else: 93 | p0 += ((p % (2**(discardbitpoint[i-1][0]-discardbitpoint[i-1][1]))) >> ele[0]) << ele[0] 94 | real.append((p % (2**ele[0]))>>(ele[0]-ele[1])) 95 | p0 += p % (2**(discardbitpoint[-1][0] - discardbitpoint[-1][1])) 96 | 97 | P = PolynomialRing(Zmod(N), 2, 'xy') 98 | P_vars = P.gens() 99 | bounds = [] 100 | f = p0 101 | for i,ele in enumerate(discardbitpoint): 102 | f += (2**(ele[0]-ele[1]))*P_vars[i] 103 | bounds.append(2**ele[1]) 104 | result = our_small_roots(f, bounds) 105 | print(f"result:{result}, real:{real}") 106 | 107 | 108 | def example_threevariable_linear(): 109 | our_small_roots = lambda f, bounds: coppersmith_linear(f, bounds, beta) 110 | bitsize = 2048 111 | while True: 112 | p = getPrime(bitsize//2) 113 | q = getPrime(bitsize//2) 114 | if p != q: 115 | break 116 | N = p*q 117 | 118 | beta = (1.0*(bitsize//2-1))/bitsize # (1024-1)/2048 >= 0.4995 (also 511/1024 >=0.499) 119 | 120 | # it seems severe for over (160, 160) (t=3 needs much time) 121 | discardbitpointlst = [[(756, 20),(512, 20),(256, 20)], [(756, 40),(512, 40),(256,40)], [(756, 72), (512,72), (256, 72)]] 122 | for discardbitpoint in discardbitpointlst: 123 | print(discardbitpoint) 124 | p0 = 0 125 | real = [] 126 | for i, ele in enumerate(discardbitpoint): 127 | if i == 0: 128 | p0 += (p>>ele[0]) << ele[0] 129 | else: 130 | p0 += ((p % (2**(discardbitpoint[i-1][0]-discardbitpoint[i-1][1]))) >> ele[0]) << ele[0] 131 | real.append((p % (2**ele[0]))>>(ele[0]-ele[1])) 132 | p0 += p % (2**(discardbitpoint[-1][0] - discardbitpoint[-1][1])) 133 | 134 | P = PolynomialRing(Zmod(N), 3, 'xyz') 135 | P_vars = P.gens() 136 | bounds = [] 137 | f = p0 138 | for i,ele in enumerate(discardbitpoint): 139 | f += (2**(ele[0]-ele[1]))*P_vars[i] 140 | bounds.append(2**ele[1]) 141 | result = our_small_roots(f, bounds) 142 | print(f"result:{result}, real:{real}") 143 | 144 | 145 | def example_shortpad_attack(): 146 | # example of Coppersmith's short-pad attack; non-monic univariate polynomial case 147 | bitsize = 2048 148 | padbytelen = 24 149 | while True: 150 | p = getPrime(bitsize//2) 151 | q = getPrime(bitsize//2) 152 | N = p * q 153 | e = 3 154 | phi = (p - 1) * (q - 1) 155 | if GCD(phi, e) == 1: 156 | d = pow(e, -1, phi) 157 | break 158 | charlist = string.ascii_uppercase + string.ascii_lowercase + string.digits 159 | M = ''.join(random_choices(charlist, k=115)) + '_' + ''.join(random_choices(charlist, k=115)) 160 | pad = ''.join(random_choices(charlist, k=padbytelen)) 161 | 162 | M_1 = bytes_to_long((M + '\x00' * padbytelen).encode()) 163 | M_2 = bytes_to_long((M + pad).encode()) 164 | 165 | C_1 = pow(M_1, e, N) 166 | C_2 = pow(M_2, e, N) 167 | 168 | # attack from here 169 | P_first = PolynomialRing(ZZ, 2, "xy") 170 | x, y = P_first.gens() 171 | 172 | ## x = (M + '\x00' * padbytelen), y = pad 173 | pol1 = x ** e - C_1 174 | pol2 = (x + y) ** e - C_2 175 | pol = pol1.resultant(pol2, x) 176 | 177 | pol_uni = pol.univariate_polynomial().change_ring(Zmod(N)) 178 | sol = coppersmith_onevariable(pol_uni, [2**(8*padbytelen)], 1.0)[0] 179 | 180 | ## Franklin-Reiter related-message attack 181 | pol1_uni = pol1.univariate_polynomial().change_ring(Zmod(N)) 182 | pol2_uni = pol2.subs({x:x, y:sol}).univariate_polynomial().change_ring(Zmod(N)) 183 | 184 | def composite_gcd(f1, f2): 185 | if f2 == 0: 186 | return f1.monic() 187 | if f1.degree() < f2.degree(): 188 | return composite_gcd(f2, f1) 189 | return composite_gcd(f2, f1 % f2) 190 | 191 | pol_gcd = composite_gcd(pol1_uni, pol2_uni) 192 | assert pol_gcd.degree() == 1 193 | 194 | degoneinv = (pol_gcd.monomial_coefficient(pol_gcd.parent().gens()[0]) ** (-1)) 195 | found_M_N = -pol_gcd.constant_coefficient() * degoneinv 196 | found_M = long_to_bytes(int(found_M_N.lift())).split(b'\x00')[0] 197 | 198 | print(f"result:{found_M}, real:{M}") 199 | 200 | 201 | def example_chronophobia(): 202 | # chronophobia from idekCTF2022 203 | L = 200 204 | 205 | p = getPrime(512) 206 | q = getPrime(512) 207 | n = p*q 208 | phi = (p-1) * (q-1) 209 | 210 | t = randint(0, n-1) 211 | d = randint(128, 256) 212 | r = pow(2, 1 << d, phi) 213 | 214 | ans1 = pow(t, r, n) 215 | u1 = int(str(ans1)[:L]) 216 | L1down = len(str(ans1)[L:]) 217 | ans2 = pow(pow(t, 2, n), r, n) 218 | u2 = int(str(ans2)[:L]) 219 | L2down = len(str(ans2)[L:]) 220 | 221 | P = PolynomialRing(Zmod(n), 2, ["x", "y"]) 222 | x, y = P.gens() 223 | 224 | f = (u1 * (10**L1down) + x)**2 - (u2 * (10**L2down) + y) 225 | bounds = [10**L1down, 10**L2down] 226 | 227 | sol = coppersmith_multivariate_heuristic(f, bounds, 1.0) 228 | print(f"result:{sol}, real:{(int(str(ans1)[L:]), int(str(ans2)[L:]))}") 229 | 230 | 231 | def example_bivariate_stereotyped_message_attack(): 232 | # @Warri posted to cryptohack discord channel (#cryptography, May.23, 2023) 233 | bitsize = 1024 234 | part_M_first_size = 14 235 | part_M_second_size = 15 236 | while True: 237 | p = getPrime(bitsize//2) 238 | q = getPrime(bitsize//2) 239 | N = p * q 240 | e = 3 241 | phi = (p - 1) * (q - 1) 242 | if GCD(phi, e) == 1: 243 | d = pow(e, -1, phi) 244 | break 245 | charlist = string.ascii_uppercase + string.ascii_lowercase + string.digits 246 | part_M_first = ''.join(random_choices(charlist, k=part_M_first_size)) 247 | part_M_second = ''.join(random_choices(charlist, k=part_M_second_size)) 248 | prefix = ''.join(random_choices(charlist, k=40)) 249 | midfix = ''.join(random_choices(charlist, k=30)) 250 | suffix = ''.join(random_choices(charlist, k=20)) 251 | 252 | M = bytes_to_long( 253 | (prefix + part_M_first + midfix + part_M_second + suffix).encode() 254 | ) 255 | C = pow(M, e, N) 256 | 257 | # attack from here 258 | P = PolynomialRing(Zmod(N), 2, "xy") 259 | x, y = P.gens() 260 | 261 | f_p = bytes_to_long(suffix.encode()) 262 | f_p += y * (2**(8*len(suffix))) 263 | f_p += bytes_to_long(midfix.encode()) * (2**(8*(part_M_second_size + len(suffix)))) 264 | f_p += x * (2**(8*(len(midfix) + part_M_second_size + len(suffix)))) 265 | f_p += bytes_to_long(prefix.encode()) * (2**(8*(part_M_first_size + len(midfix) + part_M_second_size + len(suffix)))) 266 | f = f_p ** e - C 267 | 268 | bounds = (2**(8*part_M_first_size), 2**(8*part_M_second_size)) 269 | sol = coppersmith_multivariate_heuristic(f, bounds, 1.0) 270 | 271 | found_part_M_first = long_to_bytes(int(sol[0][0])) 272 | found_part_M_second = long_to_bytes(int(sol[0][1])) 273 | print(f"result:{(found_part_M_first, found_part_M_second)}, real:{(part_M_first, part_M_second)}") 274 | 275 | 276 | def _example_multivariate_heuristic_1(): 277 | # from bivariate_example on https://github.com/josephsurin/lattice-based-cryptanalysis/blob/main/examples/problems/small_roots.sage 278 | N = random_prime(2**512) * random_prime(2**512) 279 | bounds = (2**164, 2**164) # N**0.16 280 | roots = tuple(randrange(bound) for bound in bounds) 281 | P = PolynomialRing(Zmod(N), 2, ["x", "y"]) 282 | x, y = P.gens() 283 | monomials = [x, y, x*y, x**2, y**2] 284 | f = sum(randrange(N) * monomial for monomial in monomials) 285 | f -= f(*roots) 286 | sol = coppersmith_multivariate_heuristic(f, bounds, 1.0) 287 | print(f"result:{sol}, real:{roots}") 288 | 289 | 290 | def _example_multivariate_heuristic_2(): 291 | # from trivariate_example on https://github.com/defund/coppersmith/blob/master/examples.sage 292 | p = random_prime(2**1024) 293 | q = random_prime(2**1024) 294 | N = p*q 295 | bounds = (2**246, 2**246, 2**246) # N**0.12 296 | roots = tuple(randrange(bound) for bound in bounds) 297 | P = PolynomialRing(Zmod(N), 3, ["x", "y", "z"]) 298 | x, y, z = P.gens() 299 | monomials = [x, y, x*y, x*z, y*z] 300 | f = sum(randrange(N)*monomial for monomial in monomials) 301 | f -= f(*roots) 302 | sol = coppersmith_multivariate_heuristic(f, bounds, 1.0) 303 | print(f"result:{sol}, real:{roots}") 304 | 305 | 306 | 307 | 308 | def example_multivariate_heuristic(): 309 | _example_multivariate_heuristic_1() 310 | _example_multivariate_heuristic_2() 311 | 312 | 313 | if __name__ == '__main__': 314 | example_onevariable_linear() 315 | example_twovariable_linear() 316 | example_threevariable_linear() 317 | example_shortpad_attack() 318 | example_multivariate_heuristic() 319 | example_chronophobia() 320 | example_bivariate_stereotyped_message_attack() 321 | -------------------------------------------------------------------------------- /rootfind_ZZ.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | from random import shuffle as random_shuffle 4 | from itertools import product as itertools_product 5 | import time 6 | import traceback 7 | 8 | from lll import do_lattice_reduction, FPLLL, FPLLL_BKZ, FLATTER, NTL, NTL_BKZ 9 | 10 | from logger import * 11 | 12 | 13 | # algorithm of rootfind for multivariate polys 14 | JACOBIAN = 0 15 | HENSEL = 1 16 | TRIANGULATE = 2 17 | GROEBNER = 3 18 | 19 | # algorithm of symbolic linear 20 | LINEAR_SIMPLE = 0 21 | LINEAR_NEAR_BOUNDS = 1 22 | 23 | 24 | def solve_root_onevariable(pollst, bounds, **kwds): 25 | logger.info("start solve_root_onevariable") 26 | st = time.time() 27 | 28 | for f in pollst: 29 | f_x = f.parent().gens()[0] 30 | try: 31 | rt_ = f.change_ring(ZZ).roots() 32 | rt = [ele for ele, exp in rt_] 33 | except: 34 | f_QQ = f.change_ring(QQ) 35 | f_QQ_x = f_QQ.parent().gens()[0] 36 | rt_ = f_QQ.parent().ideal([f_QQ]).variety() 37 | rt = [ele[f_QQ_x] for ele in rt_] 38 | if rt != []: 39 | break 40 | result = [] 41 | for rtele in rt: 42 | if any([pollst[i].subs({f_x: int(rtele)}) != 0 for i in range(len(pollst))]): 43 | continue 44 | if abs(int(rtele)) < bounds[0]: 45 | result.append(rtele) 46 | 47 | ed = time.time() 48 | logger.info("end solve_root_onevariable. elapsed %f", ed-st) 49 | 50 | return result 51 | 52 | 53 | def solve_root_groebner(pollst, bounds, **kwds): 54 | logger.info("start solve_root_groebner") 55 | st = time.time() 56 | 57 | if 'monomial_order_for_variety' not in kwds: 58 | kwds['monomial_order_for_variety'] = 'degrevlex' 59 | 60 | # I heard degrevlex is faster computation for groebner basis, but idk real effect 61 | polrng_QQ = pollst[0].change_ring(QQ).parent().change_ring(order=kwds['monomial_order_for_variety']) 62 | vars_QQ = polrng_QQ.gens() 63 | G = Sequence(pollst, polrng_QQ).groebner_basis() 64 | try: 65 | # not zero-dimensional ideal raises error 66 | rt_ = G.ideal().variety() 67 | except: 68 | logger.warning("variety failed. not zero-dimensional ideal?") 69 | return None 70 | rt = [[int(ele[v]) for v in vars_QQ] for ele in rt_] 71 | 72 | vars_ZZ = pollst[0].parent().gens() 73 | result = [] 74 | for rtele in rt: 75 | if any([pollst[i].subs({v: int(rtele[i]) for i, v in enumerate(vars_ZZ)}) != 0 for i in range(len(pollst))]): 76 | continue 77 | if all([abs(int(rtele[i])) < bounds[i] for i in range(len(rtele))]): 78 | result.append(rtele) 79 | 80 | ed = time.time() 81 | logger.info("end solve_root_groebner. elapsed %f", ed-st) 82 | return result 83 | 84 | 85 | def solve_ZZ_symbolic_linear_internal(sol_coefs, bounds, **kwds): 86 | # solve X_i = sum( (a_ij/b_ij)*v_j for j) for (a_ij/b_ij) in sol_coefs, |X_i| < bounds[i] (v_j: variable) 87 | 88 | if 'lllopt_symbolic_linear' not in kwds: 89 | kwds['lllopt_symbolic_linear'] = {'algorithm':FPLLL_BKZ} 90 | lllopt_symbolic_linear = kwds['lllopt_symbolic_linear'] 91 | 92 | ## for scaling lattice value 93 | mult = prod(bounds) 94 | 95 | ## construct equation (row vectors: [v_j for j] + [X_i for i] + [1]) 96 | matele = [] 97 | for i, sol_coef in enumerate(sol_coefs): 98 | denom = 1 99 | for sol_coef_ele in sol_coef: 100 | denom = LCM(denom, sol_coef_ele.denominator()) 101 | for sol_coef_ele in sol_coef: 102 | matele.append(ZZ(sol_coef_ele * denom * mult)) 103 | matele += [0]*i + [-denom * mult] + [0]*(len(bounds)-i-1) 104 | 105 | ## constrain to bounds (|X_i| < bounds[i]) 106 | for idx, bd in enumerate(bounds): 107 | matele += [0]*len(sol_coefs[0]) + [0]*idx + [mult//bd] + [0]*(len(bounds)-idx-1) 108 | 109 | ## constrain to const (kannan embedding) 110 | matele += [0]*(len(sol_coefs[0])-1) + [mult] + [0]*len(bounds) 111 | 112 | ## BKZ (assume the number of variables are small) 113 | mat = matrix(ZZ, len(sol_coefs)+len(bounds)+1, len(sol_coefs[0])+len(bounds), matele) 114 | logger.debug(f"start LLL for solve_ZZ_symbolic_linear_internal") 115 | mattrans = mat.transpose() 116 | lll, trans = do_lattice_reduction(mattrans, lllopt_symbolic_linear) 117 | logger.debug(f"end LLL") 118 | 119 | ## search solution 120 | for i in range(trans.nrows()): 121 | if all([lll[i, j] == 0 for j in range(len(sol_coefs))]): 122 | if int(trans[i,len(sol_coefs[0])-1]) in [1, -1]: 123 | linsolcoef = [int(trans[i,j])*int(trans[i,len(sol_coefs[0])-1]) for j in range(len(sol_coefs[0]))] 124 | logger.debug(f"linsolcoef found: {linsolcoef}") 125 | linsol = [] 126 | for sol_coef in sol_coefs: 127 | linsol.append(sum([ele*linsolcoef[idx] for idx, ele in enumerate(sol_coef)])) 128 | return [linsol] 129 | return [] 130 | 131 | 132 | def solve_ZZ_symbolic_linear_near_bounds_internal(sol_coefs, bounds, **kwds): 133 | # solve X_i = sum( (a_ij/b_ij)*v_j for j) for (a_ij/b_ij) in sol_coefs, |X_i| < bounds[i] (v_j: variable) 134 | 135 | if 'lllopt_symbolic_linear' not in kwds: 136 | kwds['lllopt_symbolic_linear'] = {'algorithm':FPLLL_BKZ} 137 | lllopt_symbolic_linear = kwds['lllopt_symbolic_linear'] 138 | 139 | ## for scaling lattice value 140 | mult = prod(bounds) 141 | 142 | linsollst = [] 143 | for signs in itertools_product([1, -1], repeat=len(bounds)): 144 | targetpnt = [signs[i] * bounds[i] for i in range(len(bounds))] 145 | 146 | ## construct equation (row vectors: [v_j for j] + [X_i for i] + [1]) 147 | matele = [] 148 | for i, sol_coef in enumerate(sol_coefs): 149 | denom = 1 150 | for sol_coef_ele in sol_coef: 151 | denom = LCM(denom, sol_coef_ele.denominator()) 152 | for sol_coef_ele in sol_coef: 153 | matele.append(ZZ(sol_coef_ele * denom * mult)) 154 | matele += [0]*i + [-denom * mult] + [0]*(len(bounds)-i-1) 155 | 156 | ## constrain to bounds (|X_i| < bounds[i]) 157 | for idx, pntele in enumerate(targetpnt): 158 | matele += [0]*(len(sol_coefs[0])-1) + [(mult//bounds[idx]) * -pntele] + [0]*idx + [mult//bounds[idx]] + [0]*(len(bounds)-idx-1) 159 | 160 | ## constrain to const (kannan embedding) 161 | matele += [0]*(len(sol_coefs[0])-1) + [mult] + [0]*len(bounds) 162 | 163 | ## BKZ (assume the number of variables are small) 164 | mat = matrix(ZZ, len(sol_coefs)+len(bounds)+1, len(sol_coefs[0])+len(bounds), matele) 165 | logger.debug(f"start LLL for solve_ZZ_symbolic_linear_internal") 166 | mattrans = mat.transpose() 167 | lll, trans = do_lattice_reduction(mattrans, lllopt_symbolic_linear) 168 | logger.debug(f"end LLL") 169 | 170 | ## search solution 171 | for i in range(trans.nrows()): 172 | if all([lll[i, j] == 0 for j in range(len(sol_coefs))]): 173 | if int(trans[i,len(sol_coefs[0])-1]) in [1, -1]: 174 | linsolcoef = [int(trans[i,j])*int(trans[i,len(sol_coefs[0])-1]) for j in range(len(sol_coefs[0]))] 175 | logger.debug(f"linsolcoef found: {linsolcoef}") 176 | linsol = [] 177 | for sol_coef in sol_coefs: 178 | linsol.append(sum([ele*linsolcoef[idx] for idx, ele in enumerate(sol_coef)])) 179 | linsollst.append(linsol) 180 | return linsollst 181 | 182 | 183 | def solve_root_triangulate(pollst, bounds, **kwds): 184 | logger.info("start solve_root_triangulate") 185 | st = time.time() 186 | 187 | if 'symbolic_linear_algorithm' not in kwds: 188 | kwds['symbolic_linear_algorithm'] = LINEAR_NEAR_BOUNDS 189 | 190 | polrng_QQ = pollst[0].change_ring(QQ).parent().change_ring(order='lex') 191 | vars_QQ = polrng_QQ.gens() 192 | G = Sequence(pollst, polrng_QQ).groebner_basis() 193 | if len(G) == 0: 194 | return [] 195 | 196 | symbolic_vars = [var(G_var) for G_var in G[0].parent().gens()] 197 | try: 198 | sols = solve([G_ele(*symbolic_vars) for G_ele in G], symbolic_vars, solution_dict=True) 199 | except: 200 | traceback.print_exc() 201 | return None 202 | 203 | logger.debug(f"found sol on triangulate: {sols}") 204 | 205 | result = [] 206 | # solve method returns parametrized solution. We treat only linear equation 207 | # TODO: use solver for more general integer equations (such as diophautus solver, integer programming solver, etc.) 208 | for sol in sols: 209 | sol_args = set() 210 | for symbolic_var in symbolic_vars: 211 | sol_var = sol[symbolic_var] 212 | sol_args = sol_args.union(set(sol_var.args())) 213 | 214 | sol_args = list(sol_args) 215 | sol_coefs = [] 216 | for symbolic_var in symbolic_vars: 217 | sol_var = sol[symbolic_var] 218 | sol_coefs_ele = [] 219 | for sol_arg in sol_args: 220 | if sol_var.is_polynomial(sol_arg) == False: 221 | logger.warning("cannot deal with non-polynomial equation") 222 | return None 223 | if sol_var.degree(sol_arg) > 1: 224 | logger.warning("cannot deal with high degree equation") 225 | return None 226 | sol_var_coef_arg = sol_var.coefficient(sol_arg) 227 | if sol_var_coef_arg not in QQ: 228 | logger.warning("cannot deal with multivariate non-linear equation") 229 | return None 230 | sol_coefs_ele.append(QQ(sol_var_coef_arg)) 231 | # constant term 232 | const = sol_var.subs({sol_arg: 0 for sol_arg in sol_args}) 233 | if const not in QQ: 234 | return None 235 | sol_coefs_ele.append(const) 236 | 237 | sol_coefs.append(sol_coefs_ele) 238 | 239 | if kwds['symbolic_linear_algorithm'] == LINEAR_NEAR_BOUNDS: 240 | ZZsol = solve_ZZ_symbolic_linear_near_bounds_internal(sol_coefs, bounds, **kwds) 241 | elif kwds['symbolic_linear_algorithm'] == LINEAR_SIMPLE: 242 | ZZsol = solve_ZZ_symbolic_linear_internal(sol_coefs, bounds, **kwds) 243 | else: 244 | ZZsol = [] 245 | 246 | result += ZZsol 247 | 248 | ed = time.time() 249 | logger.info("end solve_root_triangulate. elapsed %f", ed-st) 250 | # cleanup duplicated result 251 | return [list(eleele) for eleele in list(set([tuple(ele) for ele in result]))] 252 | 253 | 254 | def solve_root_jacobian_newton_internal(pollst, startpnt, **kwds): 255 | if 'maxiternum' not in kwds: 256 | kwds['maxiternum'] = 1024 257 | maxiternum = kwds['maxiternum'] 258 | 259 | # NOTE: Newton method's complexity is larger than BFGS, but for small variables Newton method converges soon. 260 | pollst_Q = Sequence(pollst, pollst[0].parent().change_ring(QQ)) 261 | vars_pol = pollst_Q[0].parent().gens() 262 | jac = jacobian(pollst_Q, vars_pol) 263 | 264 | if all([ele == 0 for ele in startpnt]): 265 | # just for prepnt != pnt 266 | prepnt = {vars_pol[i]: 1 for i in range(len(vars_pol))} 267 | else: 268 | prepnt = {vars_pol[i]: 0 for i in range(len(vars_pol))} 269 | pnt = {vars_pol[i]: startpnt[i] for i in range(len(vars_pol))} 270 | 271 | iternum = 0 272 | while True: 273 | if iternum >= maxiternum: 274 | logger.warning("failed. maybe, going wrong way.") 275 | return None 276 | 277 | evalpollst = [(pollst_Q[i].subs(pnt)) for i in range(len(pollst_Q))] 278 | if all([int(ele) == 0 for ele in evalpollst]): 279 | break 280 | jac_eval = jac.subs(pnt) 281 | evalpolvec = vector(QQ, len(evalpollst), evalpollst) 282 | try: 283 | pnt_diff_vec = jac_eval.solve_right(evalpolvec) 284 | except: 285 | logger.warning("pnt_diff comp failed.") 286 | return None 287 | 288 | prepnt = {key:value for key,value in prepnt.items()} 289 | pnt = {vars_pol[i]: round(QQ(pnt[vars_pol[i]] - pnt_diff_vec[i])) for i in range(len(pollst_Q))} 290 | 291 | if all([prepnt[vars_pol[i]] == pnt[vars_pol[i]] for i in range(len(vars_pol))]): 292 | logger.warning("point update failed. (converged local sol)") 293 | return None 294 | prepnt = {key:value for key,value in pnt.items()} 295 | iternum += 1 296 | return [int(pnt[vars_pol[i]]) for i in range(len(vars_pol))] 297 | 298 | 299 | def solve_root_jacobian_newton(pollst, bounds, **kwds): 300 | logger.info("start solve_root_jacobian newton") 301 | st = time.time() 302 | 303 | pollst_local = pollst[:] 304 | vars_pol = pollst[0].parent().gens() 305 | 306 | # not applicable to non-determined system 307 | if len(vars_pol) > len(pollst): 308 | return [] 309 | 310 | # set default options if not set 311 | if 'select_subpollst_loopnum' not in kwds: 312 | kwds['select_subpollst_loopnum'] = 10 313 | 314 | if 'search_near_positive_bounds_only' not in kwds: 315 | kwds['search_near_positive_bounds_only'] = False 316 | 317 | if 'filter_small_solution_minbound' not in kwds: 318 | kwds['filter_small_solution_minbound'] = 2**16 319 | 320 | for _ in range(kwds['select_subpollst_loopnum']): 321 | # pollst is not always algebraically independent, 322 | # so just randomly choose wishing to obtain an algebraically independent set 323 | random_shuffle(pollst_local) 324 | 325 | if kwds['search_near_positive_bounds_only']: 326 | signsiter = [[1 for _ in range(len(vars_pol))], ] 327 | else: 328 | signsiter = itertools_product([1, -1], repeat=len(vars_pol)) 329 | 330 | for signs in signsiter: 331 | startpnt = [signs[i] * bounds[i] for i in range(len(vars_pol))] 332 | result = solve_root_jacobian_newton_internal(pollst_local[:len(vars_pol)], startpnt, **kwds) 333 | # filter too much small solution 334 | if result is not None: 335 | if all([abs(ele) < kwds['filter_small_solution_minbound'] for ele in result]): 336 | continue 337 | ed = time.time() 338 | logger.info("end solve_root_jacobian newton. elapsed %f", ed-st) 339 | return [result] 340 | 341 | 342 | def _solve_root_GF_smallp(pollst, smallp, monomial_order_for_variety): 343 | Fsmallp = GF(smallp) 344 | polrng_Fsmallp = pollst[0].change_ring(Fsmallp).parent().change_ring(order=monomial_order_for_variety) 345 | vars_Fsmallp = polrng_Fsmallp.gens() 346 | fieldpolys = [varele**smallp - varele for varele in vars_Fsmallp] 347 | pollst_Fsmallp = [polrng_Fsmallp(ele) for ele in pollst] 348 | G = pollst_Fsmallp[0].parent().ideal(pollst_Fsmallp + fieldpolys).groebner_basis() 349 | rt_ = G.ideal().variety() 350 | rt = [[int(ele[v].lift()) for v in vars_Fsmallp] for ele in rt_] 351 | return rt 352 | 353 | 354 | def solve_root_hensel_smallp(pollst, bounds, smallp, **kwds): 355 | logger.info("start solve_root_hensel") 356 | st = time.time() 357 | 358 | if 'maxcands' not in kwds: 359 | kwds['maxcands'] = 800 360 | if 'monomial_order_for_variety' not in kwds: 361 | kwds['monomial_order_for_variety'] = 'degrevlex' 362 | maxcands = kwds['maxcands'] 363 | monomial_order_for_variety = kwds['monomial_order_for_variety'] 364 | 365 | vars_ZZ = pollst[0].parent().gens() 366 | smallp_exp_max = max([int(log(ele, smallp)+0.5) for ele in bounds]) + 1 367 | # firstly, compute low order 368 | rt_lows = _solve_root_GF_smallp(pollst, smallp, monomial_order_for_variety) 369 | for smallp_exp in range(1, smallp_exp_max+1, 1): 370 | cur_rt_low = [] 371 | for rt_low in rt_lows: 372 | evalpnt = {vars_ZZ[i]:(smallp**smallp_exp)*vars_ZZ[i]+rt_low[i] for i in range(len(vars_ZZ))} 373 | nextpollst = [pol.subs(evalpnt)/(smallp**smallp_exp) for pol in pollst] 374 | rt_up = _solve_root_GF_smallp(nextpollst, smallp, monomial_order_for_variety) 375 | cur_rt_low += [tuple([smallp**smallp_exp*rt_upele[i] + rt_low[i] for i in range(len(rt_low))]) for rt_upele in rt_up] 376 | rt_lows = list(set(cur_rt_low)) 377 | if len(rt_lows) >= maxcands: 378 | logger.warning("too much root candidates found") 379 | return None 380 | 381 | result = [] 382 | for rt in rt_lows: 383 | rtele = [[ele, ele - smallp**(smallp_exp_max+1)][ele >= smallp**smallp_exp_max] for ele in rt] 384 | if any([pollst[i].subs({v: int(rtele[i]) for i, v in enumerate(vars_ZZ)}) != 0 for i in range(len(pollst))]): 385 | continue 386 | if all([abs(int(rtele[i])) < bounds[i] for i in range(len(rtele))]): 387 | result.append(rtele) 388 | 389 | ed = time.time() 390 | logger.info("end solve_root_hensel. elapsed %f", ed-st) 391 | return result 392 | 393 | 394 | def solve_root_hensel(pollst, bounds, **kwds): 395 | if 'smallps' not in kwds: 396 | kwds['smallps'] = (2, 3, 5) 397 | 398 | for smallp in kwds['smallps']: 399 | result = solve_root_hensel_smallp(pollst, bounds, smallp, **kwds) 400 | if result != [] and result is not None: 401 | return result 402 | return None 403 | 404 | 405 | ## wrapper function 406 | def rootfind_ZZ(pollst, bounds, **kwds): 407 | vars_pol = pollst[0].parent().gens() 408 | if len(vars_pol) != len(bounds): 409 | raise ValueError("vars len is invalid (on rootfind_ZZ)") 410 | 411 | # Note: match-case statement introduced on python3.10, but not used for backward compati 412 | if len(vars_pol) == 1: 413 | return solve_root_onevariable(pollst, bounds, **kwds) 414 | else: 415 | if 'algorithms' not in kwds: 416 | ## default: numeric, hensel, triangulate with groebner 417 | ### numeric(jacobian): in most cases, practical, but sometimes it goes rabbit holes 418 | ### hensel: fast if the number of solutions mod smallp**a are small. in not case, cannot find solution 419 | ### triangulate: slow, but sometimes solve when above methods does not work (it handles some special equations) 420 | ### groebner: slow and simple 421 | kwds['algorithms'] = (JACOBIAN, HENSEL, TRIANGULATE) 422 | if isinstance(kwds['algorithms'], int): 423 | kwds['algorithms'] = (kwds['algorithms'], ) 424 | if not isinstance(kwds['algorithms'], (list, tuple)): 425 | raise ValueError("algorithms parameter is invalid (on rootfind_ZZ)") 426 | 427 | for algorithm in kwds['algorithms']: 428 | result = rootfindZZ_algorithm_dict[algorithm](pollst, bounds, **kwds) 429 | if result != [] and result is not None: 430 | return result 431 | 432 | # not found by any methods 433 | return None 434 | 435 | 436 | rootfindZZ_algorithm_dict = { 437 | JACOBIAN: solve_root_jacobian_newton, 438 | HENSEL: solve_root_hensel, 439 | TRIANGULATE: solve_root_triangulate, 440 | GROEBNER: solve_root_groebner 441 | } 442 | 443 | rootfindZZ_algorithm_str = { 444 | JACOBIAN: 'JACOBIAN', 445 | HENSEL: 'HENSEL', 446 | TRIANGULATE: 'TRIANGULATE', 447 | GROEBNER: 'GROEBNER' 448 | } 449 | -------------------------------------------------------------------------------- /lll.py: -------------------------------------------------------------------------------- 1 | from sage.all import * 2 | 3 | from typing import Tuple, List 4 | from subprocess import run as subprocess_run 5 | from re import sub as re_sub 6 | import os 7 | import time 8 | import traceback 9 | 10 | # NTL (some code is copied from https://github.com/sagemath/sage/blob/develop/src/sage/matrix/matrix_integer_dense.pyx) 11 | import sage.libs.ntl.all 12 | import sage.libs.ntl.ntl_mat_ZZ 13 | ntl_ZZ = sage.libs.ntl.all.ZZ 14 | ntl_mat = lambda A_: sage.libs.ntl.ntl_mat_ZZ.ntl_mat_ZZ(A_.nrows(), A_.ncols(), [ntl_ZZ(z) for z in A_.list()]) 15 | 16 | from fpylll import IntegerMatrix, GSO, Pruning, Enumeration 17 | 18 | from logger import * 19 | 20 | 21 | _coppersmith_dir = os.path.dirname(__file__) 22 | _fplll_path = os.path.join(_coppersmith_dir, 'fplll', 'fplll') # /usr/bin 23 | _flatter_path = os.path.join(_coppersmith_dir, 'flatter', 'build', 'bin') # /usr/bin 24 | 25 | fplll_path = os.environ.get('COPPERSMITHFPLLLPATH', _fplll_path) 26 | flatter_path = os.environ.get('COPPERSMITHFLATTERPATH', _flatter_path) 27 | 28 | # algorithm 29 | FPLLL = 0 30 | FPLLL_BKZ = 1 31 | FLATTER = 2 32 | NTL = 3 33 | NTL_BKZ = 4 34 | 35 | 36 | # fplll option 37 | ## fplll_version ('fast' is only double, 'proved' cannot be used with early reduction) 38 | WRAPPER = 'wrapper' 39 | HEURISTIC = 'heuristic' 40 | 41 | # on flatter, call kermat on pari 42 | pari.allocatemem(1024*1024*1024) 43 | 44 | 45 | def _from_sagematrix_to_fplllmatrix(mat: matrix) -> str: 46 | return '[' + re_sub( 47 | r'\[ ', 48 | r'[', 49 | re_sub(r' +', r' ', str(mat)) 50 | ) + ']' 51 | 52 | 53 | def _fplllmatrix_to_sagematrix(matrixstr: str) -> matrix: 54 | matlist = eval(matrixstr.replace(' ', ',').replace('\n', ',')) 55 | return matrix(ZZ, matlist) 56 | 57 | 58 | def _transformation_matrix(mat, lllmat, use_pari_matsol=False): 59 | # pari.matker() does not assure smallest kernel in Z (seems not call hermite normal form) 60 | # Sage kernel calls hermite normal form 61 | # 62 | # for computing ZZ transformation, use pari.matker, pari.matsolvemod 63 | # assume first kerdim vectors for lllmat are zero vector 64 | # 65 | # anyway, transformation computation after LLL/BKZ is slow. 66 | # instead, use builtin transformation computation on LLL/BKZ package 67 | 68 | if use_pari_matsol: 69 | mat_pari = pari.matrix(mat.nrows(), mat.ncols(), mat.list()) 70 | ker_pari_t = pari.matker(pari.mattranspose(mat_pari), 1) 71 | kerdim = len(ker_pari_t) 72 | if kerdim == 0: 73 | # empty matrix 74 | trans = matrix(ZZ, 0, mat.nrows()) 75 | else: 76 | trans = matrix(ZZ, pari.mattranspose(ker_pari_t).Col().list()) 77 | 78 | mat_pari = pari.matrix(mat.nrows(), mat.ncols(), mat.list()) 79 | for i in range(kerdim, lllmat.nrows(), 1): 80 | lllmat_pari = pari.vector(lllmat.ncols(), lllmat[i].list()) 81 | trans_pari_t = pari.matsolvemod( 82 | pari.mattranspose(mat_pari), 0, pari.mattranspose(lllmat_pari) 83 | ) 84 | transele = matrix(ZZ, trans_pari_t.mattranspose().Col().list()) 85 | trans = trans.stack(transele) 86 | else: 87 | trans = mat.kernel().matrix() 88 | kerdim = trans.nrows() 89 | 90 | for i in range(kerdim, lllmat.nrows(), 1): 91 | transele = mat.solve_left(lllmat[i]) 92 | trans = trans.stack(transele) 93 | 94 | return trans 95 | 96 | 97 | def _xgcd_list(intlst: List[int]) -> Tuple[int, List[int]]: 98 | """ 99 | extended gcd algorithm for a_0,...,a_k 100 | input: [a_0, ..., a_k] 101 | output: d_, [b_0, ..., b_k] s.t. gcd(a_0,...,a_k) = d_, sum(a_i*b_i for i) = d_ 102 | """ 103 | 104 | if len(intlst) == 1: 105 | if intlst[0] >= 0: 106 | return intlst[0], [1] 107 | else: 108 | return -intlst[0], [-1] 109 | 110 | d, a, b = xgcd(intlst[0], intlst[1]) 111 | 112 | curgcd = d 113 | curlst = [a, b] 114 | for i in range(2, len(intlst)): 115 | d, a, b = xgcd(curgcd, intlst[i]) 116 | curlst = list(map(lambda x: x*a, curlst)) + [b] 117 | curgcd = d 118 | return curgcd, curlst 119 | 120 | 121 | def do_LLL_fplll(mat: matrix, **kwds) -> Tuple[matrix, matrix]: 122 | if 'transformation' not in kwds: 123 | kwds['transformation'] = True 124 | if 'use_siegel' not in kwds: 125 | kwds['use_siegel'] = True 126 | if 'fplll_version' not in kwds: 127 | kwds['fplll_version'] = WRAPPER 128 | if 'early_reduction' not in kwds: 129 | kwds['early_reduction'] = True 130 | transformation = kwds['transformation'] 131 | use_siegel = kwds['use_siegel'] 132 | fplll_version = kwds['fplll_version'] 133 | early_reduction = kwds['early_reduction'] 134 | 135 | matstr = _from_sagematrix_to_fplllmatrix(mat) 136 | if early_reduction: 137 | result = subprocess_run( 138 | [os.path.join(fplll_path, 'fplll'), '-l', str(1-int(use_siegel)), '-m', fplll_version, '-y', '-of', 'u'], 139 | input=matstr.encode(), cwd=fplll_path, capture_output=True 140 | ) 141 | else: 142 | result = subprocess_run( 143 | [os.path.join(fplll_path, 'fplll'), '-l', str(1-int(use_siegel)), '-m', fplll_version, '-of', 'u'], 144 | input=matstr.encode(), cwd=fplll_path, capture_output=True 145 | ) 146 | if result.returncode != 0: 147 | print(result.stderr) 148 | raise ValueError(f"LLL failed with return code {result.returncode}") 149 | 150 | trans = _fplllmatrix_to_sagematrix(result.stdout.decode().strip()) 151 | lllmat = trans * mat 152 | 153 | if not(transformation): 154 | trans = None 155 | 156 | return lllmat, trans 157 | 158 | 159 | def do_BKZ_fplll(mat: matrix, **kwds) -> Tuple[matrix, matrix]: 160 | if 'transformation' not in kwds: 161 | kwds['transformation'] = True 162 | if 'blocksize' not in kwds: 163 | kwds['blocksize'] = 10 164 | if 'bkzautoabort' not in kwds: 165 | kwds['bkzautoabort'] = True 166 | transformation = kwds['transformation'] 167 | blocksize = kwds['blocksize'] 168 | bkzautoabort = kwds['bkzautoabort'] 169 | 170 | matstr = _from_sagematrix_to_fplllmatrix(mat) 171 | if bkzautoabort: 172 | result = subprocess_run( 173 | [os.path.join(fplll_path, 'fplll'), '-a', 'bkz', '-b', str(blocksize), '-bkzautoabort', '-of', 'u'], 174 | input=matstr.encode(), cwd=fplll_path, capture_output=True 175 | ) 176 | else: 177 | result = subprocess_run( 178 | [os.path.join(fplll_path, 'fplll'), '-a', 'bkz', '-b', str(blocksize), '-of', 'u'], 179 | input=matstr.encode(), cwd=fplll_path, capture_output=True 180 | ) 181 | if result.returncode != 0: 182 | print(result.stderr) 183 | raise ValueError(f"LLL failed with return code {result.returncode}") 184 | 185 | trans = _fplllmatrix_to_sagematrix(result.stdout.decode().strip()) 186 | lllmat = trans * mat 187 | 188 | if not(transformation): 189 | trans = None 190 | 191 | return lllmat, trans 192 | 193 | 194 | def do_LLL_flatter(mat: matrix, **kwds) -> Tuple[matrix, matrix]: 195 | if 'transformation' not in kwds: 196 | kwds['transformation'] = True 197 | if 'use_pari_kernel' not in kwds: 198 | kwds['use_pari_kernel'] = True 199 | if 'use_pari_matsol' not in kwds: 200 | kwds['use_pari_matsol'] = False 201 | transformation = kwds['transformation'] 202 | use_pari_kernel = kwds['use_pari_kernel'] 203 | use_pari_matsol = kwds['use_pari_matsol'] 204 | 205 | kerproc_st = time.time() 206 | 207 | if mat == zero_matrix(ZZ, mat.nrows(), mat.ncols()): 208 | return mat, identity_matrix(ZZ, mat.nrows()) 209 | 210 | # sage has integer_kernel(), but somehow slow. instead using pari.matker 211 | if use_pari_kernel: 212 | mat_pari = pari.matrix(mat.nrows(), mat.ncols(), mat.list()) 213 | ker_pari_t = pari.matker(mat_pari.mattranspose(), 1) 214 | ker = matrix(ZZ, ker_pari_t.mattranspose().Col().list()) 215 | else: 216 | ker = mat.kernel().matrix() 217 | 218 | kerdim = ker.nrows() 219 | matrow = mat.nrows() 220 | col = mat.ncols() 221 | if kerdim == matrow: # full kernel 222 | return zero_matrix(ZZ, matrow, col), ker 223 | if kerdim == 0: 224 | Hsub = mat 225 | U = identity_matrix(ZZ, matrow) 226 | else: 227 | # heuristic construction for unimodular matrix which maps zero vectors on kernel 228 | # searching unimodular matrix can be done by HNF 229 | # (echeron_form(algorithm='pari') calls mathnf()), 230 | # but it is slow and produces big elements 231 | # 232 | # instead, searching determinant of submatrix = 1/-1, 233 | # then the determinant of whole unimodular matrix is det(submatrix)*(-1)^j 234 | # assume kernel has good property for gcd (gcd of some row elements might be 1) 235 | found_choice = False 236 | ker_submat_rows = tuple(range(kerdim)) 237 | ker_submat_cols = [] 238 | pivot = matrow - 1 239 | # search submatrix of kernel assuming last column vectors are triangulate 240 | while len(ker_submat_cols) < kerdim: 241 | if ker[ker_submat_rows, tuple([pivot])] != zero_matrix(ZZ, kerdim, 1): 242 | ker_submat_cols.append(pivot) 243 | pivot -= 1 244 | ker_submat_cols = tuple(sorted(ker_submat_cols)) 245 | ker_last_det = int(ker[ker_submat_rows, ker_submat_cols].determinant()) 246 | if ker_last_det == 0: 247 | raise ValueError("no unimodular matrix found (cause ker_last_det=0)") 248 | for choice in range(pivot, -1, -1): 249 | # gcd check 250 | gcd_row = ker_last_det 251 | for i in range(kerdim): 252 | gcd_row = GCD(gcd_row, ker[i, choice]) 253 | if abs(gcd_row) != 1: 254 | continue 255 | 256 | # choice pivot: last columes for kernel are triangulated and small 257 | kersubidxes = [choice] + list(ker_submat_cols) 258 | detlst = [ker_last_det] 259 | for i in range(1, kerdim+1, 1): 260 | ker_submat_rows = tuple(range(kerdim)) 261 | ker_submat_cols = tuple(kersubidxes[:i] + kersubidxes[i+1:]) 262 | detlst.append(ker[ker_submat_rows, ker_submat_cols].determinant()) 263 | detlist_gcd, detlist_coef = _xgcd_list(detlst) 264 | if detlist_gcd == 1: 265 | found_choice = True 266 | break 267 | if not found_choice: 268 | continue 269 | detlist_coef = detlist_coef + [0] * ((kerdim + 1) - len(detlist_coef)) 270 | break 271 | if not found_choice: 272 | raise ValueError("no unimodular matrix found") 273 | U_top_vec = [0 for _ in range(matrow)] 274 | for i in range(kerdim+1): 275 | U_top_vec[kersubidxes[i]] = (-1)**i * detlist_coef[i] 276 | U_sub = matrix(ZZ, 1, matrow, U_top_vec) 277 | not_kersubidxes = sorted(list(set(list(range(matrow))) - set(kersubidxes))) 278 | for j in range(kerdim+1, matrow): 279 | onevec = [0 for _ in range(matrow)] 280 | onevec[not_kersubidxes[j-(kerdim+1)]] = 1 281 | U_sub = U_sub.stack(vector(ZZ, matrow, onevec)) 282 | Hsub = U_sub * mat 283 | U = ker.stack(U_sub) 284 | #assert abs(U.determinant()) == 1 285 | kerproc_ed = time.time() 286 | logger.info("processing kernel elapsed time: %f", kerproc_ed - kerproc_st) 287 | 288 | if Hsub.nrows() == 1: 289 | lllmat = Hsub 290 | else: 291 | matstr = _from_sagematrix_to_fplllmatrix(Hsub) 292 | result = subprocess_run( 293 | os.path.join(flatter_path, 'flatter'), 294 | input=matstr.encode(), cwd=flatter_path, capture_output=True 295 | ) 296 | if result.returncode != 0: 297 | print(result.stderr) 298 | raise ValueError(f"LLL failed with return code {result.returncode}") 299 | lllmat = _fplllmatrix_to_sagematrix(result.stdout.decode().strip()) 300 | 301 | if transformation: 302 | trans = _transformation_matrix(Hsub, lllmat, use_pari_matsol=use_pari_matsol) 303 | else: 304 | trans = None 305 | 306 | restrows = mat.nrows() - lllmat.nrows() 307 | final_lllmat = zero_matrix(ZZ, restrows, lllmat.ncols()).stack(lllmat) 308 | 309 | if transformation: 310 | middle_trans = identity_matrix(ZZ, restrows).augment(zero_matrix(ZZ, restrows, trans.ncols())).stack( 311 | zero_matrix(ZZ, trans.nrows(), restrows).augment(trans) 312 | ) 313 | final_trans = middle_trans * U 314 | #assert abs(final_trans.determinant()) == 1 315 | #assert final_trans * mat == final_lllmat 316 | else: 317 | final_trans = None 318 | 319 | return final_lllmat, final_trans 320 | 321 | 322 | def do_LLL_NTL(mat: matrix, **kwds) -> Tuple[matrix, matrix]: 323 | if 'transformation' not in kwds: 324 | kwds['transformation'] = True 325 | transformation = kwds['transformation'] 326 | 327 | delta_lll = ZZ(99)/ZZ(100) 328 | a_lll = delta_lll.numer() 329 | b_lll = delta_lll.denom() 330 | 331 | A = ntl_mat(mat) 332 | 333 | # TODO: support various floating point precision, and use_givens option 334 | r, det2, U = A.LLL(a_lll, b_lll, return_U=transformation) 335 | 336 | lllmat = matrix(ZZ, mat.nrows(), mat.ncols(), [ZZ(z) for z in A.list()]) 337 | if transformation: 338 | trans = matrix(ZZ, mat.nrows(), mat.nrows(), [ZZ(z) for z in U.list()]) 339 | else: 340 | trans = None 341 | 342 | return lllmat, trans 343 | 344 | 345 | def do_BKZ_NTL(mat: matrix, **kwds) -> Tuple[matrix, matrix]: 346 | if 'transformation' not in kwds: 347 | kwds['transformation'] = True 348 | if 'blocksize' not in kwds: 349 | kwds['blocksize'] = 10 350 | if 'prune' not in kwds: 351 | kwds['prune'] = 0 352 | transformation = kwds['transformation'] 353 | blocksize = kwds['blocksize'] 354 | prune = kwds['prune'] 355 | 356 | delta_lll = 0.99 357 | A = ntl_mat(mat) 358 | U = ntl_mat(identity_matrix(ZZ, A.nrows())) 359 | 360 | # TODO: support various floating point precision, and use_givens option 361 | r = A.BKZ_RR(U=U, delta=delta_lll, BlockSize=blocksize, prune=prune) 362 | 363 | lllmat = matrix(ZZ, mat.nrows(), mat.ncols(), [ZZ(z) for z in A.list()]) 364 | if transformation: 365 | trans = matrix(ZZ, mat.nrows(), mat.nrows(), [ZZ(z) for z in U.list()]) 366 | else: 367 | trans = None 368 | 369 | return lllmat, trans 370 | 371 | 372 | ## wrapper function 373 | def do_lattice_reduction(mat: matrix, **kwds) -> Tuple[matrix, matrix]: 374 | """ 375 | LLL/BKZ reduction 376 | input: (mat, algorithm, **kwds) 377 | - mat: target lattice representation matrix for LLL/BKZ reduction 378 | - algorithm: int value which specify which algorithm will be used 379 | (FPLLL, FPLLL_BKZ, FLATTER, NTL, NTL_BKZ) 380 | output: (lllmat, trans) 381 | - lllmat: LLL/BKZ reduced basis matrix (might include zero-vectors) 382 | - trans: transformation matrix s.t. lllmat = trans * mat 383 | """ 384 | if 'algorithm' not in kwds: 385 | kwds['algorithm'] = FLATTER 386 | algorithm = kwds['algorithm'] 387 | 388 | logger.info("size of mat for lattice reduction: (%d, %d)", int(mat.nrows()), int(mat.ncols())) 389 | logger.debug( 390 | "lattice reduction param: algorithm=%s, param=%s", 391 | LLL_algorithm_str[algorithm], str(kwds) 392 | ) 393 | logger.info("start lattice reduction") 394 | st = time.time() 395 | 396 | result = LLL_algorithm_dict[algorithm](mat, **kwds) 397 | 398 | ed = time.time() 399 | logger.info("end lattice reduction. elapsed %f", ed-st) 400 | 401 | return result 402 | 403 | 404 | def babai(mat: matrix, target: vector, algorithm: int = FLATTER, **kwds) -> Tuple[vector, vector]: 405 | """ 406 | Babai nearlest plain algorithm for solving CVP 407 | input: (mat, target, **kwds) 408 | - mat: lattice representation matrix for LLL/BKZ reduction 409 | - target: target integer vector for solving close point in lattice 410 | - algorithm: int value which specify which algorithm will be used 411 | (FPLLL, FPLLL_BKZ, FLATTER, NTL, NTL_BKZ) 412 | output: (diff, trans) 413 | - diff: subtract of target from lattice_point which is close for target 414 | - trans: transformation matrix s.t. lattice_point = trans * mat 415 | """ 416 | kwds['transformation'] = True 417 | lll, trans = do_lattice_reduction(mat, algorithm, **kwds) 418 | # gram-schmidt process is slow. use solve_left in QQ 419 | sol_QQ = (lll.change_ring(QQ)).solve_left((target.change_ring(QQ))) 420 | sol_approx_ZZ_lst = [ZZ(QQ(sol_QQ_ele).round()) for sol_QQ_ele in sol_QQ.list()] 421 | sol_approx_ZZ = vector(ZZ, len(sol_approx_ZZ_lst), sol_approx_ZZ_lst) 422 | return target - sol_approx_ZZ * lll, sol_approx_ZZ * trans 423 | 424 | 425 | def enumeration(mat: matrix, bound: int, target: vector = None, algorithm: int = FLATTER, **kwds): 426 | """ 427 | Enumeration (SVP or CVP) 428 | input: (mat, target, **kwds) 429 | - mat: lattice representation matrix for LLL/BKZ reduction 430 | - target: None for SVP, target integer vector for solving close point in lattice for CVP 431 | bound: expected norm size estimation as bound = sqrt((L2-norm**2) / size) 432 | - algorithm: int value which specify which algorithm will be used 433 | (FPLLL, FPLLL_BKZ, FLATTER, NTL, NTL_BKZ) 434 | output: enumeration generator 435 | """ 436 | kwds['transformation'] = True 437 | lll, trans = do_lattice_reduction(mat, algorithm, **kwds) 438 | lllele = [] 439 | for i in range(0, lll.nrows()): 440 | lllele.append(lll[i].list()) 441 | lll_fpylll = IntegerMatrix.from_matrix(lllele) 442 | MG = GSO.Mat(lll_fpylll) 443 | MG.update_gso() 444 | enum = Enumeration(MG) 445 | size = lll.ncols() 446 | answers = enum.enumerate(0, size, size * (bound ** 2), 0, target=target, pruning=None) 447 | for _, s in answers: 448 | v = vector(ZZ, size, list(map(int, s))) 449 | enumresult = v * trans 450 | yield (enumresult * mat, enumresult) 451 | 452 | 453 | def test(): 454 | testlst = [ 455 | ("zerodim", [[0,0,0]]), 456 | ("onedim", [[1,2,3]]), 457 | ("twodim_indep", [[1,2,3],[4,5,6]]), 458 | ("twodim_dep", [[1,2,3],[2,4,6]]), 459 | ("threedim_indep", [[1,2,3],[4,5,6],[7,8,9]]), 460 | ("threedim_one_dep", [[1,2,3],[2,4,6],[8,9,10]]), 461 | ("threedim_two_dep", [[1,2,3],[2,4,6],[3,6,9]]), 462 | ("overdim", [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]), 463 | ("overdim_onedep", [[1,2,3],[4,5,6],[3,6,9],[5,6,7]]), 464 | ("multiple_2_ker", [[-2,-4,-6],[1,2,3],[3,6,9]]), 465 | ] 466 | 467 | for LLL_algorithm in range(5): 468 | print(f"LLL_algorithm: {LLL_algorithm_str[LLL_algorithm]}") 469 | for testlstele in testlst: 470 | curmat = matrix(ZZ, testlstele[1]) 471 | try: 472 | if LLL_algorithm == FLATTER: 473 | lll, trans = LLL_algorithm_dict[LLL_algorithm](curmat, **{'use_pari_kernel':False}) 474 | #lll, trans = LLL_algorithm_dict[LLL_algorithm](curmat, use_pari_kernel=True) 475 | else: 476 | lll, trans = LLL_algorithm_dict[LLL_algorithm](curmat, **{}) 477 | except: 478 | traceback.print_exc() 479 | continue 480 | 481 | print(f"test {testlstele[1]}: {(trans * curmat == lll, abs(trans.determinant()) == 1)}") 482 | print((lll.rows(), trans.rows())) 483 | print("") 484 | 485 | 486 | LLL_algorithm_dict = { 487 | FLATTER: do_LLL_flatter, 488 | FPLLL: do_LLL_fplll, FPLLL_BKZ: do_BKZ_fplll, 489 | NTL: do_LLL_NTL, NTL_BKZ: do_BKZ_NTL 490 | } 491 | 492 | LLL_algorithm_str = { 493 | FLATTER: 'FLATTER', 494 | FPLLL: 'FPLLL', FPLLL_BKZ: 'FPLLL_BKZ', 495 | NTL: 'NTL', NTL_BKZ: 'NTL_BKZ' 496 | } 497 | 498 | 499 | if __name__ == '__main__': 500 | test() 501 | -------------------------------------------------------------------------------- /somenote_on_hackmd.md: -------------------------------------------------------------------------------- 1 | # Why we could not solve chronophobia... Analysis for Coppersmith method more 2 | 3 | [toc] 4 | 5 | ## What I Learned 6 | 7 | - It would be better trying many things, instead of detailed analysis, on CTF 8 | - Simple strategy is good, but tuning parameters not good. Applying past genius works is definitely better. 9 | 10 | ## chronophobia (from idekCTF 2022) 11 | 12 | ```python 13 | #!/usr/bin/env python3 14 | 15 | from Crypto.Util.number import * 16 | import random 17 | import signal 18 | 19 | class PoW(): 20 | 21 | def __init__(self, kbits, L): 22 | 23 | self.kbits = kbits 24 | self.L = L 25 | 26 | self.banner() 27 | self.gen() 28 | self.loop(1337) 29 | 30 | def banner(self): 31 | 32 | print("===================================") 33 | print("=== Welcome to idek PoW Service ===") 34 | print("===================================") 35 | print("") 36 | 37 | def menu(self): 38 | 39 | print("") 40 | print("[1] Broken Oracle") 41 | print("[2] Verify") 42 | print("[3] Exit") 43 | 44 | op = int(input(">>> ")) 45 | return op 46 | 47 | def loop(self, n): 48 | 49 | for _ in range(n): 50 | 51 | op = self.menu() 52 | if op == 1: 53 | self.broken_oracle() 54 | elif op == 2: 55 | self.verify() 56 | elif op == 3: 57 | print("Bye!") 58 | break 59 | 60 | def gen(self): 61 | 62 | self.p = getPrime(self.kbits) 63 | self.q = getPrime(self.kbits) 64 | 65 | self.n = self.p * self.q 66 | self.phi = (self.p - 1) * (self.q - 1) 67 | 68 | t = random.randint(0, self.n-1) 69 | print(f"Here is your random token: {t}") 70 | print(f"The public modulus is: {self.n}") 71 | 72 | self.d = random.randint(128, 256) 73 | print(f"Do 2^{self.d} times exponentiation to get the valid ticket t^(2^(2^{self.d})) % n!") 74 | 75 | self.r = pow(2, 1 << self.d, self.phi) 76 | self.ans = pow(t, self.r, self.n) 77 | 78 | return 79 | 80 | def broken_oracle(self): 81 | 82 | u = int(input("Tell me the token. ")) 83 | ans = pow(u, self.r, self.n) 84 | inp = int(input("What is your calculation? ")) 85 | if ans == inp: 86 | print("Your are correct!") 87 | else: 88 | print(f"Nope, the ans is {str(ans)[:self.L]}... ({len(str(ans)[self.L:])} remain digits)") 89 | 90 | return 91 | 92 | def verify(self): 93 | 94 | inp = int(input(f"Give me the ticket. ")) 95 | 96 | if inp == self.ans: 97 | print("Good :>") 98 | with open("flag.txt", "rb") as f: 99 | print(f.read()) 100 | else: 101 | print("Nope :<") 102 | 103 | if __name__ == '__main__': 104 | 105 | signal.alarm(120) 106 | service = PoW(512, 200) 107 | ``` 108 | 109 | We are given token `t`, exponent `d`, and modulus `n=p*q`. The goal is to find `t^r % n` for `r=2^(2^d) % phi(n)` within 2 minutes. The assumption of Wesolowski's verifiable delay function ([Efficient verifiable delay functions](https://eprint.iacr.org/2018/623.pdf)) is that this type of computation is slow if factorization of `n` is unknown. So the author may add an extra interface. We are given weird oracle "broken_oracle", which outputs most significant `L` digits for `u^r % n` given user input `u`. 110 | 111 | ## Our Strategy on CTF 112 | 113 | I saw the challenge after having nice progress by @soon_haari. His idea is: 114 | 115 | 1. obtain `u1=broken_token(t)` 116 | 2. obtain `u2=broken_token(t^2 % n)` 117 | 3. find rest of digits of `u` by LLL/BKZ 118 | 119 | If we assume that `u=u1*(10^Ludown)+x`, `u^2 % n=u2*(10^Lu2down)+y`, then 120 | 121 | $$ 122 | (u1\cdot (10^{\text{Ludown}})+x)^2 - (u2\cdot (10^{\text{Lu2down}})+y) = 0 \pmod n 123 | $$ 124 | 125 | `x,y` are small ($\le 10^\text{Ludown}, 10^\text{Lu2down}$), we may expect LLL could solve the challenge. 126 | It is nice, but it only works `L=250`. In our setting, we have to solve it for `L=200`. 127 | 128 | Then, I started tuning lattice, but it failed. And I tried to apply another idea: using `u^-1 % n` instead of `u^2 % n`. Even though it solved it for `L=210`, but it did not work for `L=200` ... 129 | 130 | After that, I changed mind. I assume that these strategy does not work cause some high degree terms (`x^2,x*y` etc.) are included. So I determined to apply another method: **Coppersmith method**. 131 | 132 | Coppersmith method is general framework for solving a polynomial equation over integer, $\mod{N}$ (not on finite field), and $\mod{p}$ for unknown modulus $p\mid N$. On Sagemath, [small_roots](https://doc.sagemath.org/html/en/reference/polynomial_rings/sage/rings/polynomial/polynomial_modn_dense_ntl.html#sage.rings.polynomial.polynomial_modn_dense_ntl.small_roots) method is implemented, but it only works for 1 variable polynomial. But we have [alternative experimental extension](https://github.com/defund/coppersmith) by @defund. Then, I wrote just like the following code. (I clean up after ctf, but the essence is same.) 133 | 134 | ```python 135 | from sage.all import * 136 | 137 | # defund/coppersmith 138 | load("coppersmith.sage") 139 | 140 | def solve(u1, Ludown, u2, L2udown, n): 141 | polyrng = PolynomialRing(Zmod(n), 2, "xy") 142 | x,y = polyrng.gen() 143 | f = (u1*(10**Ludown)+x)**2 - (u2*(10**L2udown)+y) 144 | 145 | print("computing small_root...") 146 | result = small_roots(f, [10**Ludown, 10**L2udown], m=2, d=2) 147 | if result == []: 148 | return None 149 | print(result) 150 | 151 | want_result_0 = int(int(result[0][0])%n) 152 | want_result_1 = int(int(result[0][1])%n) 153 | print((want_result_0, want_result_1)) 154 | 155 | ans = u1*(10**Ludown)+want_result_0 156 | ans_2 = u2*(10**L2udown)+want_result_1 157 | assert (ans**2 - ans_2) % n == 0 158 | 159 | return ans 160 | ``` 161 | 162 | I run the code and it outputted some result within few seconds. But it did not pass answer checking for some reason. I manipulated small_roots parameters, but it did not change the status. And I added some small bruteforce for most significant digits for `x, y`, but did not... What can I do? 163 | 164 | After CTF ended, I saw the [writeup](https://blog.maple3142.net/2023/01/16/idekCTF-2022-writeups/#chronophobia) by @maple3142. I astonished and depressed, cause **the method is almost same** except for using another alternative Coppersmith extension [lattice-based-cryptanalysis](https://github.com/josephsurin/lattice-based-cryptanalysis) by @joseph instead of defund one. And his code includes the comment: 165 | 166 | ```python 167 | sys.path.append("./lattice-based-cryptanalysis") 168 | 169 | # idk why defund/coppersmith doesn't work... 170 | # need to remove `algorithm='msolve'` from solve_system_with_gb 171 | from lbc_toolkit import small_roots 172 | ``` 173 | 174 | OK... I have to analyze why we were wrong... 175 | 176 | Note: The intended solution is to use hidden number problem with some manipulation: ([chronophobia](https://github.com/EggRoll-Taiyaki/My-CTF-Challenges/tree/d2755d4601499e6de453e4d289cedde30eb37667/idekCTF/2022/Chronophobia)). It is also good way for avoiding high degree terms. 177 | 178 | ## Introduction to Coppersmith Method 179 | 180 | Then, I review Coppersmith method. Recently, sophisticated overview has published: [A Gentle Tutorial for Lattice-Based Cryptanalysis, J. Surin and S. Cohney, 2023](https://eprint.iacr.org/2023/032.pdf). So I skip basics of lattice except citing the following theorem. 181 | 182 | ### **Theorem** [LLL: Lenstra, Lenstra, Lovasz] 183 | 184 | Let $L$ be an integer lattice of $\dim{L}=\omega$. The LLL algorithm outputs a reduced basis spanned by $\{v_1,\ldots,v_{\omega}\}$ with 185 | 186 | $$ 187 | \|v_1\|\le \|v_2\| \le \ldots \le \|v_i\| \le 2^{\frac{\omega(\omega-i)}{4(\omega+1-i)}}\cdot {\det{L}}^{\frac{1}{\omega+1-i}}\ (i=1,\ldots,\omega) 188 | $$ 189 | 190 | in polynomial time in $\omega$ and entries of the basis matrix for $L$. 191 | 192 | Especially, LLL finds a short vector $v_1$ such that $\|v_1\| \le 2^{\frac{\omega-1}{4}}\cdot {\det{L}}^{\frac{1}{\omega}}$, that is, $v_1$ is some multiples of $\det{L}^{\frac{1}{\dim{L}}}$. The multiples are called approximation factor. The multiples could be large, but in practice we may obtain much smaller vector (maybe, not shortest, though). So for analyzing lattice, firstly consider of $\det{L}^{\frac{1}{\dim{L}}}$. 193 | 194 | Then, I focus Coppersmith method. 195 | For introduction, we assume we want to solve the following equation. The modulus `N` is the product of some two 512-bit primes. 196 | 197 | ``` 198 | x^2 + 159605847057167852113841544295462218002383319384138362824655884275675114830276700469870681042821801038268322865164690838582106399495428579551586422305321813432139336575079845596286904837546652665334599379653663170007525230318464366496529369441190568769524980427016623617364193484215743218597383810178030701505*x + 159605847057167852113841544295462218002383319384138362824655884275675114830276700469870681042821801038268322865164690838582106399495428579551586422305321813432139336575079845596286904837546652665334599379653663170007525230318464366496529369441190568769524980427016623616357485735731880812507594614316394069963 = 0 % 199 | 159605847057167852113841544295462218002383319384138362824655884275675114830276700469870681042821801038268322865164690838582106399495428579551586422305321813432139336575079845596286904837546652665334599379653663170007525230318464366496529369441190568769524980427016623617364193484215743218633044486831389275043 200 | ``` 201 | 202 | If we could solve this type of equation in general, we could factor `N` efficiently and could break RSA! (It would be impossible.) But, luckily, we can deduce the following equation by just subtracting `N*x+N`: 203 | 204 | $$ 205 | x^2 -35660676653358573538x-1006707748483862406125449872514995205080 = 0 206 | $$ 207 | 208 | If the solution `x0` is small, we can assume that the modulus solution `x0` can be find by just solving over integer. (The modulus equation can be reduced to infinitely integer equations $=0,=\pm{N},\pm{2 N},\ldots$, but $=0$ is only case if `x0` is small enough.) Solving modulus equation is hard, but solving integer equation is easier. In fact, Sagemath solve it in seconds. 209 | 210 | ```python 211 | sage: P=PolynomialRing(ZZ, 'x') 212 | sage: x=P.gens()[0] 213 | sage: f= x^2 -35660676653358573538*x-1006707748483862406125449872514995205080 214 | sage: f.roots() 215 | [(54225787401085700998, 1), (-18565110747727127460, 1)] 216 | ``` 217 | 218 | This is the essence of Coppersmith method: reducing modulus equation to *small* integer equation. 219 | 220 | Let's state Howgrave-Graham theorem. First, let $h(x_1,x_2,\ldots,x_n) = \sum_{(i_1,i_2,\ldots,i_n)}h_{i_1,i_2,\ldots,i_n}{x_1}^{i_1} \cdot {x_2}^{i_2} \cdots {x_n}^{i_n} \in \mathbb{Z}[x_1,x_2,\ldots,x_n]$. And $X_1,X_2,\ldots,X_n \in \mathbb{Z}_{>0}$. Then, we define 221 | 222 | $$ 223 | {\|h(x_1X_1,\ldots,x_nX_n)\|}_2 := \sqrt{\sum_{(i_1,i_2,\ldots,i_n)} {\left( h_{i_1,i_2,\ldots,i_n}{X_1}^{i_1} \cdot {X_2}^{i_2} \cdots {X_n}^{i_n} \right)}^2} 224 | $$ 225 | 226 | Then, we can prove the following: 227 | 228 | ### **Theorem** [Howgrave-Graham] 229 | 230 | Let $N$ is a positive integer, $h(x_1,x_2,\ldots,x_n) = \sum_{(i_1,i_2,\ldots,i_n)}h_{i_1,i_2,\ldots,i_n}{x_1}^{i_1} \cdot {x_2}^{i_2} \cdots {x_n}^{i_n} \in \mathbb{Z}[x_1,x_2,\ldots,x_n]$, and the number of monomials $\omega = \#\{(i_1,i_2,\ldots,i_n)\mid h_{i_1,i_2,\ldots,i_n}\ne 0\}$. If 231 | 232 | 1. $h(r_1,\ldots,r_n)=0 \pmod{N}$ for some $|r_1|< X_1,\ldots,|r_n|= RRh(beta)**2/delta: 370 | raise ValueError("too much large bound") 371 | 372 | testimate = int(1/(((RRh(beta)**2)/delta)/log_N_X - 1))//2 373 | 374 | logger.debug("testimate: %d", testimate) 375 | t = min([maxmatsize//delta, max(testimate, 3)]) 376 | 377 | whole_st = time.time() 378 | 379 | curfoundpols = [] 380 | while True: 381 | if t*delta > maxmatsize: 382 | raise ValueError("maxmatsize exceeded(on coppersmith_one_var)") 383 | u0 = max([int((t+1)/RRh(beta) - t*delta), 0]) 384 | for u_diff in range(0, maxu+1): 385 | u = u0 + u_diff 386 | if t*delta + u > maxmatsize: 387 | break 388 | foundpols = coppersmith_one_var_core(basepoly, bounds, beta, t, u, delta) 389 | if len(foundpols) == 0: 390 | continue 391 | 392 | curfoundpols += foundpols 393 | curfoundpols = list(set(curfoundpols)) 394 | sol = rootfind_ZZ(curfoundpols, bounds) 395 | if sol != [] and sol is not None: 396 | whole_ed = time.time() 397 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 398 | return sol 399 | elif len(curfoundpols) >= 2: 400 | whole_ed = time.time() 401 | logger.warning(f"failed. maybe, wrong pol was passed.") 402 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 403 | return [] 404 | t += 1 405 | # never reached here 406 | return None 407 | ``` 408 | 409 | The code imports the following functions. For details, see appendix. 410 | 411 | - shiftpoly(basepoly, baseidx, Nidx, varsidx_lst): generate shift polynomials as $\text{basepoly}^{\text{baseidx}}\cdot N^{\text{Nidx}}\cdot {x_1}^{j_1} \cdots {x_n}^{j_n}$, whose $j_i$ is varsidx_lst 412 | - genmatrix_from_shiftpolys(shiftpolys, bounds): generate matrix corresponding to shiftpolys 413 | - do_LLL(mat): output LLL result and transformation matrix from mat to LLL result 414 | - filter_LLLresult_coppersmith(basepoly, beta, t, shiftpolys, lll, trans): output short polynomial which satisfies $\text{output}(r)=0$ over integer for solution $r$ of $\text{basepoly}(r)=0 \pmod{b}$. This function only output polynomials with L1 norm $= log_N_X_bound: 533 | raise ValueError("too much large bound") 534 | 535 | mestimate = (n*(-RRh(beta)*ln(1-beta) + ((1-RRh(beta))**(-0.278465))/pi)/(log_N_X_bound - log_N_X))/(n+1.5) 536 | tau = 1 - (1-RRh(beta))**(RRh(1)/n) 537 | testimate = int(mestimate * tau + 0.5) 538 | 539 | logger.debug("testimate: %d", testimate) 540 | t = max(testimate, 1) 541 | 542 | while True: 543 | if t == 1: 544 | break 545 | m = int(t/tau+0.5) 546 | if binomial(n+1+m-1, m) <= maxmatsize: 547 | break 548 | t -= 1 549 | 550 | whole_st = time.time() 551 | 552 | curfoundpols = [] 553 | while True: 554 | m0 = int(t/tau+0.5) 555 | if binomial(n+1+m0-1, m0) > maxmatsize: 556 | raise ValueError("maxmatsize exceeded(on coppersmith_linear)") 557 | for m_diff in range(0, maxm+1): 558 | m = m0 + m_diff 559 | if binomial(n+1+m-1, m) > maxmatsize: 560 | break 561 | foundpols = coppersmith_linear_core(basepoly, bounds, beta, t, m) 562 | if len(foundpols) == 0: 563 | continue 564 | 565 | curfoundpols += foundpols 566 | curfoundpols = list(set(curfoundpols)) 567 | sol = rootfind_ZZ(curfoundpols, bounds) 568 | if sol != [] and sol is not None: 569 | whole_ed = time.time() 570 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 571 | return sol 572 | elif len(curfoundpols) >= 2 * n + 1: 573 | whole_ed = time.time() 574 | logger.warning(f"failed. maybe, wrong pol was passed.") 575 | logger.info("whole elapsed time: %f", whole_ed-whole_st) 576 | return [] 577 | t += 1 578 | # never reached here 579 | return None 580 | ``` 581 | 582 | ## More General Case 583 | 584 | A strategy for finding a root for modulus equation (and integer equation) on general case is proposed on [A Strategy for Finding Roots of Multivariate Polynomials with New Applications in Attacking RSA Variants, E. Jochemez and A. May, 2006](https://link.springer.com/content/pdf/10.1007/11935230_18.pdf). But this method does not assure we could obtain solution. I think general multivarite polynomial root finding is complicated. You might consider a polynomial $f(x,y,z)=a x y+b y z+c x^2+d$, which involves some cross terms $x y, y z$. We want to reduce these terms in case $X Y, Y Z$ might be large, but it blows up the number of related monomials. Also we do not know which monomial ordering should be chose. (Which one should we choose for diagonal elements: $y z$ or $x^2$ ?) 585 | 586 | For obtaining good result of partial key exposure attacks, many authors propose different lattice construction. Some results are refined on [A Tool Kit for Partial Key Exposure Attacks on RSA, 587 | †. Takayasu and N. Kunihiro, 2016](https://eprint.iacr.org/2016/1056.pdf). I do not think it is realistic to construct good lattice in our own during short period such as CTF. So we would like to search papers instead of tuning parameters. It might be worth to try using pre-checked good heuristic implementions, but devoting only one implementation is bad. If you determine to use heuristics, trying various methods may lead to win. 588 | 589 | Or you can apply Herrmann-May with linearization. If $X Y, Y Z, X Z$ are reasonably small, we can set new variables $U=X Y, V=Y Z, W=X Z$. Even if this construction might not be optimal, it could be better to try to complicated parameter tuning. 590 | 591 | On the other hand, I states just simple extention of univariate case/multivariate linear case: very restrictive bivariate case 592 | 593 | ### **Proposition** 594 | 595 | Let $N$ is a (large) positive integer, which has a divisor $b\ge {N}^\beta, 0 < \beta \le 1$. Let a polynomial $f(x,y)=f_{x1} x+f_{y\delta} y^\delta+\ldots+f_{y1} y+f_{00}$ be a special form bivariate polynomial, where the coefficient of $x$ for $f$ is invertible over $\pmod{N}$. And let $0 < X,Y$ for an expected bound for a root of $f(x,y)$. Then, we can find a solution $r$ of the equation 596 | 597 | $$ 598 | f(r) = 0 \pmod{b}\ (|r_1| < X, |r_2| < Y) 599 | $$ 600 | 601 | , if around $\log_N{X},\log_N{Y}\le \frac{(3 \beta-2)+{(1-\beta)}^{\frac{3}{2}}}{1+\delta}$. 602 | 603 | ### **Proof** 604 | 605 | The coefficient of $x$ for $f(x,y)$ is invertible, we can assume the coefficient of $x$ for $f(x,y)$ is $1$. 606 | Rewrite $f(x,y)=x+f_{y\delta} y^\delta+\ldots+f_{y1} y+f_{00}$. Let $t,m$ are some non-negative integers (tuned later). Then, consider shift polynomials $g_{(i,k)}={y}^{i} f^k N^{\max{\{t-k,\ 0\}}}$ with $i \le \delta \cdot (m-k)$. Then, we can construct the following lattice $L$. 607 | 608 | - each (column) element are corresponding to: $X^m,Y^\delta X^{m-1},\ldots,X^{m-1},Y^{2 \delta}*X^{m-2},\ldots,X^{m-2},\ldots, 1$ 609 | - each row are corresponding to: $g_{(0,m)},g_{(\delta,m-1)},\ldots,g_{(0,m-1)},g_{(2 \delta,m-2)},\ldots,g_{(0,m-2)},\ldots,g_{(0,0)}$ 610 | 611 | Those vectors have triangular form. $\dim{L}=(m+1)\cdot (2+\delta m)/2$. $\det(L)=X^{s_X}\cdot Y^{s_Y}\cdot N^{s_N}$, where $s_X=m\cdot (m+1)\cdot (\delta\cdot (m-1)+3)/6, s_Y=\delta\cdot m\cdot (m+1)\cdot (\delta\cdot (2 m+1) + 3)/12, s_N=t\cdot (t+1)\cdot (\delta\cdot (3 m-t+1)+3)/6$. 612 | 613 | We want to maximize $X,Y$ on $m$ as ${\det{L}}^{1/(\dim{L}-2+1)} < N^{t \beta}$ for obtaining $2$ good polynomials. In this proof, we assume $X \simeq Y$. By rough calculus ($1/m \simeq 0$), $\tau=1-\sqrt{1-\beta}$ ($t=\tau m$) gives some optimal value. Then, $\max{\log_{N}{X}},\max{\log_N{Y}} \simeq \frac{(3 \beta-2)+{(1-\beta)}^{\frac{3}{2}}}{(1+\delta)+\frac{\delta}{2*m}}$. Like other case, we expect we can find good $2$- polynomials if above condition satisfied. $\blacksquare$ 614 | 615 | Note that $\delta=1$ in above case is just $n=2$ for linear case. Constructed lattice and $\tau=1-{(1-\beta)}^{\frac{1}{2}}$ are exactly same, and the bounds of $X, Y$ matches for large $m$. 616 | 617 | This type of lattices can be constructed for another polynomials. For example, it can be applied to $f(x,y)=a_{20} x^2+a_{11} x y+a_{02} y^2+a_{10} x+a_{01} y+a_{00}$. In fact, the monomials $f^m$ are $\{x^i y^j\ \mid\ i+j\le 2 m\}$. Even if this structure might not always be applicable to other polynomials, we might expect some heuristic works. We may start $t=1,2,\ldots$ and $m=t/\tau$ as large multiple values of $t$ respect to $\beta$. (For small $\beta$, $m$ should be large value.) 618 | 619 | ## Back to chronophobia 620 | 621 | Then, I restate what we want to solve. Let $N=p q$ be a 1024 bit integer. We have a oracle named broken_token, which leaks $L=200$ digits (about 664bits). 622 | For the sake of this oracle, we know $L$-digits $u1,u2$, and we need to solve the following equation: 623 | 624 | $$ 625 | (u1\cdot (10^{\text{Ludown}})+y)^2 - (u2\cdot (10^{\text{Lu2down}})+x) = 0 \pmod N 626 | $$ 627 | 628 | , where $x,y$ are small ($\le 10^{\text{Ludown}}, 10^{\text{Lu2down}}$). ($\text{Ludown}, \text{Lu2down} \le 108$ digits or about 359 bits) 629 | 630 | As I just stated the proposition on general case section, we may solve this type of equation if $\log_2{Y},\log_2{X}<1024/3=341$ bits for $\beta=1.0$. I just say it solve ALMOST, but not. 631 | 632 | For extending the result, we consider the following equation ($a,b$ are known): 633 | 634 | $$ 635 | f(x, y) := -x + y^2 + a y + b = 0 \pmod{N} 636 | $$ 637 | 638 | This type of equation was analyzed at [Attacking Power Generators Using Unravelled Linearization: When Do We Output Too Much?, Herrman and May, 2009](https://link.springer.com/content/pdf/10.1007/978-3-642-10366-7_29.pdf). Let $u:=y^2-x$ for linearization. Then, 639 | 640 | $$ 641 | f^2\\ 642 | = {(u+a y+b)}^2\\ 643 | = u^2 + a^2 y^2 + b^2 + 2 a y u + 2 b u + 2 a b y\\ 644 | = u^2 + 2 a y u + (a^2+2 b) u + a^2 x + 2 a b y + b^2 645 | $$ 646 | 647 | Also, $y f = y u + a u + a x + b y$. 648 | 649 | Then, we construct the lattice $L$ with monomials $U^2, Y U, U, X, Y, 1$. These shift polynomials are $f^2, y f N, f N, x N^2, y N^2, N^2$: 650 | 651 | $$ 652 | \begin{pmatrix} 653 | U^2 & 2 a Y U & (a^2+2 b) U & a^2 X & 2 a b Y & b^2\\ 654 | 0 & N Y U & a N U & a N X & b N Y & 0\\ 655 | 0 & 0 & N U & 0 & a N Y & b N\\ 656 | 0 & 0 & 0 & N^2 X & 0 & 0\\ 657 | 0 & 0 & 0 & 0 & N^2 Y & 0\\ 658 | 0 & 0 & 0 & 0 & 0 & N^2\\ 659 | \end{pmatrix} 660 | $$ 661 | 662 | $\dim{L}=6$ and $\det{L}=U^4 X Y^2 N^8$. Then, assuming $X\simeq Y, U\simeq X^2$, for about $X < N^{\frac{4}{11}}$, we can find good polynomial. 663 | 664 | In our case, $1024\cdot (4/11) = 372$, so we can solve the above problem by Coppersmith method. 665 | 666 | I do not know above discussion assures we can construct $h(x,y)$ with shift polynomials of $f(x,y)$ (without linearlization) cause I do not construct a lattice directly. (lattice is complicated!) But outputs of lbc_toolkit may be reasonable. For solving chronophobia, we need the following shift polynomials (These are generated on the paremter $m=2, d=2$.): 667 | 668 | $$ 669 | {f(x,y)}^2, y f(x,y) N, x f(x,y)*N, x y N^2, x^2 N^2, f(x,y) N, x N^2, y N^2, N^2 670 | $$ 671 | 672 | This shift polynomials have triangular form (full lattice). And the lattice can generate good polynomial related to the lattice $L$ (with linearization). 673 | 674 | Then, we relook defund coppersmith. It turns out that the parameter $m=2, d=3$ works! This is cause shift polynomials are chosen as the following. (The parameter $m$ is corresponding to our parameter $t=2$. For obtaining $x^2 N^2$, we should set $d=2+1$.) 675 | 676 | ```python 677 | for i in range(m+1): 678 | base = N^(m-i) * f^i 679 | for shifts in itertools.product(range(d), repeat=f.nvariables()): 680 | g = base * prod(map(power, f.variables(), shifts)) 681 | G.append(g) 682 | ``` 683 | 684 | Though defund coppersmith can generate arbitrary shift polynomials, it may generate many useless shift polynomials for an input multivariate polynomial, so sometimes we could not compute LLL for large lattice in practice. lbc_toolkit outputs fairly reasonable shift polynomials, it includes a few useless shift polynomials, though. 685 | 686 | ## How to Solve Future CTF Challenges? 687 | 688 | With above discussion, I suggest the following basic strategy. 689 | 690 | 1. Construct input polynomial as **no cross term**. Or applying linearization if cross term bounds are small. (If not, search papers or change polynomial.) 691 | 2. try univariate case or linear case. parameters are chose based on above discussion, and go up parameters slightly (first $m$ and then $t$) 692 | 3. try heuristic (lbc_toolkit) with going up paremters (first $d$ and then $m$), in parallel, try defund one 693 | 694 | Also, I suggest to print debug messages on each stage. If LLL takes much times, we know these parameters are too much. If passes LLL and not output solution, then we may check Howgrave-Graham bounds are satisfied (especially, $\beta$ should be fairly restricted). If stucks on computing roots over integer, you might research how to find integer solution. Finding integer solution for multivariate polynomials are not easy in general. (general solver for diophantine equation does not exist.) 695 | 696 | ## Conclusion 697 | 698 | Lattice is so wild. The detailed discussions will help us for solving many tasks. We are waiting more discussion for specific examples... 699 | 700 | ## Appendix: rootfind_ZZ 701 | 702 | Finding roots over integer is not easy. For one variable polynomial, you only have to use Sagemath roots method. For multivarite polynomials, we do not know the efficient and exact method for root finiding task. So I implement three method: 703 | 704 | 1. solve_root_jacobian_newton: 705 | - numerical method (cannot find all roots, but efficient) 706 | - iteration method (Newton method: compute gradient (jacobian) and update point to close a root) 707 | - possibly, no root found (converge local minima or divergence) 708 | 2. solve_root_hensel 709 | - algebraic method (find all roots, slow) 710 | - find root mod small p and update to mod large modulus 711 | - possibly, cannot compute a root (too many candidates on modulus even if only a few roots over integer) 712 | 3. solve_root_triangulate 713 | - algebraic method (try to find all roots, slow) 714 | - compute Groebner basis and then find solution by solve function 715 | - For finding all roots, sometimes requires manual manipulation (no general method) 716 | 717 | ```python 718 | from sage.all import * 719 | 720 | from random import shuffle as random_shuffle 721 | from itertools import product as itertools_product 722 | import time 723 | 724 | from logger import logger 725 | 726 | 727 | def solve_root_onevariable(pollst, bounds): 728 | logger.info("start solve_root_onevariable") 729 | st = time.time() 730 | 731 | for f in pollst: 732 | f_x = f.parent().gens()[0] 733 | try: 734 | rt_ = f.change_ring(ZZ).roots() 735 | rt = [ele for ele, exp in rt_] 736 | except: 737 | f_QQ = f.change_ring(QQ) 738 | f_QQ_x = f_QQ.parent().gens()[0] 739 | rt_ = f_QQ.parent().ideal([f_QQ]).variety() 740 | rt = [ele[f_QQ_x] for ele in rt_] 741 | if rt != []: 742 | break 743 | result = [] 744 | for rtele in rt: 745 | if any([pollst[i].subs({f_x: int(rtele)}) != 0 for i in range(len(pollst))]): 746 | continue 747 | if abs(int(rtele)) < bounds[0]: 748 | result.append(rtele) 749 | 750 | ed = time.time() 751 | logger.info("end solve_root_onevariable. elapsed %f", ed-st) 752 | 753 | return result 754 | 755 | 756 | def solve_root_groebner(pollst, bounds): 757 | logger.info("start solve_root_groebner") 758 | st = time.time() 759 | 760 | # I heard degrevlex is faster computation for groebner basis, but idk real effect 761 | polrng_QQ = pollst[0].change_ring(QQ).parent().change_ring(order='degrevlex') 762 | vars_QQ = polrng_QQ.gens() 763 | G = Sequence(pollst, polrng_QQ).groebner_basis() 764 | try: 765 | # not zero-dimensional ideal raises error 766 | rt_ = G.ideal().variety() 767 | except: 768 | logger.warning("variety failed. not zero-dimensional ideal?") 769 | return None 770 | rt = [[int(ele[v]) for v in vars_QQ] for ele in rt_] 771 | 772 | vars_ZZ = pollst[0].parent().gens() 773 | result = [] 774 | for rtele in rt: 775 | if any([pollst[i].subs({v: int(rtele[i]) for i, v in enumerate(vars_ZZ)}) != 0 for i in range(len(pollst))]): 776 | continue 777 | if all([abs(int(rtele[i])) < bounds[i] for i in range(len(rtele))]): 778 | result.append(rtele) 779 | 780 | ed = time.time() 781 | logger.info("end solve_root_groebner. elapsed %f", ed-st) 782 | return result 783 | 784 | 785 | def solve_ZZ_symbolic_linear_internal(sol_coefs, bounds): 786 | mult = prod(bounds) 787 | matele = [] 788 | for i, sol_coef in enumerate(sol_coefs): 789 | denom = 1 790 | for sol_coef_ele in sol_coef: 791 | denom = LCM(denom, sol_coef_ele.denominator()) 792 | for sol_coef_ele in sol_coef: 793 | matele.append(ZZ(sol_coef_ele * denom * mult)) 794 | matele += [0]*i + [-mult*denom] + [0] * (len(bounds)-i-1) 795 | for idx, bd in enumerate(bounds): 796 | matele += [0]*len(sol_coefs[0]) + [0] * idx + [mult//bd] + [0]*(len(bounds)-idx-1) 797 | # const term 798 | matele += [0]*(len(sol_coefs[0])-1) + [mult] + [0]*len(bounds) 799 | mat = matrix(ZZ, len(sol_coefs)+len(bounds)+1, len(sol_coefs[0])+len(bounds), matele) 800 | logger.debug(f"start LLL for solve_ZZ_symbolic_linear_internal") 801 | mattrans = mat.transpose() 802 | lll, trans = mattrans.LLL(transformation=True) 803 | logger.debug(f"end LLL") 804 | for i in range(trans.nrows()): 805 | if all([lll[i, j] == 0 for j in range(len(sol_coefs))]): 806 | if int(trans[i,len(sol_coefs[0])-1]) in [1,-1]: 807 | linsolcoef = [int(trans[i,j])*int(trans[i,len(sol_coefs[0])-1]) for j in range(len(sol_coefs[0]))] 808 | logger.debug(f"linsolcoef found: {linsolcoef}") 809 | linsol = [] 810 | for sol_coef in sol_coefs: 811 | linsol.append(sum([ele*linsolcoef[idx] for idx, ele in enumerate(sol_coef)])) 812 | return [linsol] 813 | return [] 814 | 815 | 816 | def solve_root_triangulate(pollst, bounds): 817 | logger.info("start solve_root_triangulate") 818 | st = time.time() 819 | 820 | polrng_QQ = pollst[0].change_ring(QQ).parent().change_ring(order='lex') 821 | vars_QQ = polrng_QQ.gens() 822 | G = Sequence(pollst, polrng_QQ).groebner_basis() 823 | if len(G) == 0: 824 | return [] 825 | 826 | symbolic_vars = [var(G_var) for G_var in G[0].parent().gens()] 827 | try: 828 | sols = solve([G_ele(*symbolic_vars) for G_ele in G], symbolic_vars, solution_dict=True) 829 | except: 830 | return None 831 | 832 | logger.debug(f"found sol on triangulate: {sols}") 833 | 834 | result = [] 835 | # solve method returns parametrized solution. We treat only linear equation 836 | # TODO: use solver for more general integer equations (such as diophautus solver, integer programming solver, etc.) 837 | for sol in sols: 838 | sol_args = set() 839 | for symbolic_var in symbolic_vars: 840 | sol_var = sol[symbolic_var] 841 | sol_args = sol_args.union(set(sol_var.args())) 842 | 843 | sol_args = list(sol_args) 844 | sol_coefs = [] 845 | for symbolic_var in symbolic_vars: 846 | sol_var = sol[symbolic_var] 847 | sol_coefs_ele = [] 848 | for sol_arg in sol_args: 849 | if sol_var.is_polynomial(sol_arg) == False: 850 | logger.warning("cannot deal with non-polynomial equation") 851 | return None 852 | if sol_var.degree(sol_arg) > 1: 853 | logger.warning("cannot deal with high degree equation") 854 | return None 855 | sol_var_coef_arg = sol_var.coefficient(sol_arg) 856 | if sol_var_coef_arg not in QQ: 857 | logger.warning("cannot deal with multivariate non-linear equation") 858 | return None 859 | sol_coefs_ele.append(QQ(sol_var_coef_arg)) 860 | # constant term 861 | const = sol_var.subs({sol_arg: 0 for sol_arg in sol_args}) 862 | if const not in QQ: 863 | return None 864 | sol_coefs_ele.append(const) 865 | 866 | sol_coefs.append(sol_coefs_ele) 867 | ZZsol = solve_ZZ_symbolic_linear_internal(sol_coefs, bounds) 868 | result += ZZsol 869 | 870 | ed = time.time() 871 | logger.info("end solve_root_triangulate. elapsed %f", ed-st) 872 | return result 873 | 874 | 875 | def solve_root_jacobian_newton_internal(pollst, startpnt): 876 | # NOTE: Newton method's complexity is larger than BFGS, but for small variables Newton method converges soon. 877 | pollst_Q = Sequence(pollst, pollst[0].parent().change_ring(QQ)) 878 | vars_pol = pollst_Q[0].parent().gens() 879 | jac = jacobian(pollst_Q, vars_pol) 880 | 881 | if all([ele == 0 for ele in startpnt]): 882 | # just for prepnt != pnt 883 | prepnt = {vars_pol[i]: 1 for i in range(len(vars_pol))} 884 | else: 885 | prepnt = {vars_pol[i]: 0 for i in range(len(vars_pol))} 886 | pnt = {vars_pol[i]: startpnt[i] for i in range(len(vars_pol))} 887 | 888 | maxiternum = 1024 889 | iternum = 0 890 | while True: 891 | if iternum >= maxiternum: 892 | logger.warning("failed. maybe, going wrong way.") 893 | return None 894 | 895 | evalpollst = [(pollst_Q[i].subs(pnt)) for i in range(len(pollst_Q))] 896 | if all([int(ele) == 0 for ele in evalpollst]): 897 | break 898 | jac_eval = jac.subs(pnt) 899 | evalpolvec = vector(QQ, len(evalpollst), evalpollst) 900 | try: 901 | pnt_diff_vec = jac_eval.solve_right(evalpolvec) 902 | except: 903 | logger.warning("pnt_diff comp failed.") 904 | return None 905 | 906 | prepnt = {key:value for key,value in prepnt.items()} 907 | pnt = {vars_pol[i]: round(QQ(pnt[vars_pol[i]] - pnt_diff_vec[i])) for i in range(len(pollst_Q))} 908 | 909 | if all([prepnt[vars_pol[i]] == pnt[vars_pol[i]] for i in range(len(vars_pol))]): 910 | logger.warning("point update failed. (converged local sol)") 911 | return None 912 | prepnt = {key:value for key,value in pnt.items()} 913 | iternum += 1 914 | return [int(pnt[vars_pol[i]]) for i in range(len(vars_pol))] 915 | 916 | 917 | def solve_root_jacobian_newton(pollst, bounds): 918 | logger.info("start solve_root_jacobian newton") 919 | st = time.time() 920 | 921 | pollst_local = pollst[:] 922 | vars_pol = pollst[0].parent().gens() 923 | 924 | # not applicable to non-determined system 925 | if len(vars_pol) > len(pollst): 926 | return [] 927 | 928 | for _ in range(10): 929 | random_shuffle(pollst_local) 930 | for signs in itertools_product([1, -1], repeat=len(vars_pol)): 931 | startpnt = [signs[i] * bounds[i] for i in range(len(vars_pol))] 932 | result = solve_root_jacobian_newton_internal(pollst_local[:len(vars_pol)], startpnt) 933 | # filter too much small solution 934 | if result is not None: 935 | if all([abs(ele) < 2**16 for ele in result]): 936 | continue 937 | ed = time.time() 938 | logger.info("end solve_root_jacobian newton. elapsed %f", ed-st) 939 | return [result] 940 | 941 | 942 | def _solve_root_GF_smallp(pollst, smallp): 943 | Fsmallp = GF(smallp) 944 | polrng_Fsmallp = pollst[0].change_ring(Fsmallp).parent().change_ring(order='degrevlex') 945 | vars_Fsmallp = polrng_Fsmallp.gens() 946 | fieldpolys = [varele**smallp - varele for varele in vars_Fsmallp] 947 | pollst_Fsmallp = [polrng_Fsmallp(ele) for ele in pollst] 948 | G = pollst_Fsmallp[0].parent().ideal(pollst_Fsmallp + fieldpolys).groebner_basis() 949 | rt_ = G.ideal().variety() 950 | rt = [[int(ele[v].lift()) for v in vars_Fsmallp] for ele in rt_] 951 | return rt 952 | 953 | 954 | def solve_root_hensel_smallp(pollst, bounds, smallp): 955 | logger.info("start solve_root_hensel") 956 | st = time.time() 957 | 958 | vars_ZZ = pollst[0].parent().gens() 959 | smallp_exp_max = max([int(log(ele, smallp)+0.5) for ele in bounds]) + 1 960 | # firstly, compute low order 961 | rt_lows = _solve_root_GF_smallp(pollst, smallp) 962 | for smallp_exp in range(1, smallp_exp_max+1, 1): 963 | cur_rt_low = [] 964 | for rt_low in rt_lows: 965 | evalpnt = {vars_ZZ[i]:(smallp**smallp_exp)*vars_ZZ[i]+rt_low[i] for i in range(len(vars_ZZ))} 966 | nextpollst = [pol.subs(evalpnt)/(smallp**smallp_exp) for pol in pollst] 967 | rt_up = _solve_root_GF_smallp(nextpollst, smallp) 968 | cur_rt_low += [tuple([smallp**smallp_exp*rt_upele[i] + rt_low[i] for i in range(len(rt_low))]) for rt_upele in rt_up] 969 | rt_lows = list(set(cur_rt_low)) 970 | if len(rt_lows) >= 800: 971 | logger.warning("too much root candidates found") 972 | return None 973 | 974 | result = [] 975 | for rt in rt_lows: 976 | rtele = [[ele, ele - smallp**(smallp_exp_max+1)][ele >= smallp**smallp_exp_max] for ele in rt] 977 | if any([pollst[i].subs({v: int(rtele[i]) for i, v in enumerate(vars_ZZ)}) != 0 for i in range(len(pollst))]): 978 | continue 979 | if all([abs(int(rtele[i])) < bounds[i] for i in range(len(rtele))]): 980 | result.append(rtele) 981 | 982 | ed = time.time() 983 | logger.info("end solve_root_hensel. elapsed %f", ed-st) 984 | return result 985 | 986 | 987 | def solve_root_hensel(pollst, bounds): 988 | for smallp in [2, 3, 5]: 989 | result = solve_root_hensel_smallp(pollst, bounds, smallp) 990 | if result != [] and result is not None: 991 | return result 992 | return None 993 | 994 | 995 | ## wrapper function 996 | def rootfind_ZZ(pollst, bounds): 997 | vars_pol = pollst[0].parent().gens() 998 | if len(vars_pol) != len(bounds): 999 | raise ValueError("vars len is invalid (on rootfind_ZZ)") 1000 | 1001 | # Note: match-case statement introduced on python3.10, but not used for backward compati 1002 | if len(vars_pol) == 1: 1003 | return solve_root_onevariable(pollst, bounds) 1004 | else: 1005 | # first numeric 1006 | result = solve_root_jacobian_newton(pollst, bounds) 1007 | if result != [] and result is not None: 1008 | return result 1009 | 1010 | # next hensel (fast if the number of solutions mod smallp**a are small. in not case, cannot find solution) 1011 | result = solve_root_hensel(pollst, bounds) 1012 | if result != [] and result is not None: 1013 | return result 1014 | 1015 | # last triangulate with groebner (slow, but sometimes solve when above methods does not work) 1016 | #return solve_root_groebner(pollst, bounds) 1017 | return solve_root_triangulate(pollst, bounds) 1018 | ``` 1019 | --------------------------------------------------------------------------------