Gestión de llaves criptográficas: solucionando errores de formato con Base64

Gestión de llaves criptográficas: solucionando errores de formato con Base64

Actualmente la seguridad en el desarrollo de aplicaciones se ha convertido en una prioridad y la gestión adecuada de llaves criptográficas es más crucial que nunca. La mayoría de las plataformas modernas emplean claves públicas y privadas para autenticar aplicaciones cliente y su conexión con APIs. Sin embargo, esto introduce desafíos únicos y complejos, como el problema que trataré de explicar en este artículo y que fue algo que encontré en una de las aplicaciones en las cuales trabajo.

Identificando el Problema

Durante la implementación de una librería para crear tokens de autenticación, me encontré con el siguiente mensaje de error en un entorno en la nube: SignatureError: error:1E08010C:DECODER routines::unsupported. Curiosamente, la aplicación funcionaba perfectamente en un entorno de desarrollo local. Pero porque fallaria en producción y no en el ambiente local, siendo que las configuraciones son las mismas? Yo solo tenía dos posibles culpables, uno de ellos era una clave mal escrita o un problema con la llave privada, y luego de varias horas de investigación, depuración y análisis de logs, pude dar con el problema, el cual resultó ser un problema entre las variables de entorno y la codificacion de las llaves.

Por ejemplo, una llave privada almacenada en un archivo .env podría verse así:

PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\nMIGbMBAGByqGkxDlT8awerfs5ghHFF54Ddde43SJ37
6QOKmLpw0VHJC2V7J3B/GGW1NN24J5S4QAcbqpo\nRBlptQveHvFs6Bf0/KGuxEKnt5N8+1d6LN+75UZHxn/f\nN1F/DUH8deWy4E1L93AHAqwwU9dk73/4VX113Rc==\n-----END EC PRIVATE KEY-----%"

que luego se utilizaría en el código de la siguiente manera (esta es la implementación sugerida por los autores de la librería):

// Recuperando la llave privada de una variable de entorno
const privateKeyPem = process.env.PRIVATE_KEY;
if (!privateKeyPem) throw new Error("Missing env var PRIVATE_KEY");

// A random body string is enough for this request as `/test-signature` endpoint does not 
// require any schema, it simply checks the signature is valid against what's received.
const body = `body-${Math.random()}`;
const idempotencyKey = new Date().getTime().toString();
const tlSignature = tlSigning.sign({
  kid: 'el secret key va aqui',
  privateKeyPem,
  method: "POST", // as we're sending a POST request
  path: "/test-signature", // the path of our request
  // Optional: /test-signature does not require any headers, but we may sign some anyway.
  // All signed headers *must* be included unmodified in the request.
  headers: {
    "Idempotency-Key": idempotencyKey,
    "X-Bar-Header": "abcdefg",
  },
  body,
});

Aparentemente esto deberia funcionar y si funcionaba pero solamente en un entorno local, pero aún asi, a pesar de parecer correcto, este enfoque fallaba en el entorno en la nube. La razón subyacente era la manipulación de los saltos de línea en las variables de entorno, un detalle crucial para el formato PEM.

Y claro, localmente funciona porque el archivo .env es manipulado directamente por un desarrollador, mientras que en un ambiente productivo, el archivo .env es generado o automaticamente o gestionado a traves de alguna herramientas de secretos como Github Secrets o el AWS Parameters Store, lo cual puede mal entender los separadores de linea "\n".

La Solución: Codificación Base64

La solución a este problema fue doble. Primero, decidí codificar la llave privada en Base64. Esta técnica permite convertir la llave en una cadena de caracteres sin saltos de línea, facilitando su almacenamiento en una variable de entorno como una sola línea de texto. Luego, en la aplicación, la cadena codificada, se decodifica para regresarla de nuevo a su formato original. Este proceso garantiza que la llave privada se mantenga intacta durante su almacenamiento y recuperación, preservando todos los caracteres necesarios, incluidos los saltos de línea.

Entonces, usando una consola de linux en mi caso, solamente fue necesario poner el contenido de la llave privada en un archivo, por ejemplo privateKey.pem y codificarla con el comando base64.

base64 privateKey.pem > privateKeyBase64.txt

Al hacer esto, se produce una cadena codificada que contiene la llave privada, dependiendo del sistema operativo, en algunos casos la cadena resultante puede o no tener saltos de linea, pero esto solamente es para efectos de visibilidad, ya que se pueden eliminar y dejar la cadena en una sola linea.

Una vez que la llave privada codificada podemos intentar ponerla nuevamente en nuestro archivo .env:

PRIVATE_KEY="LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JR2JNQkFHQnlxR2t4RGxUOGF3ZXJmczVnaEhGRjU0RGRkZTQzU0ozNwo2UU9LbUxwdzBWSEpDMlY3SjNCL0dHVzFOTjI0SjVTNFFBY2JxcG8KUkJscHRRdmVIdkZzNkJmMC9LR3V4RUtudDVOOCsxZDZMTis3NVVaSHhuL2YKTjFGL0RVSDhkZVd5NEUxTDkzQUhBcXd3VTlkazczLzRWWDExM1JjPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="

Pero a diferencia del codigo anterior, ahora en lugar de utilizar directamente la llave desde la variable de ambiente, la decodificaremos y su contenido sera el que usaremos como privateKeyPem.

// Recuperando la llave privada de una variable de entorno
const privateKeyPem = process.env.PRIVATE_KEY;
if (!privateKeyPem) throw new Error("Missing env var PRIVATE_KEY");

// aqui es donde ocurre la magia, porque regresaremos la cadena codificada a su estado original
const Buffer = require('buffer').Buffer;
const privateKeyPemDecoded = Buffer.from(privateKeyPem, 'base64').toString('utf-8');

const body = `body-${Math.random()}`;
const idempotencyKey = new Date().getTime().toString();
const tlSignature = tlSigning.sign({
  kid: 'el secret key va aqui',
  privateKeyPem: privateKeyPemDecoded,
  method: "POST", // as we're sending a POST request
  path: "/test-signature", // the path of our request
  // Optional: /test-signature does not require any headers, but we may sign some anyway.
  // All signed headers *must* be included unmodified in the request.
  headers: {
    "Idempotency-Key": idempotencyKey,
    "X-Bar-Header": "abcdefg",
  },
  body,
});

Este método preservó la integridad de la llave privada durante su almacenamiento y recuperación, manteniendo su formato exacto, incluidos los saltos de línea.

Conclusión

Definitivamente este caso resalta la importancia de los detalles en la programación, especialmente en aspectos críticos como la gestión de llaves criptográficas. La codificación en Base64 es solución eficaz para evitar problemas comunes relacionados con los formatos de llave. Este incidente también subraya la importancia de probar y validar el manejo de llaves en diferentes entornos. Compartir estas experiencias puede ayudar a otros desarrolladores a prevenir o resolver rápidamente problemas similares, reforzando así la seguridad de sus aplicaciones.

Espero que este artículo te ayuda a entender porque no debes utilizar llaves publicas o privadas directamente desde variables de entorno y como encontrar una alternativa a su uso a través de la codificación de base64.

Happy coding! :D


Photo by Mauro Sbicego on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?