From 4a5e57a38b6d3d0d6765f824673d64451958008d Mon Sep 17 00:00:00 2001 From: zmuxuny Date: Tue, 20 Oct 2026 21:13:43 -0700 Subject: [PATCH] Validate generated RSA private exponents modulo lambda(n) PyCryptodome generates d as the inverse of e modulo lcm(p-1, q-1). Checking the stronger condition modulo phi(n) therefore rejects otherwise valid generated keys and intermittently prevents regeneration of the pgcrypto session-key regression data. Use the condition from RFC 8017 section 3.2, keeping the other component checks unchanged. This only changes validation in the test-data generator, not the pgcrypto runtime. --- contrib/pgcrypto/scripts/pgp_session_data.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contrib/pgcrypto/scripts/pgp_session_data.py b/contrib/pgcrypto/scripts/pgp_session_data.py index 999350bb..0d41c527 100644 --- a/contrib/pgcrypto/scripts/pgp_session_data.py +++ b/contrib/pgcrypto/scripts/pgp_session_data.py @@ -39,7 +39,7 @@ def generate_rsa_keypair(key_size: int = 2048) -> dict: The generated key includes all components needed for PGP operations: - n: public modulus (p * q) - e: public exponent (typically 65537) - - d: private exponent (e^-1 mod phi(n)) + - d: private exponent (e^-1 mod lcm(p-1, q-1)) - p, q: prime factors of n - u: coefficient (p^-1 mod q) for CRT optimization @@ -57,7 +57,7 @@ def generate_rsa_keypair(key_size: int = 2048) -> dict: rsa_components = { 'n': key.n, # Public modulus (p * q) 'e': key.e, # Public exponent (typically 65537) - 'd': key.d, # Private exponent (e^-1 mod phi(n)) + 'd': key.d, # Private exponent (e^-1 mod lcm(p-1, q-1)) 'p': key.p, # First prime factor 'q': key.q, # Second prime factor 'u': inverse(key.p, key.q) # Coefficient for CRT: p^-1 mod q @@ -78,7 +78,7 @@ def validate_rsa_key(rsa: dict) -> None: Validations performed: 1. n = p * q (modulus is product of primes) 2. gcd(e, phi(n)) = 1 (public exponent is coprime to phi(n)) - 3. (d * e) mod(phi(n)) = 1 (private exponent is multiplicative inverse) + 3. (d * e) mod(lcm(p-1, q-1)) = 1 (private exponent is inverse) 4. (u * p) (mod q) = 1 (coefficient is correct for CRT) """ @@ -104,9 +104,11 @@ def validate_rsa_key(rsa: dict) -> None: if gcd(e, phi_n) != 1: raise ValueError("RSA validation failed: gcd(e, phi(n)) <> 1") - # Check that (d * e) mod(phi(n)) = 1 - if (d * e) % phi_n != 1: - raise ValueError("RSA validation failed: d * e <> 1 (mod phi(n))") + # RFC 8017, Section 3.2 requires the inverse modulo lambda(n), not + # phi(n). PyCryptodome also generates d using this least common multiple. + lambda_n = phi_n // gcd(p - 1, q - 1) + if (d * e) % lambda_n != 1: + raise ValueError("RSA validation failed: d * e <> 1 (mod lambda(n))") # Check that (u * p) (mod q) = 1 if (u * p) % q != 1: -- 2.52.0