توقيع نموذج HTML
يستطيع المشتري تغيير أي حقل في النموذج مباشرة في المتصفح — مثلًا وضع مبلغ أقل. لهذا تنتقل حقول الفاتورة في حاوية موقَّعة: تعيد الخدمة حساب التوقيع، وإذا لم يتطابق، لا تنشئ الفاتورة.
خادم المتجر هو الذي يحسب التوقيع. ولا يذهب إلى المتصفح إلا الزوج الجاهز data وsignature.
ما الذي يُوقَّع
data = base64url(json)
signature = HMAC-SHA256(data, formSecret)
يُحسب HMAC على السلسلة data، لا على بايتات JSON. تتحقق جهة الاستقبال من التوقيع مقابل السلسلة المرسلة كما هي، ثم تفكّ ترميزها بعد ذلك فقط.
يترتب على ذلك أنه لا وجود لصيغة قانونية موحدة: ترتيب المفاتيح وإزاحة JSON وأسلوب تهريب Unicode لا تؤثر في التوقيع. وأي مرمّز JSON قياسي يفي بالغرض.
المجموعة المرجعية
ضع هذه القيم في كودك وقارن الناتج.
المفتاح السري في المثال:
dvc3khto5WpjEPojZfYJsbJzFEPPVnRa
المبلغ ومهلة الدفع كأرقام؛ وتُقبل السلاسل النصية أيضًا:
{"projectId":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d","amountFiat":10.5,"currencyFiat":"USD","timeToPay":1,"description":"Order #7, delivery","serviceData":"order-7"}
السلسلة data هي base64url لتلك البايتات. الحشو محذوف هنا؛ وتقبله جهة الاستقبال مع الحشو أيضًا:
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
توقيع هذه السلسلة بالمفتاح السري أعلاه:
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
بهذه القيم تطبع الأمثلة أدناه هذه القيمة data وهذا التوقيع بالضبط. وJSON نفسه مع المفتاح "output":"errors" في النهاية ينتج سلسلة data مختلفة وتوقيعًا مختلفًا.
إذا لم تتطابق data بينما يبدو JSON نفسه، فمرمّز JSON هو السبب: ترتيب مفاتيح مختلف أو مسافات بعد الفواصل. ستقبل جهة الاستقبال حاوية كهذه، لكنها لن تطابق المرجع حرفيًا.
أمثلة
- 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);
هذا كود لخادم المتجر لا للمتصفح: يُحسب التوقيع حيث يُخزَّن المفتاح السري.
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'])
المعطى separators لازم للتطابق الحرفي مع المرجع. وجهة الاستقبال راضية عن إعدادات JSON الافتراضية أيضًا.
قيم المثال بترميز ASCII، لكن الكود يعمل مع أي Unicode من دون تغيير. يهرّب PHP وPython ما ليس ASCII إلى \uXXXX افتراضيًا، بينما يرسل JavaScript بايتات UTF-8 الخام — وتقبل جهة الاستقبال كليهما.
خطآن شائعان
يُوقَّع JSON بدل الحاوية. يُحسب HMAC على السلسلة data، أي على base64url. يبدو الخطأ بالشكل «كل شيء متطابق، لكن التوقيع لا يتطابق».
يُعاد بناء الحاوية بعد التوقيع. إذا تغيّر بايت واحد من data، بطل التوقيع. ابنِ الزوج في مكان واحد من كودك.
إذا لم يتطابق التوقيع
تتبَّع السلسلة من JSON إلى data إلى التوقيع؛ أول حلقة تنحرف عن المرجع هي السبب. تحقق من data أولًا: فهي لا تعتمد على المفتاح السري.