4

集成 Paypal 的智能按钮后,我无法验证 Paypal 发送的 webhook 通知。我发现的示例要么已过时,要么不起作用。

有没有办法验证 webhook 通知,最好是 DIY 方式(即无需使用庞大而复杂的 Paypal API)?

4

3 回答 3

5

据我所知,此代码只是实际有效的代码。我在堆栈溢出中找到的所有其他示例都不起作用,因为在编写签名字符串时,它们没有传递 webhook本身的 ID,而是使用 webhook 事件的 ID,因此验证将失败。

在 Paypal 的开发人员后端添加 webhook 后,将生成 webhook ID。创建 webhook 后,您将在已安装的 webhook 列表中看到它的 id。

其余的很简单:我们获取标头和 HTTP 正文并使用 Paypal 的配方组成签名:

为了生成签名,PayPal 使用竖线 (|) 字符连接和分隔这些项目。

“这些项目”是:传输 id、传输日期、webhook id 和 HTTP 正文上的 CRC。前两个可以在请求的头部找到,开发者后端的 webhook id(当然,那个 id 永远不会改变),CRC 的计算如下所示。

证书的位置也在标头中,因此我们加载它并提取私钥。

最后要注意的事情:Paypal 提供的算法名称(同样在标头字段中)与 PHP 所理解的不完全相同。Paypal 将其称为“sha256WithRSA”,但openssl_verify预计会出现“sha256WithRSAEncryption”。

// get request headers
$headers=apache_request_headers();

// get http payload
$body=file_get_contents('php://input');

// compose signature string: The third part is the ID of the webhook ITSELF(!),
// NOT the ID of the webhook event sent. You find the ID of the webhook
// in Paypal's developer backend where you have created the webhook
$data=
    $headers['Paypal-Transmission-Id'].'|'.
    $headers['Paypal-Transmission-Time'].'|'.
    '[THE_ID_OF_THE_WEBHOOK_ACCORDING_TO_DEVELOPER_BACKEND]'.'|'.
    crc32($body);

// load certificate and extract public key
$pubKey=openssl_pkey_get_public(file_get_contents($headers['Paypal-Cert-Url']));
$key=openssl_pkey_get_details($pubKey)['key'];

// verify data against provided signature 
$result=openssl_verify(
    $data,
    base64_decode($headers['Paypal-Transmission-Sig']),
    $key,
    'sha256WithRSAEncryption'
);

if ($result==1) {
    // webhook notification is verified
    ...
}
elseif ($result==0) {
    // webhook notification is NOT verified
    ...
}
else {
    // there was an error verifying this
    ...
}
于 2020-07-13T06:47:32.107 回答
0

为nodejs回答这个问题,因为原始(但非常有用)的答案存在微妙的安全问题和一些缺失的逻辑。这个答案解决了以下问题:

  1. 有人输入自己的 URL,从而获得对自己请求的身份验证
  2. CRC 需要是无符号整数,而不是有符号整数。
  3. NodeJs < 17.0 缺少一些内置的 X509 功能。
  4. 理想情况下,应该使用内置的证书链来验证签名证书,但 NodeJS < 17.0 不能轻易做到这一点 AFAICT。信任模型依赖于证书获取 URL 的 TLS 和内置的 nodejs 信任链,而不是从 cert URL 返回的证书,这可能已经足够好了。
const forge = require('node-forge');
const crypto = require('crypto')
const CRC32 = require('crc-32');
const axios = require('axios');


  const transmissionId = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-ID'];
  const transmissionTime = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-TIME'];
  const signature = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-SIG'];
  const webhookId = '<your webhook ID from your paypal dev. account>';
  const url = paypalSubsEvent.headers['PAYPAL-CERT-URL'];
  const bodyCrc32 = CRC32.str(paypalSubsEvent.body);  
  const unsigned_crc = bodyCrc32 >>> 0;     // found by trial and error

  // verify domain is actually paypal.com, or else someone
  // could spoof in their own cert
  const urlObj = new URL(url);
  if (!urlObj.hostname.endsWith('.paypal.com')) {
    throw new Error(
      `URL ${certUrl} is not in the domain paypal.com, refusing to fetch cert for security reasons`);
  }
  const validationString =
    transmissionId + '|'
    + transmissionTime + '|'
    + webhookId + '|'
    + unsigned_crc;

  const certResult = await axios.get(url);   // Trust TLS to check the URL is really from *.paypal.com
  const cert = forge.pki.certificateFromPem(certResult.data);
  const publicKey = forge.pki.publicKeyToPem(cert.publicKey)
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(validationString);
  verifier.end();
  const result = verifier.verify(publicKey, signature, 'base64');
  console.log(result);

于 2022-01-25T05:56:35.920 回答
-1

对此做出响应以节省潜在的麻烦,但上面的示例不起作用,因为需要将身份验证令牌与您对证书文件“ file_get_contents($header['Paypal-Cert-Url'])”的获取请求一起发送将无法单独工作。

只需在标头中包含您的身份验证令牌即可。

于 2021-03-01T07:50:52.153 回答