| from __future__ import annotations |
| from dataclasses import dataclass |
| import logging |
| from typing import * |
| import gmpy2 |
| from . import Challenge, Serde |
| _logger = logging.getLogger(__name__) |
|
|
| @dataclass |
| class Problem(Serde): |
| difficulty: int |
| num: int |
| num_bits: int |
|
|
| @dataclass |
| class Solution(Serde): |
| status: str |
| p: Optional[int] |
| q: Optional[int] |
|
|
| @dataclass |
| class Verif(Serde): |
| n: int |
| p: int |
| q: int |
|
|
| def validate_breaking_rsa_solution(solution: Solution, verif: Verif, prob: Problem | None=None, require_success_status: bool=True) -> tuple[bool, str | None]: |
| if require_success_status and getattr(solution, 'status', None) != 'success': |
| return (False, f"Solution status is '{getattr(solution, 'status', None)}', not 'success'") |
| if solution.p is None or solution.q is None: |
| return (False, 'Solution has missing factors (p or q is null)') |
| try: |
| sol_p = int(solution.p) |
| sol_q = int(solution.q) |
| except (TypeError, ValueError): |
| return (False, f'Solution p/q are not valid integers: p={solution.p}, q={solution.q}') |
| sol_p, sol_q = (min(sol_p, sol_q), max(sol_p, sol_q)) |
| p_check, q_check = (min(verif.p, verif.q), max(verif.p, verif.q)) |
| n = verif.n |
| if sol_p * sol_q != n: |
| return (False, f'p * q != n: {sol_p} * {sol_q} != {n}') |
| if sol_p != p_check or sol_q != q_check: |
| return (False, f"Factors don't match expected: got ({sol_p}, {sol_q}), expected ({p_check}, {q_check})") |
| if prob is not None: |
| if prob.num != n or sol_p * sol_q != prob.num: |
| return (False, f'n mismatch between problem ({prob.num}) and verif ({n})') |
| return (True, None) |
|
|
| def _gen_prime(num_bits: int, rng: gmpy2.random_state) -> gmpy2.mpz: |
| n = gmpy2.mpz_urandomb(rng, num_bits) |
| n |= 1 << num_bits - 1 | 1 |
| return gmpy2.next_prime(n) |
|
|
| @dataclass |
| class BreakingRSA(Challenge[Problem, Solution, Verif]): |
| difficulty: int |
| num_bits: int |
|
|
| def generate(self, seed: int) -> tuple[Problem, Verif]: |
| rng = gmpy2.random_state(seed) |
| bits_p = self.num_bits // 2 |
| bits_q = self.num_bits - bits_p |
| fermat_thresh = 2 ** max(bits_p - 100, 1) |
| while True: |
| p = _gen_prime(bits_p, rng) |
| q = _gen_prime(bits_q, rng) |
| n = p * q |
| if n.bit_length() == self.num_bits and abs(p - q) > fermat_thresh: |
| break |
| n = int(n) |
| p = int(p) |
| q = int(q) |
| _logger.info(f'Generated {self.num_bits}-bit semiprime ({len(str(n))} digits)') |
| problem = Problem(self.difficulty, n, self.num_bits) |
| verif = Verif(n, p, q) |
| return (problem, verif) |
|
|
| def verify(self, prob: Problem, sol: Solution, verif: Verif) -> bool: |
| success, reason = validate_breaking_rsa_solution(sol, verif, prob, require_success_status=False) |
| if success: |
| _logger.info('Verification SUCCESS') |
| elif sol.p is None or sol.q is None: |
| _logger.info(f'Verification FAILURE: missing factors, got p={sol.p} and' + f' q={sol.q}, with solution status {sol.status}') |
| else: |
| _logger.info(f'Verification FAILURE: {reason or 'factors do not match'}') |
| return success |
|
|