Signature
Algorithm
This platform signs requests and responses using HMAC-SHA256 over a JCS (RFC 8785) canonicalized JSON payload, which eliminates interop bugs caused by different JSON serializers producing different bytes for semantically identical objects (key order, whitespace, Unicode escapes, numeric precision, etc.).
Reference specs:
Full signing steps
- Drop the top-level
signfield from the payload (if present). - Apply JCS (RFC 8785) canonical serialization to the remaining object, producing
signingBytes(UTF-8 bytes).- Object keys are sorted in UTF-16 code-unit lexicographic order (per RFC 8785).
- No whitespace, newlines, or indentation anywhere.
- Numbers use the shortest IEEE 754 round-trip decimal representation.
- Only JCS-mandated control characters are
\uXXXXescaped; everything else (CJK, Emoji, etc.) stays verbatim. null,true,false, arrays and all native JSON types participate exactly as they are.
- Compute HMAC-SHA256 with
UTF8(secretKey)as the key andsigningBytesas the message. - Base64-standard-encode the 32-byte HMAC digest to obtain the final
signstring (standard alphabet+/=, no URL-safe variant, no line breaks). - Attach
signback to the original payload and send/return it.
As a formula:
signingBytes = UTF8( JCS( payload_without_sign ) )
signature = Base64( HMAC-SHA256( UTF8(secretKey), signingBytes ) )Important rules
- Only the top-level
signfield is removed before JCS; every other field (including those with valuesnull,false,0,"") is signed as-is. secretKeymust only be used as the HMAC key parameter — never put it inside the JSON before JCS, and never swap key/message order in HMAC.- The final
signis standard Base64 (not URL-safe, uses+/=). - On verify:
Trim()the receivedsign, Base64-decode both signatures, and compare with a constant-time equality helper (to prevent timing attacks). - Verifiers must tolerate new extension fields in responses — simply re-run JCS + HMAC on the whole payload, no whitelist filtering needed.
Example
Full 5-step walkthrough using the secret mvpay_demo_secret_0123.
Step 1 — Original payload before signing (no sign field yet):
Note rtn_url: "" (empty string) and extra: null are preserved as-is and participate in signing.
{
"mcode": "12345",
"amt": "50.00",
"tno": "T202608020001",
"rtn_url": "",
"extra": null,
"ip": "127.0.0.1",
"acct": { "type": "bankcard", "name": "A", "number": "001" }
}Step 2 — Drop the sign field: In this example the payload is not signed yet, so the data stays the same. When verifying a received payload, you must strip its sign field before re-running JCS + HMAC.
Step 3 — JCS canonical (UTF-8, no whitespace, keys sorted in UTF-16 code-unit order at every level; extra: null and rtn_url: "" are both present):
{"acct":{"name":"A","number":"001","type":"bankcard"},"amt":"50.00","extra":null,"ip":"127.0.0.1","mcode":"12345","rtn_url":"","tno":"T202608020001"}Step 4 — Base64( HMAC-SHA256(key = UTF8(secretKey), msg = signingBytes) ) → sign:
mac := hmac.New(sha256.New, []byte("mvpay_demo_secret_0123"))
mac.Write([]byte(`{"acct":{"name":"A","number":"001","type":"bankcard"},"amt":"50.00","extra":null,"ip":"127.0.0.1","mcode":"12345","rtn_url":"","tno":"T202608020001"}`))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// sign = "TKwGqnHzFy0BaME1rZryqJql8d1UOIAI2180dOM2VIc="Step 5 — Attach sign back; this is the final JSON you submit to the gateway:
{
"mcode": "12345",
"amt": "50.00",
"tno": "T202608020001",
"rtn_url": "",
"extra": null,
"ip": "127.0.0.1",
"acct": { "type": "bankcard", "name": "A", "number": "001" },
"sign": "TKwGqnHzFy0BaME1rZryqJql8d1UOIAI2180dOM2VIc="
}On verify: follow exactly the same 5 steps — re-run JCS + HMAC-SHA256 on the received JSON minus
sign, then compare both Base64 signatures with a constant-time equality helper.
Sample Code
Implementations by language (public JCS libraries, no hand-written canonicalizer)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/gowebpki/jcs"
)
func Sign(payload map[string]any, secretKey string) (string, error) {
// 1. 移除 sign
data := map[string]any{}
for k, v := range payload {
if k == "sign" {
continue
}
data[k] = v
}
// 2. JSON 字节 → JCS Transform
canonical, err := jcs.Transform(json.RawMessage(mustMarshal(data)))
if err != nil {
return "", err
}
// 3. HMAC-SHA256 + Base64
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(canonical)
return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
}
func mustMarshal(v any) []byte { b, _ := json.Marshal(v); return b }
func main() {
sig, _ := Sign(map[string]any{
"mcode": "12345",
"amt": "50.00",
"tno": "T202608020001",
"acct": map[string]any{"type": "bankcard", "name": "A", "number": "001"},
}, "your-secret-key")
fmt.Println(sig)
}import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.webpki.json.JsonCanonicalizer;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
public class JcsHmac {
static final ObjectMapper M = new ObjectMapper();
public static String sign(Map<String, Object> payload, String secretKey) throws Exception {
// 1. 移除顶层 sign
JsonNode root = M.valueToTree(payload);
if (root.isObject()) ((ObjectNode) root).remove("sign");
byte[] canonical = JsonCanonicalizer.canonicalize(M.writeValueAsBytes(root));
// 2. HMAC-SHA256 + Base64
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return Base64.getEncoder().encodeToString(mac.doFinal(canonical));
}
public static void main(String[] args) throws Exception {
System.out.println(sign(java.util.Map.of(
"mcode", "12345",
"amt", "50.00",
"tno", "T202608020001"
), "your-secret-key"));
}
}<?php
require 'vendor/autoload.php';
use Emnabs\JsonCanonicalization\JsonCanonicalizer;
function signJcsHmac(array $payload, string $secretKey): string
{
// 1. 移除 sign 字段
if (array_key_exists('sign', $payload)) {
unset($payload['sign']);
}
// 2. JCS 规范化 → HMAC-SHA256(raw binary) → Base64
$canonical = JsonCanonicalizer::canonicalize($payload);
return base64_encode(hash_hmac('sha256', $canonical, $secretKey, true));
}
echo signJcsHmac([
'mcode' => '12345',
'amt' => '50.00',
'tno' => 'T202608020001',
], 'your-secret-key');import base64
import hmac
import hashlib
import json_canonicalization as jcs_lib
def sign_jcs_hmac(payload: dict, secret_key: str) -> str:
# 1. 移除 sign
cleaned = {k: v for k, v in payload.items() if k != "sign"}
# 2. JCS -> bytes, HMAC-SHA256 -> bytes, Base64
signing_bytes = jcs_lib.canonicalize(cleaned)
mac = hmac.new(secret_key.encode("utf-8"), signing_bytes, hashlib.sha256)
return base64.b64encode(mac.digest()).decode("ascii")
if __name__ == "__main__":
print(sign_jcs_hmac({
"mcode": "12345",
"amt": "50.00",
"tno": "T202608020001",
"acct": {"type": "bankcard", "name": "A", "number": "001"},
}, "your-secret-key"))using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Webpki.JsonCanonicalizer;
public static class JcsHmacSigner
{
public static string Sign(object payload, string secretKey)
{
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(payload));
var root = doc.RootElement.Clone();
// 1. 移除顶层 sign 字段
var node = root.ValueKind == JsonValueKind.Object
? RemoveSign(JsonObject.Create(root))
: JsonNode.Parse(root.GetRawText());
byte[] canonical = JsonCanonicalizer.Serialize(node);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
return Convert.ToBase64String(hmac.ComputeHash(canonical));
}
static JsonNode RemoveSign(JsonObject obj)
{
obj.Remove("sign");
return obj;
}
}const { createHmac } = require('node:crypto')
const { canonicalize } = require('json-canonicalization') // RFC 8785 (JCS)
function signJcsHmac(payload, secretKey) {
// 1. 移除 sign 字段
const { sign, ...rest } = payload
// 2. JCS 规范化 → UTF-8 Buffer → HMAC-SHA256 → Base64
const signingBytes = Buffer.from(canonicalize(rest), 'utf-8')
return createHmac('sha256', secretKey)
.update(signingBytes)
.digest('base64')
}
console.log(signJcsHmac({
mcode: '12345',
amt: '50.00',
tno: 'T202608020001',
rtn_url: '',
extra: null,
ip: '127.0.0.1',
acct: { type: 'bankcard', name: 'A', number: '001' }
}, 'mvpay_demo_secret_0123'))Verification Tool
Live in-browser signer. Drop in your 64 or 32-char key + JSON payload → get the JCS canonical string and the final payload with sign attached. Runs locally, nothing is sent anywhere.