# HTML फ़ॉर्म का हस्ताक्षर

ग्राहक ब्राउज़र में ही फ़ॉर्म का कोई भी फ़ील्ड बदल सकता है — जैसे, कम राशि रख सकता है। इसीलिए इनवॉइस के फ़ील्ड साइन किए हुए कंटेनर में जाते हैं: सेवा हस्ताक्षर दोबारा निकालती है और मेल न खाने पर इनवॉइस नहीं बनाती।

हस्ताक्षर की गणना स्टोर का सर्वर करता है। ब्राउज़र में सिर्फ़ तैयार `data` और `signature` की जोड़ी जाती है।

## किस पर हस्ताक्षर होता है

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

HMAC की गणना `data` स्ट्रिंग पर होती है, JSON के बाइट पर नहीं। रिसीवर हस्ताक्षर की जाँच सबमिट हुई स्ट्रिंग पर ज्यों की त्यों करता है, और उसके बाद ही उसे डीकोड करता है।

इससे यह निकलता है कि कोई canonicalization नहीं है: कुंजियों का क्रम, 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 है। यहाँ padding हटाई गई है; रिसीवर padding के साथ भी स्वीकार करता है:

```
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
```

ऊपर वाली गुप्त कुंजी से इस स्ट्रिंग का हस्ताक्षर:

```
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
```

इन मानों के साथ नीचे के उदाहरण ठीक यही `data` और हस्ताक्षर छापते हैं। वही JSON आख़िर में `"output":"errors"` कुंजी के साथ दूसरी `data` स्ट्रिंग और दूसरा हस्ताक्षर देता है।

अगर JSON एक जैसा दिखता है, पर `data` मेल नहीं खाता, तो कारण 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 डिफ़ॉल्ट रूप से non-ASCII को `\uXXXX` की तरह एस्केप करते हैं, JavaScript raw UTF-8 भेजता है — रिसीवर दोनों स्वीकार करता है।

## दो आम ग़लतियाँ

**कंटेनर की जगह JSON पर हस्ताक्षर।** HMAC की गणना `data` स्ट्रिंग पर होती है, यानी base64url पर। यह ग़लती ऐसी दिखती है कि “सब कुछ मेल खाता है, बस हस्ताक्षर नहीं खाता”।

**हस्ताक्षर के बाद कंटेनर दोबारा बनाया जाता है।** `data` का एक भी बाइट बदलने पर हस्ताक्षर अमान्य है। जोड़ी अपने कोड में एक ही जगह बनाएँ।

## अगर हस्ताक्षर मेल नहीं खाता

JSON → `data` → हस्ताक्षर की कड़ी पर चलें; जो पहली कड़ी संदर्भ से अलग पड़ती है, वही कारण है। पहले `data` जाँचें: यह गुप्त कुंजी पर निर्भर नहीं करता।
