RSA, Certificados X509 y OpenSSL
Posted by Ahharu | Posted in Programación | Posted on 02:40
Debido a que e visto que no es un tema muy común el uso de diferentes librerías, que muchas veces las manpages no dejan si no más dudas y que los ejemplos a veces son complicados de encontrar; he creado este mini articulo de cómo usar el cifrado RSA junto con Certificados x509 mediante OpenSSL; de una vez advierto el que no me detendré a explicar que es RSA, certificados x509 u OpenSSL.
Comenzando
Bueno venga lo primero que tenemos que tener es un certificado x509 que nos funcione, así que generamos una llave privada de la siguiente forma:
openssl dsaparam -rand -genkey -out privkey.txt 1024
Listo la llave privada, ahora la usamos para crear nuestro certificado personal:
openssl req -new -x509 -days 365 -key privkey.txt -out cert.crt
Ya teniendo tanto el certificado como la llave privada podemos proceder a la parte interesante, que es utilizarlos.
Cargando el certificado
La idea de cargar el certificado es tener como tal la llave pública del mismo y usar esta para realizar algún proceso en específico como lo puede ser la autenticidad de un mensaje/archivo o el cifrado del mismo.
En este caso la forma de cargar el certificado mediante OpenSSL es bastante simple:
bool CRSA::LoadPublicKey( const char *pszCertFile )
{
FILE *pfFile = NULL;
X509 *pxCert;
pfFile = fopen( pszCertFile,"r");
if (pfFile == NULL) {
SetErr( CRSA_ERR_UNABLE_TO_OPEN_FILE );
return false;
}
pxCert = X509_new();
if (pxCert == NULL) {
SetErr( CRSA_ERR_UNABLE_TO_ALLOCATE_CERT );
fclose( pfFile );
return false;
}
if (PEM_read_X509( pfFile, &pxCert, NULL, NULL) == NULL) {
SetErr( CRSA_ERR_OPEN_SSL_INTERNAL );
fclose( pfFile );
X509_free( pxCert );
return false;
}
fclose( pfFile );
El que no sepa que es "PEM" directamente a wikipedia a leer:
http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail
Este código es extraído de mi librería para manejo básico de RSA con OpenSSL, esta muestra las diferentes operaciones que en realidad se limitan a "PEM_read_X509", cabe señalar el que también es válido cargar un certificado que traiga la llave privada embebida y en ese caso debemos especificar la contraseña del archivo; el prototipo de la función es:
X509 *PEM_read_X509(FILE *fp, X509 **x, pem_password_cb *cb, void *u);
En este caso el argumento que nos interesa es pem_password_cb que es la callback seteada para especificar el password del certificado, lo cual sería algo como:
int PassCllBck(char *pszBuf, int iSize, int iRW, void *lvParam);
{
int iLen = strlen("MyPass");
// Si es demasiado larga, limitamos el tamaño
if (iLen > iSize)
iLen = iSize;
memcpy( pszBuf, "MyPass", iLen);
return iLen;
}
Esto es válido para todas las funciones que implican el uso de algún password y que implementen callbacks como lo son "PEM_read_PrivateKey" y "PEM_read_DSAPrivateKey" entre otras; igualmente el parámetro "void *u" también puede ser usado para especificar la frase, por ende la llamada podría realizarse de esta forma.
PEM_read_X509( pfFile, &pxCert, NULL, "MyPass");
Obteniendo la llave publica
Ya que tenemos el certificado cargado podemos acceder a los diferentes valores del mismo, en este caso nos interesa la llave pública que contiene, el problema radica en que las funciones específicas para el cifrado mediante RSA requieren precisamente un objeto de tipo "RSA" lo cual no tenemos, aun así es fácilmente extraíble de la siguiente forma:
RSA *prPublicKey = NULL;
EVP_PKEY *pTmp = NULL;
X509 *pxCert;
pTmp = X509_get_pubkey( pxCert );
X509_free( pxCert );
pxCert = NULL;
if (pTmp == NULL) {
SetErr( CRSA_ERR_OPEN_SSL_INTERNAL );
return false;
}
if (prPublicKey != NULL) {
RSA_free( prPublicKey );
m_prPublicKey = NULL;
}
prPublicKey = EVP_PKEY_get1_RSA( pTmp );
EVP_PKEY_free( pTmp );
if (prPublicKey == NULL) {
SetErr( CRSA_ERR_OPEN_SSL_INTERNAL );
return false;
}
return true;
}
Como vemos la función "X509_get_pubkey" nos permite extraer la llave pública como un objeto de tipo EVP_PKEY ( El cual nos es útil en otras instancias ) y a partir de este podemos extraer la llave pública mediante "EVP_PKEY_get1_RSA" ya como un objeto "RSA" el cual es el que vamos a usar directamente para el cifrado y descifrado de la información.
Es de recalcar el que podemos exportar esta llave mediante otras funciones como lo es "PEM_write_PUBKEY"( Admite un objeto EVP_PKEY no RSA ), directamente es mejor almacenar los certificados que los exports generados por estas funciones debido a que el proceso de carga puede tener incompatibilidades dependientes de la API usada.
Cargando la llave Privada
La llave privada en este caso la generamos en texto plano ( Que si coño que no se debe hacer pero es un puto ejemplo ); así que el proceso de cargado de la misma no nos requiere mayor problema, basta con:
bool CRSA::LoadPrivateKey( const char *pszFile )
{
FILE *pfFile = NULL;
RSA *prPrivateKey;
pfFile = fopen( pszFile, "r");
if (pfFile == NULL) {
SetErr( CRSA_ERR_UNABLE_TO_OPEN_FILE );
return false;
}
if (prPrivateKey == NULL) {
RSA_free( m_prPrivateKey );
m_prPrivateKey = NULL;
}
prPrivateKey = PEM_read_RSAPrivateKey( pfFile, NULL, NULL, NULL);
fclose( pfFile );
if (prPrivateKey == NULL) {
SetErr( CRSA_ERR_OPEN_SSL_INTERNAL);
return false;
}
return true;
}
Como vemos el método es sumamente sencillo, para tener en cuenta que el método usado para cargar la llave es un método PEM precisamente porque la llave usa esta codificación; también es posible el cargar llaves que están cifradas usando otros argumentos de la función; el prototipo de la función es:
RSA *PEM_read_RSAPrivateKey(FILE *fp, RSA **x, pem_password_cb *cb, void *u);
Anteriormente ya se explico el cómo especificar una frase de password para cargar el archivo así que continuemos.
Cifrando y Descifrando
Bien ya tenemos como tal nuestras respectivas llaves y podemos realizar el proceso de cifrado y descifrado, ahora cabe anotar que existen 4 formas de realizar el cifrado con OpenSSL, ateniéndonos al man page:
RSA_PKCS1_PADDING:
PKCS #1 v1.5 padding. This currently is the most widely used mode.
RSA_PKCS1_OAEP_PADDING:
EME-OAEP as defined in PKCS #1 v2.0 with SHA-1, MGF1 and an empty encoding parameter. This mode is recommended for all new applications.
RSA_SSLV23_PADDING:
PKCS #1 v1.5 padding with an SSL-specific modification that denotes that the server is SSL3 capable.
RSA_NO_PADDING
Raw RSA encryption. This mode should only be used to implement cryptographically sound padding modes in the application code. Encrypting user data directly with RSA is insecure.
Especial cuidado en el ultimo modo que especifica el que RSA_NO_PADDING es inseguro al arrojar siempre el mismo resultado para un texto cifrado haciendo posible ataques al mismo de una forma mucho mas fácil, lo recomendable es combinarlo con alguno de los otros modos, básicamente es el mismo concepto manejado por los diferentes modos de cifrado por bloque que se usan en criptografía simetrica.Ya que tenemos claro los diferentes modos podemos pasar al cómo usarlos ya que algunos involucran IV's dentro del mensaje y es un error común el implementarlos mal y desconocer la razón de por qué la librería retorna error al realizar la operación; así que primeramente debemos tener en cuenta el tamaño del bloque a cifrar ya que para cada modo es distinto, remitiéndonos a la documentación:
flen must be less than RSA_size(rsa) - 11 for the PKCS #1 v1.5 based padding modes, less than RSA_size(rsa) - 41 for RSA_PKCS1_OAEP_PADDING and exactly RSA_size(rsa) for RSA_NO_PADDING. The random number generator must be seeded prior to calling RSA_public_encrypt().
Por ende podemos crear una función que nos calcule el tamaño del bloque mediante RSA_size y el modo que vamos a usar para cifrar de esta forma:
#define CRSA_PKCS1_PADDING_BYTES 11
#define CRSA_PKCS1_OAEP_PADDING_BYTES 41
int CRSA::GetSize( CRSA_PADDING cpMode )
{
int iRet = 0;
if (m_prPublicKey != NULL)
iRet = RSA_size( m_prPublicKey );
if (m_prPrivateKey != NULL)
iRet = RSA_size( m_prPrivateKey );
if (iRet == 0) {
SetErr( CRSA_ERR_KEYS_MISSING );
return 0;
}
switch ( cpMode ) {
case CRSA_NO_PADDING:
break;
case CRSA_PKCS1_PADDING:
iRet -= CRSA_PKCS1_PADDING_BYTES;
break;
case CRSA_PKCS1_OAEP_PADDING:
iRet -= CRSA_PKCS1_PADDING_BYTES;
default:
SetErr( CRSA_ERR_INVALID_PARAMETERS );
return 0;
}
return iRet;
}
A tener en cuenta que RSA_size requiere una llave a fin de dar el tamaño en base a la misma ( 1024 bits = 128 bytes ), luego realizamos la respectiva resta para obtener el tamaño de bloque compatible con el modo que vamos a usar.
Ya con esto creamos una implementación de cifrado compatible con los diversos tipos:
int CRSA::EncryptBlock( u_char *pszSrc, int iLenght, u_char *pszDst, CRSA_PADDING cpMode)
{
int iRet = 0, iSize = 0;
if (HavePublicKey() == false) {
SetErr( CRSA_ERR_PUBLIC_KEY_MISSING );
return 0;
}
iSize = GetSize( cpMode );
if (pszSrc == NULL || iSize == 0 || iLenght > iSize || pszDst == NULL) {
SetErr( CRSA_ERR_INVALID_PARAMETERS );
return 0;
}
memset( pszDst, 0x00, iSize + 1);
iRet = RSA_public_encrypt( iLenght, pszSrc, pszDst, prPublicKey, cpMode);
if (iRet == -1) {
SetErr( CRSA_ERR_OPEN_SSL_INTERNAL );
return 0;
}
return iRet;
}
Podemos ver que es relativamente simple el realizar la operaciones y que la función "RSA_public_encrypt" nos devolverá el tamaño del bloque cifrado, debemos recordar que este será del mismo tamaño que la llave que estamos usando para cifrar.
En el caso de descifrar vamos a realizar la misma operación solo que usando "RSA_private_decrypt", en este caso debemos tener en cuenta que siempre vamos a leer bloques del tamaño dado por RSA_size y obviamente la salida según el método de padding va a ser menor a RSA_size, así que debemos asegurarnos de los tamaños de los buffers antes de realizar cualquier operación.
Es posible también el realizar el proceso inverso ( cifrar con la llave privada, descifrar con la pública ) mediante las funciones "RSA_private_encrypt" y "RSA_public_decrypt", los prototipos de las mismas son exactamente iguales a las anteriores.
Finalizando
Ya para finalizar existen varias funciones que también resultan útiles como:
RSA_check_key
RSA_generate_key
RSA_sign
RSA_verify
Sin más que añadir este es el final de articulo.
Salu2.
PD: Es una putada publicar en 2 lados la misma joda pero que las etiquetas sean diferentes ¬¬.
Att: Iker


Comments (0)
Publicar un comentario en la entrada