El algoritmo RSA y la factorización de números grandes

  1. Introducción.
  2. El algoritmo RSA.
  3. Ejemplo con números pequeños.
  4. Ejemplo en C. (números pequeños)
  5. Ejemplo en C (con OpenSSL).
  6. Factorización de números grandes.
  7. Factorización mediante divisiones.
  8. Factorización por el método de Fermat.
  9. Factorización por el método de Monte Carlo.
  10. Referencias.


1. Introducción

El algoritmo RSA es un algoritmo de clave pública desarrollado en 1977 en el MIT por Ronald Rivest, Adi Shamir y Leonard Adelman.
Fue registrado el 20 de Septiembre de 1983. El 20 de Septiembre del 2000, tras 17 años, expiró la patente RSA, pasando a ser un algoritmo de dominio público.

Este popular sistema se basa en el problema matemático de la factorización de numeros grandes.


2. El algoritmo RSA

El algoritmo RSA funciona de la siguiente manera:

  1. Inicialmente es necesario generar aleatoriamente dos números primos grandes, a los que llamaremos p y q.

  2. A continuación calcularemos n como producto de p y q: n = p * q

  3. Se calcula fi: fi(n)=(p-1)(q-1)

  4. Se calcula un número natural e de manera que MCD(e, fi(n))=1 , es decir e debe ser primo relativo de fi(n).
    Es lo mismo que buscar un numero impar por el que dividir fi(n) que de cero como resto.

  5. Mediante el algoritmo extendido de Euclides se calcula d: e.d mod fi(n)=1 Puede calcularse d=((Y*fi(n))+1)/e para Y=1,2,3,... hasta encontrar un d entero.

  6. El par de números (e,n) son la clave pública.

  7. El par de números (d,n) son la clave privada.

  8. Cifrado: La función de cifrado es C = M^e mod n

  9. Descifrado: La función de descifrado es M = C^d mod n


3. Ejemplo con números pequeños.

  1. Escogemos dos números primos, por ejemplo p=3 y q=11.

  2. n = 3 * 11 = 33.

  3. fi(n) = (3-1) * (11-1) = 20.

  4. Buscamos e: 20/1=0, 20/3=6.67. e=3.

  5. Calculamos d como el inverso multiplicativo módulo z de e, por ejemplo, sustituyendo Y por 1,2,3,... hasta que se obtenga un valor entero en la expresión: d = ((Y * fi(n)) + 1) / e = ( Y * 20 + 1) / 3 = 21 / 3 = 7

  6. e=3 y n=33 son la clave pública.

  7. d=7 y n=33 son la clave privada.

  8. Cifrado: Mensaje = 5, C = M^e mod n = 5^3 mod 33 = 26

  9. Descifrado: M = C^d mod n = 26^7 mod 33 = 8031810176 mod 33 = 5


4. Ejemplo en C.

Veamos un ejemplo en C que utiliza números primos pequeños. Evidentemente este ejemplo solo es adecuado para comprender el algoritmo, no debe utilizarse en cifrados serios, pues el tamaño de los números hace que sea facilmente descifrable.



typedef unsigned long num;

typedef struct t_rsa_pubkey t_rsa_pubkey;
struct t_rsa_pubkey {

   num e;
   num n;
};

typedef struct t_rsa_privkey t_rsa_privkey;
struct t_rsa_privkey {


   num d;
   num n;
};


/* Verifica si el parametro es un numero primo */
int is_prime (num pr) {

   num i;

   if (pr==1) return 1;

   for (i=3; i<pr; i+=2)
      if ((pr%i != 0) && (i*i <= pr)) break;

   if (i*i > pr) return 1;
   else return 0;
}


/* (a^b) mod n */
static num modexp (num a, num b, num n) {

   num y;

   y = 1;
   while (b!=0) {

      if (b & 1) y = (y * a) % n;

      a = (a * a) % n;
      b = b >> 1;
   }

   return y;
}

