# 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:

```json
{"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**

```php showLineNumbers
<?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";
```

**TypeScript**

```typescript showLineNumbers
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);
```

**JavaScript**

Este código es para el servidor de la tienda, no para el navegador: la firma se calcula donde se guarda el secreto.

```js showLineNumbers
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);
```

**Python**

```python showLineNumbers
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.
