# توقيع نموذج HTML

يستطيع المشتري تغيير أي حقل في النموذج مباشرة في المتصفح — مثلًا وضع مبلغ أقل. لهذا تنتقل حقول الفاتورة في حاوية موقَّعة: تعيد الخدمة حساب التوقيع، وإذا لم يتطابق، لا تنشئ الفاتورة.

خادم المتجر هو الذي يحسب التوقيع. ولا يذهب إلى المتصفح إلا الزوج الجاهز `data` و`signature`.

## ما الذي يُوقَّع

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

يُحسب HMAC على السلسلة `data`، لا على بايتات JSON. تتحقق جهة الاستقبال من التوقيع مقابل السلسلة المرسلة كما هي، ثم تفكّ ترميزها بعد ذلك فقط.

يترتب على ذلك أنه لا وجود لصيغة قانونية موحدة: ترتيب المفاتيح وإزاحة JSON وأسلوب تهريب Unicode لا تؤثر في التوقيع. وأي مرمّز JSON قياسي يفي بالغرض.

## المجموعة المرجعية

ضع هذه القيم في كودك وقارن الناتج.

المفتاح السري في المثال:

```
dvc3khto5WpjEPojZfYJsbJzFEPPVnRa
```

المبلغ ومهلة الدفع كأرقام؛ وتُقبل السلاسل النصية أيضًا:

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

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

هذا كود لخادم المتجر لا للمتصفح: يُحسب التوقيع حيث يُخزَّن المفتاح السري.

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

المعطى `separators` لازم للتطابق الحرفي مع المرجع. وجهة الاستقبال راضية عن إعدادات JSON الافتراضية أيضًا.

قيم المثال بترميز ASCII، لكن الكود يعمل مع أي Unicode من دون تغيير. يهرّب PHP وPython ما ليس ASCII إلى `\uXXXX` افتراضيًا، بينما يرسل JavaScript بايتات UTF-8 الخام — وتقبل جهة الاستقبال كليهما.

## خطآن شائعان

**يُوقَّع JSON بدل الحاوية.** يُحسب HMAC على السلسلة `data`، أي على base64url. يبدو الخطأ بالشكل «كل شيء متطابق، لكن التوقيع لا يتطابق».

**يُعاد بناء الحاوية بعد التوقيع.** إذا تغيّر بايت واحد من `data`، بطل التوقيع. ابنِ الزوج في مكان واحد من كودك.

## إذا لم يتطابق التوقيع

تتبَّع السلسلة من JSON إلى `data` إلى التوقيع؛ أول حلقة تنحرف عن المرجع هي السبب. تحقق من `data` أولًا: فهي لا تعتمد على المفتاح السري.
