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,也可能只是要求:
- 提供 HTTPS Response URL;
- 提供端口,通常为
443; - 上传服务器证书,供银行系统导入信任库;
- 使用由银行认可的 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。可靠的更新流程应包括:
- 提前申请新证书;
- 将新证书提交给银行;
- 让银行同时信任新旧证书;
- 确认银行完成配置后再切换;
- 保留旧证书一段重叠期;
- 分别在 UAT 和 Production 验证通知能否送达;
- 监控证书有效期和 payment notification 失败率。
如果银行不支持新旧证书并存,双方必须协调明确的切换窗口,否则证书续期时必然会有通知中断的风险。
Firebase 支持 HTTPS,但其托管证书不能被视为固定证书。如果银行坚持 pin 某张叶子证书,应在 Firebase 前增加一个使用自管理证书的 HTTPS 入口,或者将支付回调部署到证书可控的基础设施上。
备注:内容仅供参考。
