Go y Golang

Firma digital con Go y RSA

El día de hoy vamos a ver cómo firmar y verificar un mensaje con Golang y un par de claves RSA.

Es decir, por un lado veremos cómo firmar digitalmente un mensaje usando una clave privada y por otro lado vamos a ver cómo comprobar la autenticidad de dicho mensaje.

Recordemos que las firmas digitales tienen el propósito de comprobar la autenticidad de un mensaje, no de ocultarlo (de eso se encarga la criptografía).

Veamos entonces cómo firmar y verificar firmas con Go.

Sobre las claves

Vamos a usar un par de claves. De manera distinta a la criptografía asimétrica aquí vamos a firmar con la privada y distribuir la pública junto con el mensaje plano y firmado.

Puedes generar ese par de claves con openssl o usar tus propios métodos. Eso sí, deben ser claves de tipo RSA. Mira el siguiente tutorial para ver un ejemplo de claves:

Parseando claves

En Go vamos a usar una rsa.PrivateKey y una rsa.PublicKey. Tenemos algunas funciones que van a convertir una string de clave en formato PEM a PrivateKey o PublicKey.

Las funciones no son mías. Quedan así:


func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) {
 block, _ := pem.Decode([]byte(privPEM))
 if block == nil {
  return nil, errors.New("failed to parse PEM block containing the key")
 }

 priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
 if err != nil {
  return nil, err
 }

 return priv, nil
}

func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) {
 block, _ := pem.Decode([]byte(pubPEM))
 if block == nil {
  return nil, errors.New("failed to parse PEM block containing the key")
 }

 pub, err := x509.ParsePKIXPublicKey(block.Bytes)
 if err != nil {
  return nil, err
 }

 switch pub := pub.(type) {
 case *rsa.PublicKey:
  return pub, nil
 default:
  break // fall through
 }
 return nil, errors.New("Key type is not RSA")
}

Básicamente lo que nos permite es convertir una cadena de la clave RSA a un objeto de tipo rsa.PublicKey y rsa.PrivateKey.

Firmar mensaje

Ahora vamos a ver cómo firmar digitalmente con Go. Antes que nada, vamos a calcular la suma de verificación de dicho mensaje y firmar esa suma de verificación, así la longitud de datos se acorta.

func hashearMensaje(mensaje string) ([]byte, error) {
 mensajeComoBytes := []byte(mensaje)
 hasheador := sha256.New()
 _, err := hasheador.Write(mensajeComoBytes)
 if err != nil {
  return []byte{}, err
 }
 return hasheador.Sum(nil), nil
}

No hay riesgo de seguridad al hacer esto, ya que te repito, al final solo estamos comprobando la autenticidad.

La firma se hace con rsa.SignPSS.

Por cierto, al momento de firmar se agrega una sal, así que aunque firmemos el mismo mensaje con la misma clave privada no obtendremos la misma salida pero la verificación seguirá funcionando.

/*
Regresa el mensaje firmado con la clavePrivada pero codificado en base64
*/func firmar(clavePrivadaComoCadena, mensaje string) (string, error) {
 clavePrivada, err := ParseRsaPrivateKeyFromPemStr(clavePrivadaComoCadena)
 if err != nil {
  return "", err
 }
 sumaDeVerificacionDeMensaje, err := hashearMensaje(mensaje)
 if err != nil {
  return "", err
 }

 mensajeFirmado, err := rsa.SignPSS(rand.Reader, clavePrivada, crypto.SHA256, sumaDeVerificacionDeMensaje, nil)
 if err != nil {
  return "", err
 }
 mensajeFirmadoEnBase64, err := base64.StdEncoding.EncodeToString(mensajeFirmado), nil
 return mensajeFirmadoEnBase64, err
}

Después de firmar, codificamos los bytes en base64. Esto solo es porque queremos representar el montón de bytes de una manera amigable, igual podríamos convertirlos a hexadecimal o cosas similares.

Sin importar la codificación que elijas al momento de firmar digitalmente con Go, asegúrate de hacer el proceso inverso al momento de verificar la firma.

Nota: obviamente para firmar vamos a usar la clave privada. Recuerda que esa clave no debe ser distribuida.

Verificar firma

Ahora vamos a verificar la autenticidad de un mensaje. Es decir, vamos a ver si la firma de un mensaje es válida exactamente para ese mensaje y para ello usaremos la clave pública que fue derivada de la privada.

/*
Recibe la clavePublica, el mensaje que va a ser verificado y el mensaje firmadoEnBase64
Si devuelve nil es que está verificado
*/func verificarFirma(clavePublicaComoCadena, mensaje, firmadoEnBase64 string) error {
 clavePublica, err := ParseRsaPublicKeyFromPemStr(clavePublicaComoCadena)
 mensajeFirmado, err := base64.StdEncoding.DecodeString(firmadoEnBase64)
 if err != nil {
  return err
 }
 sumaDeVerificacionDeMensaje, err := hashearMensaje(mensaje)
 if err != nil {
  return err
 }
 err = rsa.VerifyPSS(clavePublica, crypto.SHA256, sumaDeVerificacionDeMensaje, mensajeFirmado, nil)
 if err != nil {
  return err
 }
 return err
}

Para verificar la firma usamos rsa.VerifyPSS. Si la función no devuelve ningún error (es decir, que err es nil) entonces la firma coincide.

Fíjate en que estoy decodificando de base64 en la línea 7, pues yo recibo la firma codificada en ese formato ya que así fue hecho al momento de firmar.

Poniendo todo junto

Veamos entonces estas dos funciones de firmar y verificar autenticidad con Golang.

