# Chữ ký biểu mẫu HTML

Người mua có thể sửa bất kỳ trường nào của biểu mẫu ngay trong trình duyệt — ví dụ, đặt số tiền thấp hơn. Vì vậy các trường hóa đơn đi trong container đã ký: dịch vụ tính lại chữ ký và, nếu không khớp, không tạo hóa đơn.

Máy chủ của cửa hàng tính chữ ký. Chỉ cặp `data` và `signature` hoàn chỉnh mới đi đến trình duyệt.

## Ký cái gì

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

HMAC tính trên chuỗi `data`, không phải trên các byte JSON. Bên nhận xác minh chữ ký với đúng chuỗi được gửi, rồi mới giải mã nó.

Từ đó suy ra không có chuẩn hóa: thứ tự khóa, thụt lề JSON và kiểu thoát Unicode không ảnh hưởng đến chữ ký. Bất kỳ bộ mã hóa JSON chuẩn nào cũng dùng được.

## Bộ tham chiếu

Đưa các giá trị này vào mã của bạn và so sánh kết quả.

Khóa bí mật ví dụ:

```
dvc3khto5WpjEPojZfYJsbJzFEPPVnRa
```

Số tiền và thời hạn thanh toán dưới dạng số; chuỗi cũng được chấp nhận:

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

Chuỗi `data` là base64url của các byte đó. Ở đây phần đệm đã được lược bỏ; bên nhận cũng chấp nhận có phần đệm:

```
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
```

Chữ ký của chuỗi này với khóa bí mật ở trên:

```
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
```

Với các giá trị này, những ví dụ bên dưới in ra đúng `data` và chữ ký này. Cùng JSON đó nhưng thêm khóa `"output":"errors"` ở cuối sẽ cho chuỗi `data` khác và chữ ký khác.

Nếu `data` không khớp trong khi JSON trông giống nhau, nguyên nhân là bộ mã hóa JSON: thứ tự khóa khác hoặc khoảng trắng sau dấu phân tách. Bên nhận vẫn chấp nhận container như vậy, nhưng nó sẽ không trùng khớp từng ký tự với tham chiếu.

## Ví dụ

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

Đây là mã cho máy chủ của cửa hàng, không phải trình duyệt: chữ ký tính ở nơi lưu khóa bí mật.

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

Đối số `separators` cần cho việc trùng khớp từng ký tự với tham chiếu. Bên nhận cũng chấp nhận cài đặt JSON mặc định.

Các giá trị ví dụ là ASCII, nhưng mã chạy với mọi Unicode mà không cần sửa. PHP và Python mặc định thoát ký tự ngoài ASCII thành `\uXXXX`, JavaScript gửi UTF-8 thô — bên nhận chấp nhận cả hai.

## Hai lỗi thường gặp

**Ký JSON thay vì container.** HMAC tính trên chuỗi `data`, tức là trên base64url. Lỗi này biểu hiện như “mọi thứ đều khớp, nhưng chữ ký thì không”.

**Dựng lại container sau khi ký.** Chỉ cần một byte của `data` thay đổi, chữ ký mất hiệu lực. Hãy dựng cặp này ở một chỗ duy nhất trong mã của bạn.

## Nếu chữ ký không khớp

Đi dọc chuỗi JSON → `data` → chữ ký; mắt xích đầu tiên lệch khỏi tham chiếu chính là nguyên nhân. Kiểm tra `data` trước: nó không phụ thuộc vào khóa bí mật.
