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
- TypeScript
- JavaScript
- Python
<?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";
import { createHmac } from 'node:crypto';
// Values are JSON strings or numbers, optional keys may be omitted
export interface FormFields {
projectId: string;
amountFiat: string | number;
currencyFiat: string;
timeToPay: string | number;
description?: string;
serviceData?: string;
output?: string;
}
export interface FormContainer {
data: string;
signature: string;
}
export const signedContainer = (fields: FormFields, formSecret: string): FormContainer => {
const data = Buffer.from(JSON.stringify(fields), 'utf8').toString('base64url');
return {
data,
signature: createHmac('sha256', formSecret).update(data, 'utf8').digest('hex'),
};
};
// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
const container = signedContainer({
projectId: 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
amountFiat: 10.5,
currencyFiat: 'USD',
timeToPay: 1,
description: 'Order #7, delivery',
serviceData: 'order-7',
}, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');
console.log(container.data);
console.log(container.signature);
Este código es para el servidor de la tienda, no para el navegador: la firma se calcula donde se guarda el secreto.
import { createHmac } from 'node:crypto';
const signedContainer = (fields, formSecret) => {
const data = Buffer.from(JSON.stringify(fields), 'utf8').toString('base64url');
return {
data,
signature: createHmac('sha256', formSecret).update(data, 'utf8').digest('hex'),
};
};
// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
const container = signedContainer({
projectId: 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
amountFiat: 10.5,
currencyFiat: 'USD',
timeToPay: 1,
description: 'Order #7, delivery',
serviceData: 'order-7',
}, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');
console.log(container.data);
console.log(container.signature);
import base64
import hashlib
import hmac
import json
def signed_container(fields: dict, form_secret: str) -> dict:
# compact separators: the default ones add spaces after ',' and ':'
body = json.dumps(fields, separators=(',', ':'))
data = base64.urlsafe_b64encode(body.encode('utf-8')).decode().rstrip('=')
return {
'data': data,
'signature': hmac.new(
form_secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256
).hexdigest(),
}
# Test values from the reference set. Before going live,
# replace the secret with the form secret from your project settings
container = signed_container({
'projectId': 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
'amountFiat': 10.5,
'currencyFiat': 'USD',
'timeToPay': 1,
'description': 'Order #7, delivery',
'serviceData': 'order-7',
}, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa')
print(container['data'])
print(container['signature'])
El argumento separators hace falta para coincidir literalmente con la referencia. Al receptor también le sirve la configuración JSON por defecto.
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.