跳转到内容
PGSwift Docs
文档/签名生成

签名生成

要生成签名,您需要一对 RSA 私钥和公钥。
您可以自行生成这些密钥,或使用第三方工具(例如 cryptotools.net)来创建。

必填参数
要生成有效的签名,请包含以下参数:

参数描述
METHOD大写的 HTTP 请求方法 (e.g., POST, GET).
URL仅请求路径,以 / 开头 (e.g., /api/checkout)。不包含协议、域名或查询字符串(query string)。路径区分大小写。末尾斜杠会被忽略:/api/balance/ 按 /api/balance 进行签名和验证。
PAYLOAD对实际发送的请求正文的原始字节进行 SHA-256 哈希运算,结果为小写十六进制字符串。对于 GET 请求(无请求正文),对空字符串进行哈希:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。
TIMESTAMP当前时间戳,采用带有明确时区偏移的 ISO8601 格式(例如 +07:00 或 Z),且须与服务器时间相差在 ±5 分钟以内。参阅时间戳规则。

参数拼接

签名字符串(stringToSign)通过 : 按以下格式将参数依次拼接而成:

stringToSign = {METHOD}:{URL}:{PAYLOAD}:{TIMESTAMP}

将占位符替换为实际值:

  • {METHOD} → 大写的 HTTP 请求方法。
  • {URL} → 仅请求路径 (e.g., /api/checkout)。不含查询字符串,区分大小写,不含末尾斜杠。
  • {PAYLOAD} → 请求正文原始字节的 SHA-256 哈希值(小写十六进制)。对于 GET 请求,使用空字符串的哈希值(见上文)。
  • {TIMESTAMP} → 当前时间戳,采用带有明确时区偏移的 ISO8601 格式,并须与 X-TIMESTAMP 请求头的值完全一致

对实际发送的正文进行哈希

服务器按接收到的原始请求正文逐字节计算哈希,不会解析或重新序列化 JSON。

  • 只将请求正文序列化为字符串一次。对该字符串进行哈希,并将同一字符串作为请求正文发送。
  • 不要对重新序列化后的副本进行哈希(例如 JSON.stringify(JSON.parse(body))、json_encode(json_decode($body)) 或 json.dumps(json.loads(body)))。键的顺序、空白字符、斜杠转义(\/)、Unicode 转义和数字格式都可能改变,导致签名不匹配。
  • 签名后不要让 HTTP 客户端重新编码请求正文(例如传入对象而不是已签名的字符串)。

签名算法

使用私钥对 stringToSign(UTF-8 字节)进行签名,算法为 RSA + SHA-256,填充方式为 PKCS#1 v1.5(RSASSA-PKCS1-v1_5,也称为 RSA-SHA256 或 SHA256withRSA)。不要使用 PSS 填充。将结果进行 Base64 编码后放入 X-SIGNATURE 请求头。

const crypto = require('crypto');
const fs = require('fs');

const httpMethod = 'POST';
const endpointUrl = '/api/create/qris'; // 仅路径,不含查询字符串
const timestamp = new Date().toISOString(); // e.g. 2024-12-16T05:11:14.123Z

// 只序列化一次。将这个字符串原样作为请求正文发送。
// GET 请求使用: const body = '';
const body = JSON.stringify({
  merchant_transaction_id: 'ORDER-001',
  amount: '50000',
  currency: 'IDR',
  customer_name: 'John Doe',
  product_name: 'Test Order',
});

const payloadHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
const stringToSign = `${httpMethod}:${endpointUrl}:${payloadHash}:${timestamp}`;

console.log('String to Sign:', stringToSign);

// RSA-SHA256,PKCS#1 v1.5 填充(Node.js 对 RSA 密钥的默认值)
const privateKey = fs.readFileSync('./path-to-private-key-file.pem');
const signature = crypto.sign('sha256', Buffer.from(stringToSign, 'utf8'), {
  key: privateKey,
  padding: crypto.constants.RSA_PKCS1_PADDING,
}).toString('base64');

console.log('Generated Signature:', signature);
// 原样发送 `body`,并附带请求头 X-TIMESTAMP: timestamp, X-SIGNATURE: signature
  • 请确保私钥被安全存储,且不要在公共代码库中公开。
  • 每次请求都必须重新生成 TIMESTAMP,采用带有明确时区偏移的 ISO8601 格式。须与服务器时间相差在 ±5 分钟以内,超出该范围的请求可能被拒绝(参阅时间戳规则)。

测试向量

