PAYATHON 2026

Firebase Cloud Functions/Hosting 是否支持 SSL pinning?

支付老李

结论

Firebase Cloud Functions 和 Firebase Hosting 都能提供 HTTPS,但无法保证 SSL Certificate 长期固定、由使用者控制且不会自动轮换。如果银行要求严格 pin 某一张服务器证书,Firebase 默认的托管证书通常无法满足要求。

Firebase 负责证书的签发、续期和轮换。证书更新后,原有 .cer 或 .crt 文件对应的指纹可能失效,导致银行发送的 payment notification 失败。

在确定方案前,应先向银行确认:它要求的是严格的 SSL pinning,还是只需上传当前服务器证书来建立信任。这两种情况的处理方式不同。

为什么 Firebase 的托管证书不适合固定证书 Pinning

访问以下地址时,TLS 通常在 Google 管理的基础设施上终止:

https://<project-id>.web.app/
https://<project-id>.firebaseapp.com/
https://<region>-<project-id>.cloudfunctions.net/<function-name>

即使 Firebase Hosting 绑定了自定义域名,证书通常仍由 Firebase 自动签发和续期。使用者无法保证以下内容长期不变:

  • 服务器叶子证书的内容;
  • 证书序列号;
  • 证书指纹;
  • 证书公钥;
  • 中间证书;
  • 实际使用的 Certificate Authority;
  • 证书轮换时间。

因此,将当前线上证书导出为 .cer 或 .crt 并提交给银行,只能代表当前使用的证书。Firebase 续期或更换证书后,银行保存的证书可能不再匹配。

Firebase Hosting 的 rewrite 也不会改变这一点。例如:

{
  "hosting": {
    "rewrites": [
      {
        "source": "/payment/notification",
        "function": "paymentNotification"
      }
    ]
  }
}

在这种配置下,银行访问的是 Firebase Hosting 地址,TLS 仍在 Firebase 托管层终止,而不是由 Cloud Function 代码处理。函数无法决定向银行展示哪一张服务器证书。

先确认银行的真实要求

银行表格中的描述未必指严格意义上的 certificate pinning,也可能只是要求:

  1. 提供 HTTPS Response URL;
  2. 提供端口,通常为 443;
  3. 上传服务器证书,供银行系统导入信任库;
  4. 使用由银行认可的 CA 签发的证书。

建议请银行明确回答以下问题:

  • Pin 的是叶子证书、证书公钥,还是 CA/intermediate certificate?
  • 证书续期后是否需要重新提交?
  • 是否支持证书重叠更新,也就是同时保存新旧两张证书?
  • 是否接受 Google 托管证书使用的 CA?
  • 是否只接受表格中列出的 Comodo 或 VeriSign?
  • 是否支持标准的公共 CA 链验证,而不是固定证书指纹?

如果银行只进行标准 TLS certificate chain validation,Firebase 提供的 HTTPS 可能已经足够,不一定需要固定证书。如果银行指定了 CA,还要检查 Firebase 实际使用的证书链是否符合要求,不能仅凭 Firebase 支持 HTTPS 就认定满足条件。

可行方案

方案一:让银行支持标准 CA 验证

这种方案的维护成本最低。可以向银行提供:

Response URL: https://your-domain.example/payment/notification
Port: 443

银行按照标准 HTTPS 规则验证证书链和主机名,不固定某一张叶子证书。Firebase 则继续自动续期证书。

能否采用这种方案,取决于银行是否接受标准公共 CA 验证。

方案二:由银行 Pin CA 或中间证书

如果银行允许 pin CA 或 intermediate certificate,而不是 pin 叶子证书,证书续期带来的影响可能较小。

不过,这种方式仍有风险。Firebase 或其基础设施以后可能调整 CA 或证书链。除非银行允许配置多个 pin,并提供明确的更新流程,否则它仍不能作为永久稳定的方案。

方案三:在 Firebase 前面部署可控的 HTTPS 入口

如果银行必须固定证书,可以使用能够自行管理证书的 HTTPS 入口,例如:

银行
  ↓ HTTPS,展示由你管理的证书
自有域名 + Reverse Proxy / Load Balancer / API Gateway
  ↓ HTTPS
Cloud Functions 或 Cloud Run

外部入口负责:

  • 使用银行认可 CA 签发的证书;
  • 固定 Response URL;
  • 控制证书轮换的上线时间;
  • 将 payment notification 转发给后端函数;
  • 保存访问日志,并实施 IP、签名和重放保护。

