# Tanda tangan formulir HTML

Pembeli dapat mengubah field formulir apa pun langsung di browser—misalnya, memasang jumlah yang lebih kecil. Karena itu field tagihan berjalan dalam kontainer yang ditandatangani: layanan menghitung ulang tanda tangan dan, jika tidak cocok, tidak membuat tagihan.

Server toko yang menghitung tanda tangan. Hanya pasangan `data` dan `signature` yang sudah jadi yang dikirim ke browser.

## Apa yang ditandatangani

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

HMAC dihitung atas string `data`, bukan atas byte JSON. Penerima memverifikasi tanda tangan terhadap string yang dikirim apa adanya, baru kemudian mendekodenya.

Artinya tidak ada kanonikalisasi: urutan kunci, indentasi JSON, dan gaya escaping Unicode tidak memengaruhi tanda tangan. Encoder JSON standar apa pun bisa dipakai.

## Set acuan

Masukkan nilai-nilai ini ke kode Anda dan bandingkan keluarannya.

Kunci rahasia contoh:

```
dvc3khto5WpjEPojZfYJsbJzFEPPVnRa
```

Jumlah dan batas waktu pembayaran sebagai angka; string juga diterima:

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

String `data` adalah base64url dari byte tersebut. Padding di sini dibuang; penerima juga menerimanya dengan padding:

```
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
```

Tanda tangan string ini dengan kunci rahasia di atas:

```
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
```

Dengan nilai-nilai ini, contoh di bawah mencetak `data` dan tanda tangan persis seperti ini. JSON yang sama dengan kunci `"output":"errors"` di akhirnya menghasilkan string `data` yang berbeda dan tanda tangan yang berbeda.

Jika `data` tidak cocok padahal JSON-nya terlihat sama, penyebabnya adalah encoder JSON: urutan kunci berbeda atau ada spasi setelah pemisah. Penerima tetap menerima kontainer semacam itu, tetapi tidak akan sama persis dengan acuan.

## Contoh

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

Ini kode untuk server toko, bukan browser: tanda tangan dihitung di tempat kunci rahasia disimpan.

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

Argumen `separators` diperlukan agar sama persis dengan acuan. Penerima juga tidak masalah dengan pengaturan JSON bawaan.

Nilai contohnya ASCII, tetapi kode ini bekerja dengan Unicode apa pun tanpa perubahan. PHP dan Python secara bawaan meng-escape non-ASCII sebagai `\uXXXX`, JavaScript mengirim UTF-8 mentah—penerima menerima keduanya.

## Dua kesalahan umum

**Yang ditandatangani JSON, bukan kontainer.** HMAC dihitung atas string `data`, yaitu atas base64url. Kesalahan ini tampak seperti “semuanya cocok, tetapi tanda tangannya tidak.”

**Kontainer dibangun ulang setelah penandatanganan.** Jika satu byte saja dari `data` berubah, tanda tangan tidak sah. Bangun pasangan ini di satu tempat dalam kode Anda.

## Jika tanda tangan tidak cocok

Telusuri rantai JSON → `data` → tanda tangan; mata rantai pertama yang menyimpang dari acuan adalah penyebabnya. Periksa `data` lebih dulu: nilainya tidak bergantung pada kunci rahasia.