使用以下数据检查您的实现。RSA PKCS#1 v1.5 签名是确定性的,因此使用下方测试密钥时,您的代码必须生成完全相同的哈希值、stringToSign 和签名。这些测试向量已通过 OpenSSL 验证。它们仅用于离线验证,请勿发送到 API:固定的时间戳不在允许的时间范围内。

测试私钥 (PEM)
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSH6vw8qTE2hBN
gzCPOKk0VfpJg3CBXFC0SmByTnUXJS8sQDqBBga8essnUGJgwpTVAiQ+G/H/UkfK
pPVxieAFO06JPMK2M7thj1gUgH6APpN2l/M4G3uO04wElFNpJKE+kDwZ8pQ+kWRf
DTSbNkp3iOgKogf3pKaPkC6AeDqeRe233aYmMwFaMecSMRSfjVZ6XOxZIONLStgv
t7FsxJssU9qSYkKxmFUZpSZLtsK/KqwfX3bs/Q0ueb9MTadkcAFWsZ9ll7CttgkQ
dMESpQoP1EeOuYTZqhSO+WvEokvjDNtz6vkZMnr3o7MR3RuHJ3DlelOHQHm0Q+kU
JFSVProzAgMBAAECggEAJm66kUigUI0eHD9Zm2iesj7jl5yd8FtFO6S6/U1SZ1ha
x3o7+St0AfdnvxoZ1F7iwA8zN9T+r1eKYAtKBWlVez98vudJKfBOXqrMTnuuJ6DK
cg+a4fFM6TPSUWgb1YhoobvZ8lkHOmZ3eFc+/wCwICr/XiqC0I3iFTIm/0J+XkJR
MVDqfFsdNWD7ASj6K4Sl/UauN/+VC8Dkfy2j4GlZ4XcIOPYNUfU+55mqhNc+2aNK
xZUASqsggeLtgopXQoBeXFnRqzMbky63PMu0E88mdbozV62fgWTfLwpcpV0ICcbB
r4S73D1Tk9nlD067Oxwf5c/kw+M9dcbGPvTvqSigeQKBgQDwieRX0e3VriaK2hf9
2lyWR0SmW3NxDBZEilRWVHFNMBK1RyNACvpirdjjLktLOu2zWh0efnhltZm0y1FK
vGEbVM+4zsNPKEUpoI2IXuZqKu/obENyxln08Kcnm50h8Jtxx3pj5maygB/2umO/
iOybSMB1GOQZT6KLRFCpqN7OHwKBgQDfoUvlStFdH4PH+xq0eitFzqmSfyUKo7XS
HLmigowxKHbXr0YjLTGAceTG356D8tGlkYRs0BZCQ3mFgdJmlzHMQefAgpwciJ3n
lZlAcY5abJO07ReV7g8qiJGdwaloazfeXAYR67e7tpdto4dmgPzmhyOdIK/v3ZEo
Cr7U+OIpbQKBgQCdPecxqaBKYW/8sRmh7w87hhdQkNPkgLxFzg/X55exl82DB5/5
i7OmsnLoAClkYn9lEXGbr51RD+gNEiRwMLliPo0UCMYQhwi/b59WGIOTv8HZNHzb
GONRhcgf0lBkEX/sXzSY/dMAuVUr2yi92uqhkGuPoNfr2+0sEVqRrj0IjQKBgQCe
SrjDG5tolCcehdJYT2WT6v3CsXbRuiG4ERchPdDn9uFwOCOoQJQafAPY9W5prZXR
4VfgAcbPrp008Diy33CjST821/ySge1rgEvS/s4pmVNQLMaWHMTlcXfJN8ynGcWZ
AoBVbQlHuK4lMCxmVQUuVBfUX8/JNIdL1ZdQfr7vXQKBgDBrw5daXitkNJ51leIz
pfZaP8aHHKs9Kl/DIL7prTMx8oV3Mz5fa1oLvzJkGhl0k3SIt7LKxOSxJjYOPZM2
K8fi8fGAIgBOPwpyqvDHhaQ2Jfj0uwD9+8Xy9LDzpgxN17Ru3zhyojYNXU/dCpl+
KRG7MjWhT0CuKEk1WE+LEiVC
-----END PRIVATE KEY-----
测试公钥 (PEM)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0h+r8PKkxNoQTYMwjzip
NFX6SYNwgVxQtEpgck51FyUvLEA6gQYGvHrLJ1BiYMKU1QIkPhvx/1JHyqT1cYng
BTtOiTzCtjO7YY9YFIB+gD6TdpfzOBt7jtOMBJRTaSShPpA8GfKUPpFkXw00mzZK
d4joCqIH96Smj5AugHg6nkXtt92mJjMBWjHnEjEUn41WelzsWSDjS0rYL7exbMSb
LFPakmJCsZhVGaUmS7bCvyqsH1927P0NLnm/TE2nZHABVrGfZZewrbYJEHTBEqUK
D9RHjrmE2aoUjvlrxKJL4wzbc+r5GTJ696OzEd0bhydw5XpTh0B5tEPpFCRUlT66
MwIDAQAB
-----END PUBLIC KEY-----

