私钥放在公网服务器,软件授权迟早会出事?Quartermaster 的双机签名隔离方案
独立开发者售卖桌面软件、内部工具或自托管产品时,常见的授权流程看似简单:支付成功后生成一串许可证,客户端启动时向服务器校验。真正难的部分不是生成随机字符串,而是保护“能签发有效许可证的私钥”。一旦私钥和公网 Web 服务放在同一台机器上,攻击者只要攻破这台机器,就可能绕过支付流程无限签发许可证。
Quartermaster 是一个面向独立开发者的自托管授权签发与交付项目。它的重点不是把授权做成 SaaS,而是把签名能力从公网入口中拆出去:公网服务处理 Stripe Webhook、激活和邮件交付;另一台受信任机器保存 Ed25519 私钥并负责签名。项目发布了 v1.0.0,但采用 PolyForm Noncommercial 1.0.0 许可:可用于非商业学习、研究与个人项目,商业使用需要单独授权,因此不应把它误称为宽松开源授权方案。
先定义边界:许可证系统到底要防什么
授权系统不必解决所有问题。离线可验证许可证通常希望满足三件事:
- 客户端能在没有网络、甚至授权服务器已下线时验证许可证;
- 修改产品编号、版本上限或席位数会导致验证失败;
- 同一密钥不能被大量陌生机器同时滥用。
但“离线验证”和“限制多人共享”天然有矛盾:如果每台客户端都只看本地许可证,它们无法知道这串密钥是否已经被其他机器占满。Quartermaster 的取舍是只让首次激活在线完成;服务器记录机器指纹占用的席位,之后客户端用嵌入的公钥离线验签。用户主动停用后释放席位,另一台机器才可以重新激活。
这不是 DRM 万能解。已成功激活的机器即使之后发生退款或拒付,仍可持续离线运行;项目把这定义为可接受的、有限的商业成本。重要的是把产品承诺写成精确的威胁模型,而不是宣传“绝对防破解”。
双机而不是“更严的防火墙”
Quartermaster 包含两个独立程序。公网侧的 quartermaster 运行在服务器上,接收并校验 Stripe Webhook、写入签名请求队列、提供激活接口、发送邮件;它只持有公钥,不能生成新许可证。私钥只存在于受信任机器上的 signer 中,后者经由 WireGuard 主动轮询队列,签完后再把结果提交回公网侧。
这个方向很关键:公网机器不能主动连入签名机,签名机也不暴露入站服务。即使公网服务器被完全控制,攻击者最多能向受日志和限速约束的入口提交签名请求,无法直接读取或替换私钥。安全性不再只依赖“运维永不失误”,而是由部署拓扑限制攻击能力。
一次购买大致经过以下路径:
Stripe Checkout 完成 → Stripe Webhook(公网 quartermaster) → 校验签名、时间窗口与购买元数据 → SQLite 中写入 sign_requests → signer 经 WireGuard 长轮询取走请求 → Ed25519 私钥签名 → signer 回传结果 → 公网服务邮件发送许可证 → 客户端首次激活并登记一个席位
文档说明 Webhook 校验的是未经解析的原始请求体,并使用 Stripe 签名与五分钟重放窗口;席位元数据会先做下限和上限检查。这里的原则可以迁移到其他支付回调:先验证消息来源和时效,再执行业务写入;不要先解析、落库、发货后才补验签。
固定格式为什么比“JWT 随便塞字段”更适合长期授权
Quartermaster 的许可证不是依赖服务器数据库查询的 opaque token,而是“固定载荷 + Ed25519 签名”。v1 载荷为 33 字节,包含格式版本、随机许可证 ID、四字符产品代码、产品主版本、席位数与 UTC 签发时间;再附上 64 字节签名,最终以无填充 Base32 展示。
固定布局的价值是兼容性边界清楚:格式版本描述字节布局,产品主版本描述产品自身的兼容范围,两者不能混为一谈。验证器先拒绝未知的格式版本,再进行签名验证,避免未来格式改变后仍按旧偏移读取字段。由于签名覆盖完整载荷,攻击者即使能看到 Base32 内容,也不能把 1 个席位改成 100 个席位。
不过,Base32 只是便于输入与展示,不提供保密性。把可逆编码当成“加密”是授权系统里很常见的误区;真正需要保护的是私钥以及激活接口的业务规则。
激活接口应该以幂等性和席位计数为中心
客户端首次联网激活时,Quartermaster 将机器标识和产品代码计算为 SHA-256 指纹。对同一许可证、同一指纹重复激活是幂等操作:它不应再次消耗席位。只有新指纹到来且活跃数已经达到签名载荷中的席位数时,接口才返回 409 Conflict;被撤销的许可证则拒绝激活。
这比“每次启动都联网打卡”更适合离线优先产品:网络短暂不可用不会让已付费用户无法使用,也减少了服务可用性成为产品单点故障的风险。代价是无法实时收回已离线运行的副本。是否接受该代价,应由产品类型、退款政策和风险模型决定。
用项目现有入口做一次可复核的代码阅读
仓库没有把它包装成一条适合所有人的一键部署命令;部署依赖两台机器、WireGuard、Stripe 与邮件服务配置,应该先阅读运维文档并在隔离环境验证。对源码和信任边界的第一步可以从测试开始:
git clone https://github.com/laudendev/quartermaster.git cd quartermaster go test ./...
项目 README 给出的部署入口是从签名机执行的 ./deploy.sh,不是从公网服务器执行。这个细节体现了双机模型:构建和发布动作也不应要求把私钥所在角色混入公网节点。实际接入支付前,还应分别演练 Webhook 重放、签名机离线、队列堆积、席位耗尽、用户停用和密钥轮换;其中任何一个失败模式都比“能否生成许可证字符串”更接近生产风险。
何时值得采用这种复杂度
若你只是在验证一个原型,成熟授权服务或简单的服务端校验通常更省事。若产品需要离线可用、希望不被按交易抽成的授权平台绑定,并且愿意承担两台机器、密钥仪式、隧道和事件响应的运维成本,分离签名机是一条值得认真考虑的路径。
Quartermaster 最值得借鉴的不是某个 Go 包,而是先把不可妥协的安全性质写清楚:公网节点永远不能伪造许可证;能够签名的节点永远不接受公网入站连接。之后再让 Webhook、队列、激活和交付流程全部服务于这两个边界。
还有一个实践提醒:不要把“私钥离开公网”简化为把密钥文件复制到另一台 VPS。签名机需要独立的账户权限、备份策略、系统更新节奏和审计日志;回传结果的接口同样应当限制在隧道内,并校验请求身份。真正的隔离不仅是网络地址不同,还包括密钥生命周期、部署权限和故障恢复过程不能在同一次入侵里被同时接管。
相关链接