/* Cifra plaintext */
void rsa_cipher (num plaintext, num *ciphertext, t_rsa_pubkey pubkey) {

   *ciphertext = modexp (plaintext, pubkey.e, pubkey.n);
}

/* Descifra ciphertext */
void rsa_decipher (num ciphertext, num *plaintext, t_rsa_privkey privkey) {

   *plaintext = modexp (ciphertext, privkey.d, privkey.n);
}

/* Genera un clave publica y una privada a partir de p y q. */
void rsa_genkeys (num p, num q, t_rsa_pubkey *pubkey, t_rsa_privkey *privkey) {

   num n, fi, i, e, d;

   /* Calculamos n */
   n = p * q;

   /* Calculamos fi */
   fi = (p-1) * (q-1);

   /* Calculamos e */
   for (i=1; i<fi; i++)  if (is_prime(i)) { if (fi%i!=0) { e=i; break; } }

   /* Calculamos d */
   for (i=1; i<fi; i++)  if ( ((i*fi)+1)%e == 0) { d=((i*fi)+1)/e; break; }

   pubkey->e = e;
   pubkey->n = n;

   privkey->d = d;
   privkey->n = n;
}


int main (int argc, char **argv)
{
   num num_plain;
   num num_cipher;
   t_rsa_pubkey pubkey;
   t_rsa_privkey privkey;
   num p, q;

   if (argc != 4) {

      printf ("Uso: %s  

\n\n", argv[0]); return 0; } num_plain = atoll (argv[1]); p = atoll (argv[2]); q = atoll (argv[3]); /* Generamos las claves a partir de p y q */ rsa_genkeys (p, q, &pubkey, &privkey); /* Ciframos */ rsa_cipher (num_plain, &num_cipher, pubkey); printf ("Plain number: %ld\n", num_plain); printf ("Cipher number: %ld\n", num_cipher); /* Desciframos */ rsa_decipher (num_cipher, &num_plain, privkey); printf ("Plain number: %ld\n", num_plain); printf ("\n"); return 0; }


5. Ejemplo en C (con OpenSSL)

Para finalizar, un ejemplo sencillo de como utilizar las funciones RSA de la librería OpenSSL.


#include <stdio.h>
#include <openssl/rsa.h>
#include <openssl/engine.h>



int main()
{


   RSA *keypair;
   char *cleartext = "Hola caracola";
   char *ciphertext;
   char *cleartext2;

   keypair = RSA_new();


   /* Generamos un par de claves de 1024 bits */
   keypair = RSA_generate_key (1024, 17, NULL, NULL);


   ciphertext = malloc(RSA_size(keypair));
   
   RSA_public_encrypt (strlen(cleartext)+1, cleartext, ciphertext, keypair, 
                       RSA_PKCS1_PADDING);


   cleartext2 = malloc(RSA_size(keypair));

   RSA_private_decrypt (RSA_size(keypair), ciphertext, cleartext2, keypair, 
                        RSA_PKCS1_PADDING);


   printf("%s\n", cleartext2);

   RSA_free(keypair);

   
   return 0;
}


6. Factorización de números grandes.

El primer método que se le puede ocurrir para romper rsa consiste en factorizar n. Una vez factorizado n obtenemos p y q, y con estos fi(n).
Con estos datos ya podemos obtener la clave privada mediante el algoritmo de euclides extendido.

Lo que puede parecer en principio algo trivial, no lo es en absoluto. Si los numeros elegidos como claves son los suficiente grandes (a partir de 1024 bits) resulta computacionalmente imposible factorizar estos números en un tiempo razonable. Pues los algoritmos de factorización son de tiempo exponencial.

Actualmente uno de los algoritmos más rápidos de factorización es el conocido como Number Field Sieve (NFS). Aunque existen otros algoritmos muy rápidos como los de curva elíptica, el algoritmo de Pollard, etc.

En teoría el algoritmo RSA se basa en el problema de la factorización de numeros grandes, aunque nunca se ha demostrado matematicamente que no pueda obtenerse d a partir de n y e.


7. Factorización mediante divisiones.

El método más secillo para factorizar un número n consiste en realizar divisiones sucesivamente por los números primos p= 2, 3, 5, ... hasta p >= n. Cuando n mod p = 0 sabremos que p es uno de los números primos factores de n. Puede repetirse este procedimiento con el resultado de la división hasta encontrar todos los factores.

Como ejemplo vamos a factorizar el número 12345:
  • 12345/2=6173, 12345 mod 2 = -1
  • 12345/3=4115, 12345 mod 3 = 0 [ factor: 3 ]
  • 4115/3=1372, 4115 mod 3 = -1
  • 4115/5=823, 4115 mod 5 = 0 [ factor: 5 ]
  • 823/5=165, 823 mod 5 = -2
  • 823/7=118, 823 mod 7 = -3
  • 823/11=75, 823 mod 11 = -2
  • 823/13=64, 823 mod 13 = -9
  • 823/17=49, 823 mod 17 = -10
  • 823/19=44, 823 mod 19 = -13
  • 823/23=36, 823 mod 23 = -5
  • 823/29=29, 823 mod 29 = -18 [ factor: 823 ]
  • Por lo tanto los factores primos son: 3, 5 y 823.


    Veamos un programa de ejemplo, utilizando la librería
    GMP:

    
    #include <gmp.h>
    
    int main (int argc, char **argv) {
    
       mpz_t n;
       mpz_t d;
       mpz_t q;
       mpz_t r;
    
       if (argc!=2) {
    
          printf ("%s <numero>\n", argv[0]);
          return 0;
       }
    
       mpz_init (n);
       mpz_init (d);
       mpz_init (q);
       mpz_init (r);
    
       /* Inicializamos valores */
       mpz_set_ui  (d, 2);
       mpz_set_str (n, argv[1], 10);
    
    
       while (1) {
    
          /* Si n=1 salir */
          if (mpz_cmp_ui (n, 1)==0) return 0;
    
          /* q=n/d, r=n mod d */
          mpz_cdiv_qr (q, r, n, d);
    
          if (mpz_probab_prime_p (d, 5)>0) {
    
             /* Si r=0 */
             if (mpz_cmp_ui (r, 0)==0) {
    
                gmp_printf ("factor: %Zd\n", d);
                mpz_set (n, q);
    
             }
             else {
                /* Si q > d, d++ */
                if (mpz_cmp (q,d) > 0) mpz_add_ui (d, d, 1);
                else {
                   gmp_printf ("factor: %Zd\n", n);
                   break;
                }
             }
          }
          else  mpz_add_ui (d, d, 1);
    
       }
    
       mpz_clear (n);
       mpz_clear (d);
       mpz_clear (q);
       mpz_clear (r);
    
       return 0;
    }
    
    

    Este algoritmo no es muy apropiado para factorizar números del estilo n=p*q, como los utilizados en el algoritmo RSA. Si p y q son primos grandes, y empezamos a probar divisiones a partir de 2, 3, 5, ... Es muy probable que realicemos una gran cantidad de divisiones inútiles.

    Sabemos que si n=p*q y p > q. Entonces p > raiz(n), y q < raiz(n). Esto permite empezar a buscar por el numero mas grande. En caso de que p y q sean cercanos, llegaremos antes si empezamos por el primer primo cercano a raiz(n) y continuamos hasta 2 o hasta encontrar un factor.

    Factoricemos el producto de dos primos 459:
  • 459/19=25, r:-16
  • 459/17=27, r:0 fator: 27 459/27=17
  • Y el programa de ejemplo:

    
    
    #include <gmp.h>
    
    int main (int argc, char **argv) {
    
       int cmp;
    
       mpz_t n;
       mpz_t p;
       mpz_t q;
       mpz_t r;
       mpz_t raiz_n;
       gmp_randstate_t state;
    
    
       if (argc!=3) {
    
          printf("%s <num bits primo 1> <num bits primo 2>\n",argv[0]);
          return 0;
       }
    
    
       mpz_init (n);
       mpz_init (p);
       mpz_init (q);
       mpz_init (r);
       mpz_init (raiz_n);
    
       /* Generamos dos primos aleatorios */
       gmp_randinit_default (state);
       while (mpz_probab_prime_p (p, 5)==0) mpz_urandomb (p, state, atoi(argv[1]));
       while (mpz_probab_prime_p (q, 5)==0) mpz_urandomb (q, state, atoi(argv[2]));
       gmp_printf ("%Zd * %Zd", p, q);
       mpz_mul (n, p, q);
       gmp_printf (" = %Zd\n", n);
    
    
       mpz_set_str (n, argv[1], 10);
    
       /* parte_entera(raiz(n)) */
       mpz_sqrt (raiz_n, n);
    
       /* Necesitamos un numero impar */
       if (mpz_even_p (raiz_n)) mpz_add_ui (raiz_n, raiz_n, 1);
    
       /* Inicializamos valores */
       mpz_set (q, raiz_n);
    
    
       while (mpz_cmp_ui (q, 2) > 0)  {
    
          if (mpz_probab_prime_p (q, 5) > 0)  {
    
             mpz_cdiv_qr (p, r, n, q);
    
             if (mpz_cmp_ui (r, 0) == 0) {
    
                gmp_printf ("\np:%Zd q:%Zd\n", p, q);
                return 0;
             }
    
          }
          mpz_sub_ui (q, q, 2);
       }
    
       gmp_printf ("No encontrado!\n");
       gmp_printf ("No es una multiplicacion de dos primos\n");
    
    
       mpz_clear (n);
       mpz_clear (p);
       mpz_clear (q);
       mpz_clear (r);
       mpz_clear (raiz_n);
    
       return 0;
    }
    
    



    8. Factorización por el método de Fermat.

    Este método fue usado por Pierre de Fermat en 1643 y consiste en representar el número n como n = x^2 - y^2.
    Suponiendo N = uv, con u <=v, donde N es un número impar, tenemos que:
    
       x=(u+v)/2,      y=(v-u)/2
       N=x^2-y^2,      0<=y<=x<=N
    

    Dada esta representación, podemos realizar una búsqueda de valores de x y de y que cumplan la ecuación N=x^2-y^2.
    A continuación un ejemplo utilizando la librería
    GMP:

    
    
    #include 
    
    int main (int argc, char **argv) {
    
       mpz_t n;
       mpz_t x;
       mpz_t y;
       mpz_t r;
       mpz_t tmp;
    
       if (argc!=2) {
    
          printf ("%s \n", argv[0]);
          return 0;
       }
    
       mpz_init (n);
       mpz_init (x);
       mpz_init (y);
       mpz_init (r);
       mpz_init (tmp);
    
       /* Inicializamos valores */
       mpz_set_str (n, argv[1], 10);  
    
    	/* Si es 2 salimos */
    	if (mpz_cmp_ui (n, 2) == 0) {
    	
    		printf ("2 es primo\n");
    		return 0;
    	}
    
    	/* Mientras el numero no sea impar, dividimos por 2 */
    	mpz_mod_ui (r, n, 2);
    
    	while (mpz_cmp_ui (r, 0) == 0) {
    
          mpz_cdiv_q_ui (n, n, 2);
    		mpz_mod_ui (r, n, 2);
    	}
    
       /* x=2*parte_entera(raiz(n))+1 */
       mpz_sqrt (tmp, n);
       mpz_mul_ui (x, tmp, 2);
       mpz_add_ui (x, x, 1);
    
       mpz_set_ui (y, 1);
    
       /* r=parte_entera(raiz(n))^2 -n */
       mpz_mul (r, tmp, tmp);
       mpz_sub (r, r, n);
    
       while (1) {
    
          /* mientras r > 0 */
          while (mpz_cmp_ui (r, 0)>0) {
    
             mpz_sub (r, r, y);
             mpz_add_ui (y, y, 2);
          }
    
          if (mpz_cmp_ui (r, 0)==0) {
    
             /* El factor mas grande es: */
             mpz_sub (tmp, x, y);
             mpz_cdiv_q_ui (tmp, tmp, 2);
    
             gmp_printf ("Factor mas grande <= raiz(N): %Zd\n", tmp);
             return 0;
          }
    
          mpz_add (r, r, x);
          mpz_add_ui (x, x, 2);
       }
    
       mpz_clear (n);
       mpz_clear (x);
       mpz_clear (y);
       mpz_clear (r);
       mpz_clear (tmp);
    
       return 0;
    }
    
    



    9. Factorización por el método de Monte Carlo.

    Un sistema bastante rápido de factorización es el conocido como algoritmo de Monte Carlo o método de Pollard (1975).

    
    #include 
    
    int main (int argc, char **argv) {
    
       mpz_t x1;
       mpz_t x2;
       mpz_t k;
       mpz_t l;
       mpz_t n;
       mpz_t g;
       mpz_t tmp;
    
       if (argc!=2) {
    
          printf ("%s \n", argv[0]);
          return 0;
       }
    
       mpz_init (x1);
       mpz_init (x2);
       mpz_init (k);
       mpz_init (l);
       mpz_init (n);
       mpz_init (g);
       mpz_init (tmp);
     
       /* Inicializamos valores */
       mpz_set_ui  (x1, 5);  
       mpz_set_ui  (x2, 2);  
       mpz_set_ui  (k, 1);  
       mpz_set_ui  (l, 1);  
       mpz_set_str (n, argv[1], 10);  
    
       /* Si n es primo, salir */
       if (mpz_probab_prime_p (n, 5)) {
    
          gmp_printf ("primo: %Zd\n", n);
          return 0;
       }
    
       while (1) {
    
          /* MCD (x1-x2, n) */
          mpz_sub (tmp, x2, x1);
          mpz_gcd (g, tmp, n);
    
          /* si g=1 */
          if (mpz_cmp_ui (g, 1)==0) {
    
             mpz_sub_ui (k, k, 1);
             
             if (mpz_cmp_ui (k, 0)==0) {   
          
                mpz_set (x2, x1);
                mpz_mul_ui (l, l, 2);
                mpz_set (k, l);     
             }
                
             /* x=x^2+1 */
             mpz_mul (x1, x1, x1);
             mpz_add_ui (x1, x1, 1);
             mpz_mod (x1, x1, n);
             
          }
          else {
    
             gmp_printf ("factor: %Zd \n", g);
    
             /* Si g=n, error!! */
             if (mpz_cmp (g, n)==0) { gmp_printf ("Error!!\n"); return 0; }
    
             /* n=n/g */
             mpz_cdiv_q (n, n, g);
    
             /* x1=x1 mod n */
             mpz_mod (x1, x1, n);
    
             /* x2=x2 mod n */
             mpz_mod (x2, x2, n);
    
             /* Si n es primo, salir */
             if (mpz_probab_prime_p (n, 5)) {
    
                gmp_printf ("primo: %Zd \n", n);
                return 0;
             }
          }
       
       }
    
       mpz_clear (x1);
       mpz_clear (x2);
       mpz_clear (k);
       mpz_clear (l);
       mpz_clear (n);
       mpz_clear (g);
       mpz_clear (tmp);
    
       return 0;
    }
    
    



    10. Referencias.

    [1] The Art Of Computer Programming (2nd ed). Donald E. Knuth. (Addison Wesley). Volume 2: Seminumerical Algorithms.
    [2] Applied Cryptography (2nd Ed). Bruce Schneier.
    [3] Criptografía y seguridad en computadores (3a ed). Manuel José Lucena López.


    daniellerch.com