laudendev/quartermaster

GitHub: laudendev/quartermaster

面向独立开发者的自托管软件许可证平台,采用双机分离密钥架构和 Stripe 集成,以固定成本实现安全的许可证签发与席位管理。

Stars: 5 | Forks: 0

# Quartermaster 我想销售软件。我不想为了获得这种特权,而把每一笔销售的一部分永远分给授权平台。我研究了市面上的方案并拒绝了所有选项——每一个选项的收费都会随着你的成功而变得*更*昂贵,这完全搞反了。固定的基础设施成本就应该保持固定。如果一个授权平台的账单仅仅因为我卖出了更多副本而增加,那么这个平台是在为我的成功收费,而不是为它提供的服务收费。 它们中也没有一个是为单兵作战的人准备的。它们假设你拥有一个团队、一个支持组织、一个合规部门——这些是独立开发者既没有也不需要的额外开销。因此,我没有妥协,而是构建了我真正想要的东西:Stripe 负责处理资金,而之后的所有事情——签发许可证、验证其真实性、限制可在多少台机器上运行——都由我自己掌控。它是自托管的,其规模恰好适合一个人运行,并且无论我是卖出十个还是一万个许可证,它的成本都是一样的固定金额。 整个系统建立在一个严格的安全保证之上:暴露在互联网上的机器永远无法伪造许可证,而能够伪造许可证的机器永远无法从互联网访问。 这不是一个玩具或教程项目。它的构建和测试采用了与真正的生产级软件同样严格的标准——真正的信任边界、对每一个边界的真实测试覆盖率、一个真实的、经过端到端验证(已针对真实的结账流程进行验证)的 Stripe 集成部署——随时准备向真实的客户签发真实的许可证。 ## 它的功能 对已确认的 Stripe 付款做出反应,签发 Ed25519 签名的许可证,执行基于席位(per-seat)的激活,并通过电子邮件发送结果——除了 Stripe 自身的费用外,没有单笔交易手续费,没有用户数量上限,也不依赖任何第三方授权平台的正常运行时间。 ## 为什么架构是这样的 私有签名密钥——唯一能够生成有效许可证的东西——保存在一台只*主动发起*出站连接、从不接受入站连接的机器上。如果面向公众的服务器被完全攻破,攻击者只能得到一个有日志记录、有速率限制的队列来提交*请求*——永远无法签署任何东西。这种权衡(使用两台机器和一个隧道,而不是一台更简单的服务器)是深思熟虑的:它使得最坏情况下的故障模式在结构上变得不可能,而不仅仅是可能性不大。 许可证格式本身是一个固定宽度、带版本号的二进制 payload,被设计为一个永久的 ABI——今天签发的许可证,意味着几年后,在没有服务器且不需要网络调用的情况下,面对应用程序中内置的公钥,依然能够正确地离线验证。 从一开始,验证在设计上就是多密钥的,因此未来可以轮换签名密钥,而不会使已经售出的许可证失效。 完整设计请参见 [docs/architecture.md](docs/architecture.md)。 ## 许可机制实际上如何对待客户 大多数授权平台默认将每一次激活视为可疑行为——将密钥永远锁定在一台机器上,要求提交支持工单才能重新安装,使得转售实际上变得不可能。我故意构建了完全相反的做法。 这里的许可证表现得就像一件实物商品。停用它,席位就会释放——把它交给其他人,他们可以直接重新激活,这与转售任何其他二手物品没有区别。该平台从不试图追踪首次销售后*谁*拥有许可证,只关心*同时有多少台机器*在使用它。退款或信用卡拒付也不会追溯性地破坏一台已经在合法运行的机器——这被视为一种可接受的、有边界的成本,而不是一个需要通过工程手段消除的 bug。 但它确实在严格阻止一件事:一个密钥被公开发布,并被无限多的陌生人同时认领。一旦尝试激活的、互不相同的且前所未见的机器数量超过了许可证的席位数量,请求将被彻底拒绝——席位数量是真正的执行手段,而不是机器追踪,不是回传验证(phone-home),也不是周期性重新验证。 一旦机器被激活,就结束了——对于之后每次启动,验证都是完全离线的,永远如此。没有签入(check-in),没有过期时间,也不依赖于我的服务器保持运行。在整个生命周期中,唯一的一次网络调用就是首次激活本身。 这两个方面都基于同一个理念:服务器只需要知道*有多少*台机器正在使用许可证,而不需要知道*是哪个人*拥有它。这使得在转售和重新安装方面表现得非常慷慨,同时又能对真正耗费成本的事情——大规模密钥共享——保持严格,成为可能。完整的推理过程以及它刻意要防止和不防止的事情,都在 [docs/license-scheme.md](docs/license-scheme.md) 中。 ## 如果你正在构建类似的东西 这虽然不是作为教学示例而构建的,但它完全经得起作为示例的考验,因为它的每一个部分都是真实的:真实的资金流经真实的 Stripe 集成,由实际的机器隔离(而不是配置标志)所强制执行的真正加密信任边界,针对真实数据库的真实席位强制执行,以及测试了实际故障模式(而不仅仅是理想路径)的用例。 如果你正试图弄清楚如何作为一个人销售软件——如何构建公共服务器与签名密钥之间的信任边界,如何设计一个必须在没有后端服务器的情况下持续运行多年的许可证格式,如何正确处理 Stripe webhook(签名验证、重放防御、幂等性)——`docs/architecture.md` 和 `docs/license-scheme.md` 介绍了实际的推理过程,而不仅仅是代码。这些推理才是值得借鉴的部分。 ## 文档 - **[docs/architecture.md](docs/architecture.md)** — 双程序拆分、整个设计所依赖的安全属性,以及端到端的购买流程。 - **[docs/license-scheme.md](docs/license-scheme.md)** — 规范标准:payload 字节布局、席位规则、产品代码格式、指纹派生、激活状态机以及完整的威胁模型。 - **[docs/api.md](docs/api.md)** — 每个 HTTP endpoint、请求和响应结构、状态码。 - **[docs/operations.md](docs/operations.md)** — 部署、密钥仪式和轮换、备份、事件响应、支持手册。 ## 组件 | 路径 | 它是什么 | |---|---| | `cmd/quartermaster` | 面向公众的服务:Stripe webhook、激活 API、队列 API。运行在 droplet 上。 | | `cmd/signer` | 轮询队列,使用私钥签署许可证。仅在受信任的硬件上运行——绝不在 droplet 上。 | | `cmd/keygen` | 为 `signer` 进行的一次性 Ed25519 密钥对生成。 | | `license` | 签名的许可证格式本身——构建、签名、验证。 | | `queue` | 拥有 `sign_requests` 表:Stripe → signer 交接。 | | `activations` | 拥有 `activations` 表:基于席位的激活、停用、撤销。 | ## 开发 ``` go test ./... ``` 每个信任边界都有真实的测试覆盖率——签名验证(有效、被篡改、密钥错误、版本无法识别)、webhook 业务逻辑、队列的并发行为,以及激活模型的席位计算。 ## 部署 ``` ./deploy.sh ``` 从 signer 机器而不是 droplet 上运行——原因请参见 [docs/operations.md](docs/operations.md),其中还包含了完整的部署、密钥仪式和备份流程。 ## 关于 AI 辅助开发 截至 2026 年中,我在整个项目的设计和实现过程中使用了 AI 工具——特别是 Claude Sonnet 5 和 Claude Fable 5——用于代码审查、文档起草以及探讨设计权衡。我对此毫无歉意;仅仅因为外界的看法就拒绝一种能产生真正良好效果的工具是愚蠢的,而非有原则的。 这里的任何环节都没有运行 AI。这个代码库中的每一个二进制文件都是确定性的 Go,编译一次并完全按照所写的内容执行——在 runtime、droplet、signer 或请求路径的任何地方都没有模型参与循环。AI 是一个在构建过程中使用的起草工具,与编译器属于同一类工具:它帮助将决策转化为可运行的语法。而这些决策——分离密钥架构、席位规则、payload 的字节布局及其为何如此进行版本控制——都是我做出的。 我没有盲目相信其中任何一行代码。每一行代码都经过了验证,而不是想当然——包括逐字节地查阅 Go 标准库自身的 base32 实现,以确切了解它的功能及其原因,而不是仅仅信任对它的总结。当 AI 建议的测试代码存在真正的 bug 时(确实存在),这些 bug 都被实际运行的测试捕获了。当 AI 起草的许可证文件出现实质性错误时,它也被捕获了,方法是在它发布之前,获取并将其与实际的规范源代码进行 diff 对比。 这不是凭感觉编码(vibe-coded)。它是 AI 辅助且经过严格验证的——这也是我在开发客户付费产品时使用这些工具的唯一方式。 ## License [PolyForm Noncommercial 1.0.0](LICENSE)。可免费阅读、学习并用于任何非商业目的——个人项目、研究、教育、非营利组织和政府。商业使用需要另行达成协议。
标签:EVTX分析, 日志审计, 测试用例