iDEAL payment 出现 SE2700 Invalid electronic signature
结论
SE2700 Invalid electronic signature 表示 ING 校验商户请求的数字签名失败。这个错误通常与 expose_php 无关。该设置只决定 HTTP 响应头是否暴露 PHP 版本,不会影响 XML、证书或签名结果。
证书仍在有效期内,也不代表当前请求使用的私钥一定与 ING 后台保存的 merchant certificate 匹配。建议先检查证书与私钥是否配对、测试和生产环境是否混用、ING 后台是否保存了正确证书,以及 XML 在签名后是否又被修改。
为什么会出现这个错误
返回信息包含:
SE2700 Invalid electronic signature
System generating error: Acquirer
这说明错误由 acquirer 在接收请求时产生。问题出在 ING 验证商户发出的请求,而不是 PHP 解析响应的过程。
堆栈中的:
XmlSerializer->checkForErrorMessage()
只是 connector 收到 ING 返回的错误 XML 后抛出异常,并非签名失败的原因。
排查时需要区分两类证书:
- merchant certificate:由商户上传至 ING,供 ING 验证商户请求。
- acquirer certificate:由 ING 提供,供商户验证 ING 返回的响应。
因此,下载并安装 acquirer certificate 无法解决商户请求签名无效的问题。当前应重点检查 merchant private key、merchant certificate 和实际发出的 XML。
建议的排查顺序
1. 确认证书与私钥确实配对
“证书有效”通常只能说明证书尚未过期且格式可以解析,不能说明 PHP 实际加载的私钥就是该证书对应的私钥。
可以使用 OpenSSL 比较证书中的公钥和私钥导出的公钥:
openssl x509 -in merchant.crt -pubkey -noout > certificate-public.pem
openssl pkey -in merchant-private.pem -pubout > private-public.pem
diff certificate-public.pem private-public.pem
如果私钥有密码:
openssl pkey \
-in merchant-private.pem \
-passin pass:'PRIVATE_KEY_PASSWORD' \
-pubout > private-public.pem
只有 diff 没有输出,才能说明两份公钥内容一致。
也可以比较公钥摘要:
openssl x509 -in merchant.crt -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256
openssl pkey -in merchant-private.pem -pubout -outform DER \
| openssl dgst -sha256
两个 SHA-256 值必须相同。
2. 核对 ING 保存的是否正是当前证书
计算本地 merchant certificate 的指纹:
openssl x509 -in merchant.crt -noout -fingerprint -sha256
将结果与 ING 商户后台显示或确认的证书指纹进行比较。如果后台只显示 SHA-1 指纹,可以使用:
openssl x509 -in merchant.crt -noout -fingerprint -sha1
常见问题包括:
- 重新生成了私钥,但 ING 仍保存着旧证书。
- 上传了文件名相同、内容却不同的证书。
- 上传的是 acquirer certificate,而不是 merchant certificate。
- 测试环境和生产环境保存了不同证书。
- ING 后台更新的证书尚未应用到当前 Merchant ID。
- 程序仍从旧路径读取证书或私钥。
不要通过文件名判断证书是否正确,应直接比较证书指纹和公钥。
3. 检查程序实际加载的文件
配置文件看起来正确,不代表运行中的 PHP 进程读取了预期文件。可以记录以下诊断信息,但不要记录私钥内容或私钥密码:
<?php
$certificatePath = '/absolute/path/merchant.crt';
$privateKeyPath = '/absolute/path/merchant-private.pem';
foreach ([$certificatePath, $privateKeyPath] as $path) {
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException('File is missing or unreadable: ' . $path);
}
}
$certificatePem = file_get_contents($certificatePath);
$certificate = openssl_x509_read($certificatePem);
if ($certificate === false) {
throw new RuntimeException('Unable to parse merchant certificate');
}
$info = openssl_x509_parse($certificate);
error_log('Merchant certificate path: ' . realpath($certificatePath));
error_log('Merchant certificate SHA-256: ' . openssl_x509_fingerprint(
$certificate,
'sha256'
));
error_log('Certificate validFrom: ' . ($info['validFrom_time_t'] ?? 'unknown'));
error_log('Certificate validTo: ' . ($info['validTo_time_t'] ?? 'unknown'));
加载私钥时,还要检查密码和 OpenSSL 错误队列:
<?php
$privateKeyPem = file_get_contents($privateKeyPath);
$privateKey = openssl_pkey_get_private(
$privateKeyPem,
$privateKeyPassword
);
if ($privateKey === false) {
$errors = [];
while (($error = openssl_error_string()) !== false) {
$errors[] = $error;
}
throw new RuntimeException(
'Unable to load private key: ' . implode('; ', $errors)
);
}
如果项目使用相对路径,CLI、PHP-FPM 和 Web Server 的工作目录可能并不相同。证书配置最好使用绝对路径,并通过 realpath() 确认最终加载的位置。
4. 在本地验证私钥和证书能否完成签名校验
下面的代码不是用来生成 iDEAL 协议签名的,只用于确认 PHP 当前加载的私钥与证书公钥匹配:
<?php
$data = 'iDEAL local signature diagnostic';
$certificatePem = file_get_contents('/absolute/path/merchant.crt');
$privateKeyPem = file_get_contents('/absolute/path/merchant-private.pem');
$privateKey = openssl_pkey_get_private(
$privateKeyPem,
$privateKeyPassword
);
$publicKey = openssl_pkey_get_public($certificatePem);
if ($privateKey === false || $publicKey === false) {
throw new RuntimeException('Unable to load certificate or private key');
}
if (!openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
throw new RuntimeException('Local signing failed');
}
$result = openssl_verify(
$data,
$signature,
$publicKey,
OPENSSL_ALGO_SHA256
);
if ($result !== 1) {
throw new RuntimeException(
'The private key does not match the merchant certificate'
);
}
这里使用 SHA-256 只是为了进行本地配对诊断。实际 iDEAL 请求必须使用对应协议版本和 ING 接口文档规定的签名算法,不能直接拿这段代码替换 connector 的签名实现。
5. 检查测试环境与生产环境是否混用
以下配置必须属于同一套环境:
- Merchant ID
- Sub ID
- merchant certificate
- merchant private key
- acquirer certificate
- ING endpoint
- iDEAL 协议版本
例如,将生产 Merchant ID 与测试 endpoint 配合使用,或者用生产证书签名后向测试环境发送请求,都可能遭到拒绝。
还要检查 Merchant ID 是否包含前导零。它通常应以字符串形式保存和传递,不能先转换成整数:
$merchantId = '001234567';
不要写成:
$merchantId = 1234567;
不过,Merchant ID 有误时通常也可能返回其他错误码。它是否会直接导致 SE2700 取决于具体实现,不能只根据这个错误码下结论。
6. 保存并检查实际发出的 XML
在 connector 完成签名后、执行 HTTPS 请求前,保存一份脱敏后的原始 XML。主要检查:
- XML 使用的命名空间是否正确。
Signature、SignedInfo、Reference和DigestValue是否完整。- 被引用的节点 ID 是否存在且唯一。
- canonicalization 和 signature method 是否符合当前协议要求。
- XML 编码是否与声明一致。
- 完成签名后,是否又格式化、缩进或重新构建了 DOM。
- 签名后是否修改了
merchantReturnURL、时间戳或 Merchant ID。 - 字段是否被转义了两次。
- 是否存在 BOM、不可见字符或错误的换行处理。
一种典型的错误流程是:
$xml = buildRequest();
signXml($xml);
$xml->formatOutput = true; // 签名后再次序列化或修改
send($xml->saveXML());
更安全的流程是:
$xml = buildRequest();
signXml($xml);
$payload = $xml->saveXML();
// 从这里开始不要再修改或重新构建 XML。
send($payload);
XML Digital Signature 对节点内容、命名空间和规范化方式非常敏感。不要通过字符串拼接重写已经签名的 XML。
7. 确认 connector 与协议版本匹配
题目中的调用栈来自较早期的 PHP connector。如果将它迁移到不同的 PHP 或 OpenSSL 环境,可能出现以下兼容性问题:
- connector 使用的签名算法已经不符合当前接口要求。
- DOM 或 XML Security 依赖版本不兼容。
- 接口不接受所用的私钥类型或长度。
- 新版 OpenSSL 拒绝旧算法或旧密钥格式。
- connector 生成的 canonicalization 结果发生了变化。
允许使用哪些签名算法、证书需要满足什么条件,以及应连接哪个 endpoint,都必须以当前 ING iDEAL 接入文档和商户环境配置为准,不能根据旧示例推测。排查时应先使用 ING 当前提供或认可的 connector/SDK,避免自行实现 XML Digital Signature。
merchantReturnURL 是否相关
http://www.04dertien.com 本身没有明显的 XML 特殊字符。即使 URL 带有查询参数,正确的 XML 序列化器也应该把 & 编码成 &。
仍需确认签名前后的逻辑值相同,例如:
<merchantReturnURL>https://example.com/return?a=1&b=2</merchantReturnURL>
不能先手工对 URL 进行 HTML 转义,再交给 DOM 二次转义,否则可能生成:
&amp;
当前示例中的 URL 很简单,因此不应作为首要排查对象。
expose_php 是否会影响签名
不会。expose_php 主要控制是否发送类似下面的 HTTP 响应头:
X-Powered-By: PHP/...
它不参与:
- merchant certificate 的选择;
- private key 的加载;
- XML canonicalization;
- XML 摘要计算;
- RSA 签名;
- ING 对请求签名的验证。
关闭 expose_php 可以减少 PHP 版本信息暴露,但无法修复 SE2700。
最有效的解决办法
建议按以下顺序处理:
- 计算程序实际使用的 merchant certificate 的 SHA-256 指纹。
- 确认 ING 当前环境保存的是同一张证书。
- 验证该证书的公钥与 PHP 加载的 private key 一致。
- 确认 Merchant ID、证书、endpoint 和运行环境属于同一套配置。
- 捕获签名后实际发出的 XML,确认发送前没有再次修改。
- 使用符合当前协议要求的官方或受支持 connector 生成同一个
DirectoryRequest。 - 如果问题仍未解决,将 Merchant ID、环境、证书指纹、请求时间和 ING 返回的错误时间戳提供给 ING 技术支持,请其核对服务端验签所用的证书。不要发送私钥或私钥密码。
根据现有信息,最可能的原因不是 expose_php 或 merchantReturnURL,而是 ING 保存的 merchant certificate 与应用实际用于签名的 private key 不匹配,或者请求 XML 在签名后又发生了变化。
备注:内容仅供参考。
