# Flash Pay 印尼商户 API 与 RSA 签名 接口域名:`https://inpay.happypay99.com`。代收 `/api/payment/create`,代付 `/api/payout/create`。 商户请求沿用 RSA-SHA256 / PKCS#1 v1.5 / Base64,至少2048位 RSA 密钥。 这是 Flash Pay 印尼商户 API,币种固定为 IDR,金额符号为 Rp。代收成功响应的 order_data 为收银台完整地址,客户端原样打开;pageUrl 回跳不代表支付成功。商户号由本站分配,商户登记自己的 RSA 公钥,平台回调公钥通过本站后台获取。 字段定义、查单、余额和通知见[商户文档](index.html)。当前代收为整数10,000–50,000,000 IDR,代付为整数10,000–50,000 IDR;业务开关、商户限额和银行/钱包支持范围须先开通。 所有字段使用 JSON string,金额 `order_amount` 如 `"10000.00"`,手机号和账号保留前导零。 签名时排除 `sign`、未传字段和空串,将其余字段按 ASCII 升序拼接为 `key=value&key=value`。 原值保留,不 URL 编码、不 trim、不格式化金额、不追加 key、不签整段 JSON; 将 UTF-8 字节直接交给 RSA-SHA256 签名 API,最后标准 Base64 编码。 服务端拒绝未知字段、重复字段、null 和非字符串参数。 ## 生成并登记密钥 ### 1. 准备 OpenSSL(官方页面) - [OpenSSL 官方网站](https://www.openssl.org/)。 - [官方下载页面](https://openssl-library.org/source/):提供源码发行包,选用仍受支持的正式版本。 - [官方安装/编译说明 INSTALL](https://github.com/openssl/openssl/blob/openssl-3.5/INSTALL.md); Windows 另见 [官方 Windows 安装说明](https://github.com/openssl/openssl/blob/openssl-3.5/NOTES-WINDOWS.md)。 - 命令官方手册:[genpkey(生成密钥)](https://docs.openssl.org/3.5/man1/openssl-genpkey/)、 [pkey(导出与检查公钥)](https://docs.openssl.org/3.5/man1/openssl-pkey/)。 在终端执行以下命令确认 OpenSSL 已安装且可用: ```bash openssl version ``` Windows 可在已安装 OpenSSL 的 PowerShell 或 Git Bash 中执行。若提示找不到 `openssl`,将安装目录下的 bin 目录加入 PATH,或使用 `openssl.exe` 的完整路径。 官方下载页提供的是源码;第三方 Windows 安装包不属于 OpenSSL 官方发行包。 ### 2. 生成 RSA 私钥,再导出公钥 在商户自己的机器、专门保存密钥的目录中执行。第一条生成2048位 RSA 私钥, 第二条从该私钥导出匹配的 PEM 公钥,第三条检查公钥是否有效: ```bash openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out merchant-private.pem openssl pkey -in merchant-private.pem -pubout -out merchant-public.pem openssl pkey -pubin -in merchant-public.pem -pubcheck -noout ``` 检查成功显示 `Key is valid`。 | 文件 | 格式与用途 | | --- | --- | | `merchant-private.pem` | 未加密 PKCS#8 RSA 私钥,保留在商户服务器生成 `sign`,不要上传到平台或提交到 Git | | `merchant-public.pem` | PEM / SPKI RSA 公钥,完整粘贴到后台登记,供平台验签 | 已有有效密钥时继续使用原密钥。生成命令会覆盖同名文件;已有私钥但丢失公钥时, 只执行导出和检查两条命令,无需重新生成私钥。 `ssh-ed25519 AAAA...` 是 SSH 的 Ed25519 公钥,算法与编码均不符合本接口的 RSA 要求,不能转换成 RSA 密钥;请另行生成上面的 RSA 密钥对。 ### 3. 读取并登记完整公钥 Windows PowerShell: ```powershell Get-Content -Raw ./merchant-public.pem ``` Linux、macOS 或 Git Bash: ```bash cat merchant-public.pem ``` 复制输出的完整内容,包括开头和结尾两行,在商户后台「API 对接 → 机器 RSA 公钥」 中粘贴并保存。格式应为: ```text -----BEGIN PUBLIC KEY----- 这里是 merchant-public.pem 中实际生成的 Base64 内容,请完整复制 -----END PUBLIC KEY----- ``` 上面只是格式示意,不能把示意文字当成真实公钥。也可使用登录后的接口登记: ```http PUT /backend/merchant/updateApiPublicKey Content-Type: application/json {"apiPublicKey":"-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"} ``` 公钥保存后仅显示「已配置,不可查看」,读取接口只返回 `apiPublicKeyConfigured`, 不再返回商户公钥内容。修改时填写新公钥,替换后旧公钥立即失效。 平台回调公钥是另外一把公钥,仍可在「商户信息」和「API 对接」中查看、复制。 平台接受至少2048位的 RSA 公钥。浏览器工具支持 `PUBLIC KEY`(SPKI)与 `RSA PUBLIC KEY`(PKCS#1),私钥支持未加密的 `PRIVATE KEY`(PKCS#8)与 `RSA PRIVATE KEY`(PKCS#1)。若手头是证书,可先在本机提取公钥: ```bash openssl x509 -in merchant-cert.pem -pubkey -noout > merchant-public.pem ``` ## 印尼代收签名示例 示例仅生成签名和请求体;替换真实资料并登记商户公钥后再下单。 ```python import base64 import json from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding body = {'mer_no': 'YOUR_MERCHANT_ID', 'mer_order_no': 'IDR_IN_20261102_001', 'pname': 'Budi Santoso', 'pemail': 'buyer@example.com', 'phone': '081234567890', 'order_amount': '10000.00', 'ccy_no': 'IDR', 'busi_code': '101202', 'notifyUrl': 'https://merchant.example/notify', 'pageUrl': 'https://merchant.example/paid'} params = {key: value for key, value in body.items() if key != "sign" and value != ""} canonical = "&".join(f"{key}={params[key]}" for key in sorted(params)) with open("merchant-private.pem", "rb") as key_file: private_key = serialization.load_pem_private_key(key_file.read(), password=None) signature = private_key.sign(canonical.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()) body["sign"] = base64.b64encode(signature).decode("ascii") print(json.dumps(body, ensure_ascii=False, indent=2)) # import requests # response = requests.post("https://inpay.happypay99.com/api/payment/create", json=body, timeout=15) # print(response.json()) ``` ## 印尼银行 / 银行 代付签名示例 示例仅生成签名和请求体;替换真实资料并登记商户公钥后再下单。 ```python import base64 import json from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding body = {'mer_no': 'YOUR_MERCHANT_ID', 'mer_order_no': 'IDR_OUT_20261102_001', 'acc_no': '081234567890', 'acc_name': 'Budi Santoso', 'ccy_no': 'IDR', 'order_amount': '10000.00', 'bank_code': '014', 'mobile_no': '081234567890', 'email': 'payee@example.com', 'notifyUrl': 'https://merchant.example/notify', 'summary': 'Payout IDR_OUT_20261102_001'} params = {key: value for key, value in body.items() if key != "sign" and value != ""} canonical = "&".join(f"{key}={params[key]}" for key in sorted(params)) with open("merchant-private.pem", "rb") as key_file: private_key = serialization.load_pem_private_key(key_file.read(), password=None) signature = private_key.sign(canonical.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()) body["sign"] = base64.b64encode(signature).decode("ascii") print(json.dumps(body, ensure_ascii=False, indent=2)) # import requests # response = requests.post("https://inpay.happypay99.com/api/payout/create", json=body, timeout=15) # print(response.json()) ``` ## 使用 OpenSSL 独立核对 将待签名串保存为 UTF-8 的 `signing.txt`,不要 BOM 或末尾换行: ```bash openssl dgst -sha256 -sign merchant-private.pem -out signature.bin signing.txt openssl base64 -A -in signature.bin openssl dgst -sha256 -verify merchant-public.pem -signature signature.bin signing.txt ``` Java 使用 `Signature.getInstance("SHA256withRSA")`;Python 使用 `padding.PKCS1v15()` 和 `hashes.SHA256()`;浏览器使用 `RSASSA-PKCS1-v1_5` 和 `SHA-256`。不要二次摘要或使用 RSA-PSS。 本地浏览器工具支持代收、代付、查单与余额,不发送订单或上传、保存密钥。 ## 平台回调验签 平台使用系统运行配置中的平台私钥签发通知。商户在后台复制「平台回调 RSA 公钥」 保存为 `platform-public.pem`,核对公钥标识。所有后续通知和重试(含历史订单)使用 RSA-SHA256 / PKCS#1 v1.5 / 标准 Base64,历史订单仅保留原业务字段名。 通知中 `sign_type` 固定 `RSA-SHA256`;`sign_key_id` 是平台公钥 SPKI DER 的 SHA-256 前16个小写十六进制字符。这两个字段都参与签名。密钥轮换时应先向运营 核对新公钥,按标识选择本地可信公钥,不能从通知传入的 URL 下载验签密钥。 ```python import base64 import hashlib from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa def verify_platform_callback(body): if not isinstance(body, dict) or any(not isinstance(v, str) for v in body.values()): raise ValueError("回调所有字段必须为字符串") if body.get("sign_type") != "RSA-SHA256": raise ValueError("不支持的签名算法") with open("platform-public.pem", "rb") as key_file: public_key = serialization.load_pem_public_key(key_file.read()) if not isinstance(public_key, rsa.RSAPublicKey): raise ValueError("平台公钥必须是 RSA") der = public_key.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) if body.get("sign_key_id") != hashlib.sha256(der).hexdigest()[:16]: raise ValueError("平台公钥标识不匹配") canonical = "&".join(f"{key}={body[key]}" for key in sorted(body) if key != "sign" and body[key] != "") public_key.verify(base64.b64decode(body["sign"], validate=True), canonical.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()) return True ``` 验签成功后再按平台订单号和商户订单号幂等处理,核对金额和状态,成功后返回 HTTP 200 + `success`。不得追加共享密钥或改为 MD5,不得用商户公钥验平台通知。 平台签名配置为空或无效时暂停发送;有效配置保存后,等待中的回调自动继续。