PAYATHON 2026

payment server 的 cUrl HTTP POST 为何需要 private key?

支付老李

明确结论

CURLOPT_SSLCERT 用来配置客户端证书。如果支付服务器采用双向 TLS(mTLS)认证,客户端必须持有与该证书匹配的 private key。

这里需要的不是 payment server 的 private key,而是你方客户端证书对应的 private key。证书本身可以公开,但仅发送证书不能证明你是合法持有者。在 TLS 握手过程中,客户端需要使用自己的 private key 完成签名,服务器再用证书中的 public key 验证签名。

如果对方提供的只是服务器证书或 CA certificate,不应将其配置为 CURLOPT_SSLCERT,而应通过 CURLOPT_CAINFO 验证服务器身份。

先确认对方提供的文件是什么

常见情况有以下三种:

  1. 客户端证书和 private key

    用于 mTLS。通常包括:

    • client-cert.pem:客户端证书
    • client-key.pem:客户端 private key
  2. PKCS#12 文件

    扩展名通常是 .p12 或 .pfx,文件中可以同时包含客户端证书和 private key,一般还会设置导出密码。

  3. 服务器证书或 CA certificate

    这类文件只用于验证支付服务器,不能代表客户端身份。此时应配置 CURLOPT_CAINFO,不需要客户端 private key。

可以使用 OpenSSL 检查文件内容:

openssl x509 -in certificate.pem -noout -subject -issuer

检查 PEM 文件中是否包含 private key:

grep "PRIVATE KEY" certificate.pem

如果文件是 .p12 或 .pfx:

openssl pkcs12 -info -in client.p12 -noout

这些命令只能检查文件结构。证书用于服务器验证还是客户端认证,仍要以支付服务商的接入文档为准。

使用独立的客户端证书和 private key

假设支付服务商要求使用 mTLS,并且你已有以下文件:

  • client-cert.pem
  • client-key.pem

PHP cURL 可以这样配置:

<?php

$x = curl_init();

$options = [
    CURLOPT_URL => $serverURL,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $string,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_TIMEOUT => 30,

    // 客户端身份认证
    CURLOPT_SSLCERT => __DIR__ . '/client-cert.pem',
    CURLOPT_SSLCERTTYPE => 'PEM',
    CURLOPT_SSLKEY => __DIR__ . '/client-key.pem',
    CURLOPT_SSLKEYTYPE => 'PEM',

    // 验证支付服务器
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,

    CURLOPT_FAILONERROR => true,
];

curl_setopt_array($x, $options);

$data = curl_exec($x);

if ($data === false) {
    throw new RuntimeException(
        curl_error($x) . ' (' . curl_errno($x) . ')'
    );
}

$statusCode = curl_getinfo($x, CURLINFO_RESPONSE_CODE);
curl_close($x);

echo "HTTP status: {$statusCode}\n";
echo $data;

如果 private key 已加密,还要配置密码:

CURLOPT_KEYPASSWD => $privateKeyPassword,

部分 PHP/cURL 环境也支持名称相近的 CURLOPT_SSLKEYPASSWD。具体使用哪个常量,取决于当前 PHP 构建提供的支持。

使用包含证书和 private key 的 PKCS#12 文件

如果收到的是 client.p12 或 client.pfx,可以直接使用:

<?php

$x = curl_init();

curl_setopt_array($x, [
    CURLOPT_URL => $serverURL,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $string,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,

    CURLOPT_SSLCERT => __DIR__ . '/client.p12',
    CURLOPT_SSLCERTTYPE => 'P12',
    CURLOPT_KEYPASSWD => $certificatePassword,

    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);

$data = curl_exec($x);

if ($data === false) {
    throw new RuntimeException(
        curl_error($x) . ' (' . curl_errno($x) . ')'
    );
}

curl_close($x);

echo $data;

PHP 能否直接读取 PKCS#12 文件,取决于它使用的 libcurl 和 TLS 后端。如果当前环境不支持,可以先将文件转换为 PEM,再通过 CURLOPT_SSLCERT 和 CURLOPT_SSLKEY 分别配置证书与 private key。

如果提供的是 CA certificate

如果支付服务商提供的文件用于验证其服务器证书,应改为:

CURLOPT_CAINFO => __DIR__ . '/payment-ca.pem',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,

完整配置中的关键部分如下:

curl_setopt_array($x, [
    CURLOPT_URL => $serverURL,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $string,
    CURLOPT_RETURNTRANSFER => true,

    CURLOPT_CAINFO => __DIR__ . '/payment-ca.pem',
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);

这种情况下不要设置 CURLOPT_SSLCERT,除非支付服务器明确表示还需要客户端证书认证。

为什么浏览器也需要 private key

访问普通 HTTPS 网站时,通常只有服务器发送证书,浏览器不会发送客户端证书。

只有网站要求使用 mTLS 时,浏览器才会选择并发送客户端证书。对应的 private key 也保存在浏览器可访问的证书存储区、操作系统或硬件安全设备中。这个过程通常自动完成,所以用户看不到显式配置 private key 的步骤。

TLS 不会将 private key 发送给服务器。客户端只用它完成密码学签名,证明自己持有该密钥。服务器保存的是用于验证签名的 public key 或受信任的 CA 信息,不应持有客户端的 private key。

服务器知道客户端 IP,也不能代替这种身份凭证。IP 白名单和客户端证书属于两套不同的安全控制。

当前代码中的主要问题

当前代码将:

CURLOPT_SSLCERT => getcwd() . "/certificate.pem",

配置为客户端证书。如果该 PEM 文件不包含 private key,同时也没有通过 CURLOPT_SSLKEY 指定 private key,cURL 就可能报告无法设置或加载 private key。

代码中还有一项配置:

CURLOPT_SSL_VERIFYPEER => false,

它会关闭服务器证书链验证,连接可能因此遭受中间人攻击。支付接口的生产环境应保持以下配置:

CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,

如果系统不信任支付服务商的证书链,应正确安装 CA,或者通过 CURLOPT_CAINFO 指定 CA certificate,不能直接关闭验证。

还需要向支付服务商确认的内容

继续修改代码前,需要确认:

  • 对方提供的是客户端证书、服务器证书还是 CA certificate;
  • 接口是否要求使用 mTLS;
  • 客户端 private key 由谁生成和保管;
  • 对方是否提供 .p12/.pfx 文件及其密码;
  • 是否需要发送中间证书链;
  • HTTP 请求体的格式和 Content-Type;
  • 是否还要对业务报文单独签名。

mTLS 中的 private key 用于 TLS 身份认证,支付报文签名属于应用层安全机制。两者可能同时使用,不能互相替代。

如果支付服务商只提供了一个不含 private key 的客户端证书,却要求使用该证书进行 mTLS,那么接入材料并不完整。另一种可能是,应先由你方生成 key pair 和 CSR,再交给对方签发客户端证书。无论属于哪种情况,都不应索取或使用 payment server 自己的 private key。

备注:内容仅供参考。