PAYATHON 2026

PHP cURL 使用证书连接 Swish Payment API 时握手失败

支付小周

结论

PHP 请求没有完整复现命令行中能够正常运行的参数。命令行明确指定了:

  • 客户端证书 --cert
  • 客户端私钥 --key
  • Swish 根 CA --cacert
  • TLS 版本 --tlsv1.1
  • 正确的 API 路径 /swish-cpcapi/api/v1/paymentrequests

PHP 代码没有配置 CA 和 TLS 版本,URL 中还缺少 /api。提交数据也被写成了字符串列表,而不是 JSON 对象。这几个问题都需要修正。

SSL handshake failure 出现在 HTTP 请求发出之前,因此应先检查 TLS 版本、客户端证书、私钥、CA 证书,以及 PHP 使用的 libcurl/OpenSSL 环境。URL 和 JSON 错误不会直接引起 TLS 握手失败,但握手成功后可能导致 404 或参数校验失败。

正确的 PHP cURL 配置

<?php

$certificateFile = __DIR__ . '/Swish_Merchant_TestCertificate_1231181189.pem';
$privateKeyFile  = __DIR__ . '/Swish_Merchant_TestCertificate_1231181189.key';
$caFile          = __DIR__ . '/Swish_TLS_RootCA.pem';

$data = array(
    'payeePaymentReference' => '0123456789',
    'callbackUrl'           => 'https://myfakehost.se/swishcallback.cfm',
    'payerAlias'            => '4671234768',
    'payeeAlias'            => '1231181189',
    'amount'                => '100',
    'currency'              => 'SEK',
    'message'               => 'Kingston USB Flash Drive 8 GB'
);

$json = json_encode($data);

if ($json === false) {
    throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg());
}

$curl = curl_init();

curl_setopt_array($curl, array(
    CURLOPT_URL => 'https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $json,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,

    // 客户端证书和私钥
    CURLOPT_SSLCERT => $certificateFile,
    CURLOPT_SSLCERTTYPE => 'PEM',
    CURLOPT_SSLKEY => $privateKeyFile,
    CURLOPT_SSLKEYTYPE => 'PEM',

    // 只有私钥确实经过密码加密时才设置
    CURLOPT_KEYPASSWD => 'swish',

    // 验证 Swish 服务端证书
    CURLOPT_CAINFO => $caFile,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,

    // 与题目中的命令行测试保持一致
    CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_1,

    CURLOPT_HTTPHEADER => array(
        'Content-Type: application/json',
        'Accept: application/json',
        'Content-Length: ' . strlen($json)
    )
));

$response = curl_exec($curl);

if ($response === false) {
    $errorNumber = curl_errno($curl);
    $errorMessage = curl_error($curl);
    curl_close($curl);

    throw new RuntimeException(
        sprintf('cURL error %d: %s', $errorNumber, $errorMessage)
    );
}

$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

echo 'HTTP status: ' . $statusCode . PHP_EOL;
echo $response;

部分 PHP/cURL 版本使用 CURLOPT_SSLKEYPASSWD,有些环境也支持别名 CURLOPT_KEYPASSWD。如果当前环境无法识别后者,可以改为:

CURLOPT_SSLKEYPASSWD => 'swish',

私钥没有设置密码时,不要添加这个选项。

原 PHP 代码中的具体问题

1. 缺少 Swish 根 CA

命令行使用了:

--cacert ./Swish_TLS_RootCA.pem

对应的 PHP 配置是:

CURLOPT_CAINFO => __DIR__ . '/Swish_TLS_RootCA.pem',

不要用下面的配置绕过证书验证:

CURLOPT_SSL_VERIFYPEER => false

关闭验证可能暂时掩盖 CA 配置问题,却会破坏 HTTPS 身份校验,不适合支付接口。

2. 没有复现 TLS 版本设置

命令行明确使用了:

--tlsv1.1

要复现这个历史测试请求,PHP 也需要设置:

CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_1,

不过,Swish API、测试环境或证书版本发生变化时,TLS 要求也可能变化。生产环境应以当前 Swish 接口文档为准。如果接口要求 TLS 1.2,应使用:

CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,

旧示例使用 TLS 1.1,并不意味着新环境也应长期固定为 TLS 1.1。

3. API URL 写错

能够正常工作的命令行地址是:

https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests

原 PHP 地址是:

https://mss.cpc.getswish.net/swish-cpcapi/v1/paymentrequests/

原地址缺少 /api。这不会引起 TLS 握手失败,因为服务器会先完成握手,再处理 URL。但握手成功后,请求仍会进入错误的接口路径。

4. JSON 数据结构错误

原代码创建的是普通索引数组:

$data = array(
    "payeePaymentReference: 0123456789",
    "amount: 100"
);

编码结果类似:

[
  "payeePaymentReference: 0123456789",
  "amount: 100"
]

Swish 接口需要 JSON 对象:

{
  "payeePaymentReference": "0123456789",
  "amount": "100"
}

因此,应使用关联数组:

$data = array(
    'payeePaymentReference' => '0123456789',
    'amount' => '100'
);

content-type 和 accept 是 HTTP 请求头,不应写入 JSON 请求体。

如果仍然出现 handshake failure

同一组文件能在 Git Bash 中正常使用,不代表 PHP 使用的是同一套 TLS 环境。Git Bash 的 curl 和 PHP 扩展可能分别链接到不同版本的 libcurl、OpenSSL、Schannel 或其他 TLS 后端。

可以用下面的代码查看 PHP 实际使用的版本:

<?php
print_r(curl_version());
echo OPENSSL_VERSION_TEXT . PHP_EOL;

检查以下项目:

  • PHP 是否启用了 cURL 扩展;
  • PHP 的 libcurl 是否支持接口要求的 TLS 版本;
  • PHP 和命令行 curl 是否使用了不同的 SSL 后端;
  • PHP 运行用户是否有权读取证书、私钥和 CA 文件;
  • 证书与私钥是否匹配;
  • 私钥密码是否正确;
  • 客户端证书是否仍在有效期内;
  • 测试证书是否用于正确的测试环境。

也可以临时启用详细日志:

$verbose = fopen('php://temp', 'w+');

curl_setopt($curl, CURLOPT_VERBOSE, true);
curl_setopt($curl, CURLOPT_STDERR, $verbose);

$response = curl_exec($curl);

rewind($verbose);
$debugLog = stream_get_contents($verbose);
fclose($verbose);

error_log($debugLog);

详细日志可能包含证书路径、主机名和 TLS 环境信息,不要在生产环境中直接展示给访客。

如果 PHP 参数已经与命令行完全一致,但请求仍然只有在 PHP 中失败,最可能的原因是两者使用了不同的 libcurl/TLS 后端,或者 PHP 运行用户无法正确读取证书和私钥。此时应对比命令行 curl --version 与 curl_version() 的输出,并升级或重新配置 PHP cURL/OpenSSL 环境。

备注:内容仅供参考。