HTML फ़ॉर्म का हस्ताक्षर
ग्राहक ब्राउज़र में ही फ़ॉर्म का कोई भी फ़ील्ड बदल सकता है — जैसे, कम राशि रख सकता है। इसीलिए इनवॉइस के फ़ील्ड साइन किए हुए कंटेनर में जाते हैं: सेवा हस्ताक्षर दोबारा निकालती है और मेल न खाने पर इनवॉइस नहीं बनाती।
हस्ताक्षर की गणना स्टोर का सर्वर करता है। ब्राउज़र में सिर्फ़ तैयार data और signature की जोड़ी जाती है।
किस पर हस्ताक्षर होता है
data = base64url(json)
signature = HMAC-SHA256(data, formSecret)
HMAC की गणना data स्ट्रिंग पर होती है, JSON के बाइट पर नहीं। रिसीवर हस्ताक्षर की जाँच सबमिट हुई स्ट्रिंग पर ज्यों की त्यों करता है, और उसके बाद ही उसे डीकोड करता है।
इससे यह निकलता है कि कोई canonicalization नहीं है: कुंजियों का क्रम, 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 है। यहाँ padding हटाई गई है; रिसीवर padding के साथ भी स्वीकार करता है:
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
ऊपर वाली गुप्त कुंजी से इस स्ट्रिंग का हस्ताक्षर:
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
इन मानों के साथ नीचे के उदाहरण ठीक यही data और हस्ताक्षर छापते हैं। वही JSON आख़िर में "output":"errors" कुंजी के साथ दूसरी data स्ट्रिंग और दूसरा हस्ताक्षर देता है।
अगर JSON एक जैसा दिखता है, पर data मेल नहीं खाता, तो कारण 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 डिफ़ॉल्ट रूप से non-ASCII को \uXXXX की तरह एस्केप करते हैं, JavaScript raw UTF-8 भेजता है — रिसीवर दोनों स्वीकार करता है।
दो आम ग़लतियाँ
कंटेनर की जगह JSON पर हस्ताक्षर। HMAC की गणना data स्ट्रिंग पर होती है, यानी base64url पर। यह ग़लती ऐसी दिखती है कि “सब कुछ मेल खाता है, बस हस्ताक्षर नहीं खाता”।
हस्ताक्षर के बाद कंटेनर दोबारा बनाया जाता है। data का एक भी बाइट बदलने पर हस्ताक्षर अमान्य है। जोड़ी अपने कोड में एक ही जगह बनाएँ।
अगर हस्ताक्षर मेल नहीं खाता
JSON → data → हस्ताक्षर की कड़ी पर चलें; जो पहली कड़ी संदर्भ से अलग पड़ती है, वही कारण है। पहले data जाँचें: यह गुप्त कुंजी पर निर्भर नहीं करता।