2025, Oct 19 04:16
Проверка RSA в PyCryptodome: используйте λ(n), а не φ(n)
Разбираемся, почему в RSA PyCryptodome e и d обратны по λ(n), а не по φ(n). Пошаговые примеры с gmpy2 и Python, корректная проверка через lcm(p−1,q−1).
Генерация ключей RSA в PyCryptodome на первый взгляд может показаться несогласованной, когда вы пытаетесь проверить модульную обратимость между открытой и закрытой экспонентами. Неожиданность обычно возникает, когда проверка по функции Эйлера возвращает ненулевой остаток для некоторых ключей, хотя шифрование и расшифрование работают безупречно.
Как воспроизвести проблему
Ниже приведён минимальный пример, который использует Crypto.PublicKey.RSA и gmpy2 для вычисления остатка (e · d − 1) по модулю (p − 1)(q − 1). Ожидание, что этот остаток всегда будет равен нулю, и приводит к путанице.
from Crypto.PublicKey import RSA
import gmpy2 as gp
z = gp.mpz(0)
o = gp.mpz(1)
kpair = RSA.generate(1024)
p_val = gp.mpz(kpair.p)
q_val = gp.mpz(kpair.q)
n_mod = gp.mpz(kpair.n)
d_sec = gp.mpz(kpair.d)
e_pub = gp.mpz(kpair.e)
phi_mod = gp.mul(gp.sub(p_val, o), gp.sub(q_val, o))
quot, rem = gp.t_divmod(gp.sub(gp.mul(e_pub, d_sec), o), phi_mod)
print("remainder:", rem)
assert rem == z
Что происходит на самом деле
Корень проблемы в предположении, что e и d являются взаимно обратными по модулю функции Эйлера (phi) для n. В PyCryptodome d строится как обратное к e относительно лямбда-функции Кармайкла для n, а не функции Эйлера. Оба подхода корректны для RSA, но использование лямбда Кармайкла даёт закрытую экспоненту на несколько бит меньше и поэтому немного эффективнее. Этого подхода придерживаются PyCryptodome и многие другие реализации.
Для модулей RSA, построенных из двух простых p и q, лямбда Кармайкла вычисляется как lcm(p − 1, q − 1). Получить её почти так же просто, как произведение (p − 1)(q − 1), но это меняет сравнение, которому должны удовлетворять e и d.
Как правильно проверять
Чтобы валидировать ключевой материал так же, как его определяет PyCryptodome, вычислите наименьшее общее кратное p − 1 и q − 1 и проверьте обратимость по модулю этого значения.
from Crypto.PublicKey import RSA
import gmpy2 as gp
import math
z = gp.mpz(0)
o = gp.mpz(1)
kpair = RSA.generate(1024)
p_val = gp.mpz(kpair.p)
q_val = gp.mpz(kpair.q)
d_sec = gp.mpz(kpair.d)
e_pub = gp.mpz(kpair.e)
lam_mod = math.lcm(int(p_val - o), int(q_val - o))
quo2, rem2 = gp.t_divmod(gp.sub(gp.mul(e_pub, d_sec), o), gp.mpz(lam_mod))
print("remainder_lambda:", rem2)
assert rem2 == z
Если хотите, то же условие можно записать ещё проще:
assert (int(e_pub) * int(d_sec)) % lam_mod == 1
Почему это важно
При аудите или инструментировании криптографического кода легко добавить проверки, которые принимают функцию Эйлера за «канонический» модуль для обратимости. Это допущение вызовет ложные срабатывания в библиотеках, использующих лямбда Кармайкла, хотя ключи корректны и алгоритмы работают правильно. Понимание того, на какой модуль ориентируется реализация, предотвращает неверную диагностику и помогает согласовать проверки с устройством библиотеки.
Выводы
Если вы проверяете ключи RSA из PyCryptodome, не ожидайте, что (e · d − 1) mod (p − 1)(q − 1) будет нулём для каждого ключа. Вместо этого используйте lcm(p − 1, q − 1) как модуль проверки. Это соответствует тому, как библиотека определяет закрытую экспоненту, помогает избежать ложных ошибок assert и отражает распространённую практику немного уменьшать d без ущерба для корректности RSA.
Статья основана на вопросе с StackOverflow от smss и ответе President James K. Polk.