128 / Avanzado

Verificar un aviso de pago firmado

Una pasarela de pago avisa un pago con un POST cuyo cuerpo viene firmado con RSA; la firma llega en base64 en un header. Con HMAC no alcanza porque el proveedor no comparte un secreto: publica su CLAVE PÚBLICA (PEM) y uno verifica con ella. std/rsa lo hace con OpenSSL.

128-rsa-verificar-webhook.nxFuente →
// Nyx by Example: verificar el aviso firmado de una pasarela de pago (RSA-SHA256).
//
// El caso real: la pasarela avisa un pago con un POST cuyo cuerpo viene firmado
// con RSA; la firma llega en base64 en un header. Con HMAC no alcanza porque el
// proveedor no comparte un secreto: publica su CLAVE PÚBLICA (PEM) y uno
// verifica con ella. std/rsa lo hace con OpenSSL:
//   - rsa_sha256_verify_b64(pem, cuerpo, firma_base64): PKCS#1 v1.5, lo común;
//   - rsa_sha256_verify(pem, cuerpo, firma_cruda): la misma firma en bytes;
//   - rsa_pss_sha256_verify(pem, cuerpo, firma_cruda): RSASSA-PSS.
// Ok(true) = firma válida; Ok(false) = el aviso NO es del proveedor (datos
// alterados o clave ajena); Err = no se pudo ni intentar (PEM ilegible).
// Se verifica el cuerpo EXACTO recibido, byte a byte, antes de parsearlo.
//
// Los datos de la receta son los fixtures de prueba del repo (openssl CLI). En un
// programa real: la clave con read_file("proveedor.pem") y el cuerpo y la firma
// de la petición entrante.

import "std/rsa"
import "std/error"

fn aviso_valido(pem: String, cuerpo: String, firma_b64: String) -> bool {
    match rsa_sha256_verify_b64(pem, cuerpo, firma_b64) {
        Result.Ok(ok) => { return ok }
        Result.Err(e) => {
            print("no se pudo verificar: " + e.msg)
            return false
        }
    }
}

fn main() {
    let pem: String = include_bytes("tests/fixtures/rsa/publica.pem")
    let cuerpo: String = include_bytes("tests/fixtures/rsa/datos.txt")
    let firma: String = include_bytes("tests/fixtures/rsa/firma.b64")

    assert(aviso_valido(pem, cuerpo, firma), "el aviso genuino verifica")
    print("aviso genuino: aceptado")

    // Un atacante cambia el monto: la firma ya no corresponde.
    assert(aviso_valido(pem, cuerpo + " ", firma) == false, "un cuerpo alterado se rechaza")
    print("cuerpo alterado: rechazado")

    // Una clave que no se puede leer se dice, no se toma por «firma falsa».
    assert(aviso_valido("no soy un pem", cuerpo, firma) == false, "PEM ilegible")
}
Salidastdout
aviso genuino: aceptado
cuerpo alterado: rechazado
no se pudo verificar: rsa: la clave pública PEM no se pudo leer o no es RSA

Cómo funciona

Tres puntos de entrada: rsa_sha256_verify_b64(pem, cuerpo, firma_base64) para PKCS#1 v1.5, lo común; rsa_sha256_verify(pem, cuerpo, firma_cruda) para la misma firma en bytes; y rsa_pss_sha256_verify(pem, cuerpo, firma_cruda) para RSASSA-PSS. La respuesta tiene tres desenlaces: Ok(true), firma válida; Ok(false), el aviso NO es del proveedor (datos alterados o clave ajena); y Err, ni siquiera se pudo intentar, como con un PEM ilegible — las tres líneas de la salida.

Se verifica el cuerpo EXACTO recibido, byte a byte, ANTES de parsearlo. Los datos de la receta son los fixtures de prueba del repositorio, hechos con la CLI de openssl; en un programa real la clave sale de read_file("proveedor.pem") y el cuerpo y la firma, de la petición entrante.