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 :
{"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
- 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);
Ceci est du code pour le serveur de la boutique, pas pour le navigateur : la signature se calcule là où le secret est stocké.
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'])
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.