# Signature du formulaire HTML

Le client peut modifier n'importe quel champ du formulaire directement dans le navigateur — par exemple, mettre un montant plus bas. C'est pourquoi les champs de la facture voyagent dans un conteneur signé : le service recalcule la signature et, si elle ne correspond pas, ne crée pas la facture.

Le serveur de la boutique calcule la signature. Seule la paire finie `data` et `signature` va au navigateur.

## Ce qui est signé

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

Le HMAC est calculé sur la chaîne `data`, pas sur les octets du JSON. Le destinataire vérifie la signature sur la chaîne soumise telle quelle, et seulement ensuite la décode.

Il en découle qu'il n'y a pas de canonicalisation : l'ordre des clés, l'indentation du JSON et le style d'échappement Unicode n'affectent pas la signature. N'importe quel encodeur JSON standard convient.

## Jeu de référence

Insérez ces valeurs dans votre code et comparez le résultat.

Le secret d'exemple :

```
dvc3khto5WpjEPojZfYJsbJzFEPPVnRa
```

Le montant et le délai de paiement comme des nombres ; les chaînes sont acceptées aussi :

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

La chaîne `data` est le base64url de ces octets. Le padding est retiré ici ; le destinataire l'accepte aussi avec padding :

```
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
```

La signature de cette chaîne avec le secret ci-dessus :

```
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
```

Avec ces valeurs, les exemples ci-dessous impriment exactement ces `data` et signature. Le même JSON avec la clé `"output":"errors"` à la fin produit une chaîne `data` différente et une signature différente.

Si `data` ne correspond pas alors que le JSON semble identique, l'encodeur JSON est en cause : un ordre de clés différent ou des espaces après les séparateurs. Le destinataire acceptera un tel conteneur, mais il ne correspondra pas mot pour mot à la référence.

## Exemples

**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**

Ceci est du code pour le serveur de la boutique, pas pour le navigateur : la signature se calcule là où le secret est stocké.

```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'])
```

L'argument `separators` est nécessaire pour une correspondance mot pour mot avec la référence. Le destinataire accepte aussi les réglages JSON par défaut.

Les valeurs d'exemple sont en ASCII, mais le code fonctionne tel quel avec tout Unicode. PHP et Python échappent le non-ASCII en `\uXXXX` par défaut, JavaScript envoie l'UTF-8 brut — le destinataire accepte les deux.

## Deux erreurs courantes

**Le JSON est signé au lieu du conteneur.** Le HMAC est calculé sur la chaîne `data`, c'est-à-dire sur le base64url. L'erreur ressemble à « tout correspond, mais pas la signature ».

**Le conteneur est reconstruit après la signature.** Si un seul octet de `data` change, la signature est invalide. Construisez la paire à un seul endroit de votre code.

## Si la signature ne correspond pas

Parcourez la chaîne JSON → `data` → signature ; le premier maillon qui diverge de la référence est la cause. Vérifiez `data` d'abord : elle ne dépend pas du secret.