En este caso lo hacemos en el mismo archivo porque es un ejemplo, pero en la vida real deberías dejar la clave privada y funciones de firma en un lugar, y la clave pública con funciones de verificación en otro.

El uso del código está en la función main.

package main

import (
 "crypto"
 "crypto/rand"
 "crypto/rsa"
 "crypto/sha256"
 "crypto/x509"
 "encoding/base64"
 "encoding/pem"
 "errors"
 "fmt"
)

const ClavePrivada = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----`

const ClavePublica = `-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvn02Fu+stJGZ9MjSjdHE
qHWfvg7zqOuw4BHRTLP2iYUUuv5yzWBJC5g6Kt6nvUSVUuwdggSOvvH3vP8AVNSG
6dM8HEBfXFO5KYbOY2JKoY6tkU37xNWzFS03W24V95kZRUslgFo6xFxVCxqcHkhc
SmwcofYO1fs71gWIiW3yPhB/ZvX6YEGp/0zFYPdRiOAbKCyIhnv5zbEw8jhKYEf7
EaUjy7o1TdiVn1Gd5S6z8k1AChDgkDOtFY0bqqdDtN2/hM0dFdJZ+ifH6SL+Obc3
VPbkNGI93C8KqCaTU3W/pDrh5Kbu3KiMmZOPjiCnXZpIUYQnLKw7zfvwccESwUuR
52LeytM9UUnu03QIAqfmqWC206xGqPSGWgxhNCP9dzjm9y3URfJzFV3i8HAR9/7q
1uQKcwDUjp0ES4joaO3UKjizMLEM9clfU8xXvumf6gAupjxQolwhfiBK9nEPrnNR
tzsFhoDzMFKVWQwIMrTEEkX3O0Fa73SEXSSWYcI5EAXLAgMBAAE=
-----END PUBLIC KEY-----`

func main() {
 mensajeSecreto := "Hola mundo"
 resultado, err := firmar(ClavePrivada, mensajeSecreto)
 fmt.Printf("Resultado: %v error: %v", resultado, err)
 if err == nil {
  err := verificarFirma(ClavePublica, mensajeSecreto, resultado)
  fmt.Printf("Error al verificar: %v", err)
 }
}

/*
Recibe la clavePublica, el mensaje que va a ser verificado y el mensaje firmadoEnBase64
Si devuelve nil es que está verificado
*/func verificarFirma(clavePublicaComoCadena, mensaje, firmadoEnBase64 string) error {
 clavePublica, err := ParseRsaPublicKeyFromPemStr(clavePublicaComoCadena)
 mensajeFirmado, err := base64.StdEncoding.DecodeString(firmadoEnBase64)
 if err != nil {
  return err
 }
 sumaDeVerificacionDeMensaje, err := hashearMensaje(mensaje)
 if err != nil {
  return err
 }
 err = rsa.VerifyPSS(clavePublica, crypto.SHA256, sumaDeVerificacionDeMensaje, mensajeFirmado, nil)
 if err != nil {
  return err
 }
 return err
}

func hashearMensaje(mensaje string) ([]byte, error) {
 mensajeComoBytes := []byte(mensaje)
 hasheador := sha256.New()
 _, err := hasheador.Write(mensajeComoBytes)
 if err != nil {
  return []byte{}, err
 }
 return hasheador.Sum(nil), nil
}

/*
Regresa el mensaje firmado con la clavePrivada pero codificado en base64
*/func firmar(clavePrivadaComoCadena, mensaje string) (string, error) {
 clavePrivada, err := ParseRsaPrivateKeyFromPemStr(clavePrivadaComoCadena)
 if err != nil {
  return "", err
 }
 sumaDeVerificacionDeMensaje, err := hashearMensaje(mensaje)
 if err != nil {
  return "", err
 }

 mensajeFirmado, err := rsa.SignPSS(rand.Reader, clavePrivada, crypto.SHA256, sumaDeVerificacionDeMensaje, nil)
 if err != nil {
  return "", err
 }
 mensajeFirmadoEnBase64, err := base64.StdEncoding.EncodeToString(mensajeFirmado), nil
 return mensajeFirmadoEnBase64, err
}

func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) {
 block, _ := pem.Decode([]byte(privPEM))
 if block == nil {
  return nil, errors.New("failed to parse PEM block containing the key")
 }

 priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
 if err != nil {
  return nil, err
 }

 return priv, nil
}

func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) {
 block, _ := pem.Decode([]byte(pubPEM))
 if block == nil {
  return nil, errors.New("failed to parse PEM block containing the key")
 }

 pub, err := x509.ParsePKIXPublicKey(block.Bytes)
 if err != nil {
  return nil, err
 }

 switch pub := pub.(type) {
 case *rsa.PublicKey:
  return pub, nil
 default:
  break // fall through
 }
 return nil, errors.New("Key type is not RSA")
}

Al compilarlo con go build y ejecutar el archivo compilado tenemos la siguiente salida de ejemplo:

Firma digital con Golang y RSA

En este caso el error es nil así que no hubo error, por lo tanto el mensaje fue firmado y verificado correctamente.

Recuerda que si vas a comprobar la firma necesitarás la clave pública, el mensaje en “texto plano” y la firma en base64.

Nota importante: aunque en el código la variable se llama mensajeSecreto me equivoqué al ponerle ese nombre. Te repito que aquí no vamos a encriptar ni esconder nada, solo vamos a firmar digitalmente ese mensaje con la clave privada.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

17 horas hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

1 semana hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

1 semana hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

1 semana hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

1 semana hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

1 semana hace

Esta web usa cookies.