可选服务要根据现有云架构确定。例如,可以使用支持自管理证书的反向代理或负载均衡器,再把请求转发到 Cloud Run 或 Cloud Functions。部署前,需要确认所选负载均衡产品、后端类型和区域组合确实受支持。

下面是一个简化的 Nginx 配置:

server {
    listen 443 ssl;
    server_name payment.example.com;

    ssl_certificate     /etc/nginx/tls/payment-fullchain.crt;
    ssl_certificate_key /etc/nginx/tls/payment-private.key;

    location = /payment/notification {
        proxy_pass https://REGION-PROJECT_ID.cloudfunctions.net/paymentNotification;
        proxy_set_header Host REGION-PROJECT_ID.cloudfunctions.net;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

生产环境还需要设置超时和请求大小限制,对日志做脱敏处理,并为代理到后端的请求配置身份认证。入口使用 HTTPS,并不意味着后端函数可以开放给任何人调用。

方案四:将支付回调入口移出 Firebase 托管证书体系

如果银行对 CA、证书格式、固定公钥或变更流程有严格要求,可以将 payment notification endpoint 部署到支持自有证书的服务。收到并验证通知后,再通过受保护的接口或消息队列写入 Firebase。

这样能把银行接口的证书要求与 Firebase 的证书生命周期分开处理。

如何检查当前证书

可以使用 OpenSSL 查看 Firebase 地址或自定义域名当前返回的证书:

openssl s_client \
  -connect payment.example.com:443 \
  -servername payment.example.com \
  -showcerts </dev/null

导出当前叶子证书:

openssl s_client \
  -connect payment.example.com:443 \
  -servername payment.example.com </dev/null 2>/dev/null |
openssl x509 -outform PEM > payment-current.crt

查看证书信息和 SHA-256 指纹:

openssl x509 \
  -in payment-current.crt \
  -noout \
  -subject \
  -issuer \
  -dates \
  -fingerprint \
  -sha256

这些命令只能说明检查当时服务器展示了哪张证书,不能证明 Firebase 以后仍会使用同一张证书。因此,不能把导出的当前证书直接作为长期 pinning 方案。

回调函数仍需验证业务签名

SSL pinning 只能保护 TLS 连接,不能替代对支付通知的业务签名验证。回调处理至少还应包括:

  • 验证银行提供的签名或 MAC;
  • 校验订单号、金额和币种;
  • 防止重复通知;
  • 防止旧请求重放;
  • 使用幂等事务更新支付状态;
  • 避免在日志中记录银行卡号、密钥和完整支付报文。

下面是一个简化的 Node.js 示例:

const { onRequest } = require("firebase-functions/v2/https");
const crypto = require("node:crypto");

function verifySignature(rawBody, receivedSignature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const expectedBuffer = Buffer.from(expected, "hex");
  const receivedBuffer = Buffer.from(receivedSignature || "", "hex");

  return (
    expectedBuffer.length === receivedBuffer.length &&
    crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
  );
}

exports.paymentNotification = onRequest(async (req, res) => {
  if (req.method !== "POST") {
    res.status(405).send("Method Not Allowed");
    return;
  }

  const rawBody = req.rawBody;
  const signature = req.get("X-Payment-Signature");
  const secret = process.env.PAYMENT_WEBHOOK_SECRET;

  if (!secret || !verifySignature(rawBody, signature, secret)) {
    res.status(401).send("Invalid signature");
    return;
  }

  // 根据银行协议解析报文,并以交易编号执行幂等更新。
  res.status(200).send("OK");
});

签名算法、Header 名称、报文编码和响应内容都必须以银行的实际接口规范为准,不能直接照搬示例。

证书更新时的注意事项

即使使用自管理证书,也不存在永不过期的 SSL Certificate。可靠的更新流程应包括:

  1. 提前申请新证书;
  2. 将新证书提交给银行;
  3. 让银行同时信任新旧证书;
  4. 确认银行完成配置后再切换;
  5. 保留旧证书一段重叠期;
  6. 分别在 UAT 和 Production 验证通知能否送达;
  7. 监控证书有效期和 payment notification 失败率。

如果银行不支持新旧证书并存,双方必须协调明确的切换窗口,否则证书续期时必然会有通知中断的风险。

Firebase 支持 HTTPS,但其托管证书不能被视为固定证书。如果银行坚持 pin 某张叶子证书,应在 Firebase 前增加一个使用自管理证书的 HTTPS 入口,或者将支付回调部署到证书可控的基础设施上。

备注:内容仅供参考。