Saltar al contenido principal

Firma del formulario HTML

El cliente puede cambiar cualquier campo del formulario directamente en el navegador —por ejemplo, poner un importe más bajo. Por eso los campos de la factura viajan en un contenedor firmado: el servicio recalcula la firma y, si no coincide, no crea la factura.

La firma la calcula el servidor de la tienda. Al navegador solo llega el par terminado de data y signature.

Qué se firma

data = base64url(json)
signature = HMAC-SHA256(data, formSecret)

El HMAC se calcula sobre la cadena data, no sobre los bytes del JSON. El receptor verifica la firma contra la cadena enviada tal cual, y solo después la decodifica.

De ahí que no haya canonicalización: el orden de las claves, la sangría del JSON y el estilo de escape Unicode no afectan a la firma. Sirve cualquier codificador JSON estándar.

Conjunto de referencia

Introduce estos valores en tu código y compara el resultado.

El secreto de ejemplo:

dvc3khto5WpjEPojZfYJsbJzFEPPVnRa

El importe y el plazo de pago como números; también se aceptan cadenas:

{"projectId":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d","amountFiat":10.5,"currencyFiat":"USD","timeToPay":1,"description":"Order #7, delivery","serviceData":"order-7"}

La cadena data es el base64url de esos bytes. Aquí el relleno está eliminado; el receptor también la acepta con relleno:

eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0

La firma de esta cadena con el secreto de arriba:

09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e

Con estos valores, los ejemplos de abajo imprimen exactamente este data y esta firma. El mismo JSON con la clave "output":"errors" al final produce una cadena data distinta y una firma distinta.

Si data no coincide aunque el JSON parezca el mismo, la causa es el codificador JSON: otro orden de claves o espacios después de los separadores. El receptor aceptará ese contenedor, pero no coincidirá literalmente con la referencia.

Ejemplos

<?php

function signedContainer(array $fields, string $formSecret): array {
$data = rtrim(strtr(base64_encode(json_encode($fields)), '+/', '-_'), '=');

return [
'data' => $data,
'signature' => hash_hmac('sha256', $data, $formSecret),
];
}

// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
$container = signedContainer([
'projectId' => 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
'amountFiat' => 10.5,
'currencyFiat' => 'USD',
'timeToPay' => 1,
'description' => 'Order #7, delivery',
'serviceData' => 'order-7',
], 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');

echo $container['data'], "\n";
echo $container['signature'], "\n";

Los valores del ejemplo son ASCII, pero el código funciona sin cambios con cualquier Unicode. PHP y Python escapan lo no ASCII como \uXXXX por defecto, JavaScript envía UTF-8 sin procesar —el receptor acepta ambos.

Dos errores comunes

Se firma el JSON en lugar del contenedor. El HMAC se calcula sobre la cadena data, es decir, sobre el base64url. El error se manifiesta como «todo coincide, pero la firma no».

El contenedor se reconstruye después de firmar. Si cambia aunque sea un byte de data, la firma deja de ser válida. Construye el par en un único punto de tu código.

Si la firma no coincide

Recorre la cadena JSON → data → firma; el primer eslabón que diverge de la referencia es la causa. Comprueba data primero: no depende del secreto.