向量 1:带 JSON 正文的 POST

请求正文(原始字节,共 128 字节,末尾无换行符):

{"merchant_transaction_id":"ORDER-001","amount":"50000","currency":"IDR","customer_name":"John Doe","product_name":"Test Order"}
字段 值
METHOD POST
URL /api/create/qris
TIMESTAMP 2026-01-15T14:30:00+07:00
SHA-256 (正文) fbe2d3b8024638a94ab7104abc45489faa5af28eb3d94d59457b6eab37346113

stringToSign:

POST:/api/create/qris:fbe2d3b8024638a94ab7104abc45489faa5af28eb3d94d59457b6eab37346113:2026-01-15T14:30:00+07:00

X-SIGNATURE:

D+2aHQbhBxIDXlSMPL62QdIonsjQvRT5QjMD86ctgmWzLSNsFfYJNC+9Q7tXBjOeKTaaOQWGta3bnPDN0E1TxE/wJdzn5nmjc0SKdLZ9O2yvR77xfYVGPaDcD308tjfcfM6utNZtvXJWUnQaZzWy8cUiMr7pAeI0cqNUaCJa6EpBVzIIpSshwhEMfaO5BWNoz15A7DqA4z0raCaf7S4acmoS/lsDw+6Dli7mZml5JCL6QVu02XXd8+fnNlCep0tL4XYakmgOJnBqV7Vm0XSVQwTb3RK6/AWSt6ESVJjr9dfTtsTT3B9GEGgPwP99spQky/kOaV0H90TkO7tR7s/EjQ==

向量 2:无正文的 GET

字段 值
METHOD GET
URL /api/balance
TIMESTAMP 2026-01-15T14:30:00+07:00
SHA-256 (空正文) e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

stringToSign:

GET:/api/balance:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855:2026-01-15T14:30:00+07:00

X-SIGNATURE:

zAa91hW77Y5LHv3lGHVRtv3Z7Jy7wmhbZ/PNzB3Di5ysr4n6H2JMTefpqVy8poj7oZOyCkN3Oy7o1Tzu16L+J+jG8UTOffPKW8L95bBozyh7AggKpQ+bcQuCleNk8FwiOyFNy4z5t6rP3KIj8Ry+O9C7S39bCY/6aCSSEIGrsc5hbTh+yihsZMEgzySFu5M2x654khwxugEz57A2KqDnK1iNLIHCrWFLHJtTzH1WYwBIO6Aw0HbBuxMPahStPr4UHkQJlznyCkzM6LQURgIbcYit5mhTg/gQdOaz4/1fULmLqBtbvmBwuTPQcxr5rMGmnSlxWEVXQcT+wCm3HEpj0Q==

使用 OpenSSL 复现

将测试私钥保存为 test-private.pem,测试公钥保存为 test-public.pem,然后运行:

# 向量 1
printf '%s' '{"merchant_transaction_id":"ORDER-001","amount":"50000","currency":"IDR","customer_name":"John Doe","product_name":"Test Order"}' > body.json
HASH=$(openssl dgst -sha256 -hex body.json | awk '{print $NF}')
printf '%s' "POST:/api/create/qris:${HASH}:2026-01-15T14:30:00+07:00" > string-to-sign.txt

# 签名(RSA-SHA256,PKCS#1 v1.5)并进行 Base64 编码
openssl dgst -sha256 -sign test-private.pem string-to-sign.txt | base64 | tr -d '\n'; echo

# 验证
openssl dgst -sha256 -sign test-private.pem -out sig.bin string-to-sign.txt
openssl dgst -sha256 -verify test-public.pem -signature sig.bin string-to-sign.txt   # Verified OK

# 向量 2:空字符串的哈希值
printf '' | openssl dgst -sha256 -hex

使用 printf '%s',不要使用 echo:echo 会追加换行符,从而改变哈希值。

© 2026 PGSwift. 保留所有权利。