GrantSeal signs software rights, not secrets.
2.6K
English | 简体中文 — full docs: English | 中文文档 — Changelog
grantseal is an offline software licensing library and CLI written in
Go 1.26 using only the standard library (crypto/ed25519, crypto/rand,
crypto/sha256, crypto/hmac, crypto/subtle, encoding/json). No third-party
dependencies. It issues Ed25519-signed licenses on the issuer side and verifies
them client-side with a fail-closed, never-panicking verifier.
Its goal is to raise the cost of forgery and offline tampering, not to make
software uncrackable. See SECURITY.md for the honest threat
model and limits.
A good fit when you:
Not a fit when you:
| Claim | How you can verify it |
|---|---|
| Zero third-party dependencies | go.mod has no require block for external modules; build with go build ./.... |
| Ed25519-only, no algorithm downgrade | pkg/license/verifier.go rejects any non-Ed25519 algorithm; see error LICENSE_UNSUPPORTED_ALGORITHM. |
| Private keys never link into a client | Clients import pkg/license; signing lives under internal/issuer (unimportable). CI scans release artifacts for key material. |
| Signature covers the whole payload | Deterministic canonical JSON is signed verbatim; any edit yields LICENSE_SIGNATURE_INVALID. See architecture. |
| Fail-closed, never panics | The verifier returns a stable LICENSE_* code on every failure path and does not panic on malformed input. |
| Golden envelope vectors carry no private key | Test vectors embed only public keys, canonical payloads, and signatures. See architecture. |
| Package / Command | Role |
|---|---|
pkg/license | Client-side verification only. Public keys, signature checking, validation orchestration, fail-closed. Never contains private keys. |
pkg/fingerprint | Cross-platform device fingerprint (Linux/macOS/Windows + fallback). |
internal/issuer | Issuer-side private-key logic (keygen, signing, issuing, revocation lists). Isolated via Go internal/. |
cmd/license-tool | Issuer CLI: keygen, public-key, issue, verify, inspect, fingerprint, revoke-list, version. |
examples/ | Client integration & batch-issue config examples. See examples/README.md. |
# Issuer: generate a key pair into a gitignored dir (private key stays local).
# ./_keys is gitignored; never commit a private key.
go run ./cmd/license-tool keygen -key-id k1 -out-dir ./_keys
# Issue a license
go run ./cmd/license-tool issue -config examples/issue-config.json \
-key ./_keys/k1-private.key -out customer.lic
# Client: verify + policy-validate
go run ./cmd/license-tool verify -license customer.lic -pubkey ./_keys/k1-public.key \
-product acme-app -version 1.4.0
The client embeds only public keys and never touches the private key. Always branch on the stable error code and provide a recovery path — do not ignore the error.
ring := license.NewKeyRing()
_ = ring.AddPublicKeyBase64("k1", embeddedPublicKeyB64)
mgr := license.NewManager(ring)
res, err := mgr.LoadAndValidate("customer.lic", license.ValidationContext{
ProductID: "acme-app",
ProductVersion: "1.4.0",
})
if err != nil {
switch license.CodeOf(err) {
case license.CodeExpired:
// prompt the user to renew; show the expiry date
case license.CodeDeviceMismatch:
// show a device request code and ask the user to re-bind
case license.CodeClockRollback:
// warn about the system clock; block time-bound features
default:
// treat as invalid; offer to re-import a license file
}
return
}
if err := res.RequireFeature("api"); err != nil {
// license.CodeOf(err) == license.CodeFeatureDenied
}
if err := res.CheckLimit("max_seats", seatsInUse); err != nil {
// license.CodeOf(err) == license.CodeLimitExceeded
}
See examples/client/main.go and the full library
guide in docs/enUS/README.md.
internal/issuer + cmd/license-tool): holds the private
key, runs keygen/issue/revoke-list. Runs only on trusted issuer
machines. The private key is written mode 0600 and never overwritten without
-force.pkg/license): embeds public keys only, verifies and gates.
Cannot import internal/issuer (enforced by Go's internal/ mechanism), so
private-key logic never links into a client binary.| Business need | Mechanism | On failure |
|---|---|---|
| "Is this license genuine and unmodified?" | Ed25519 signature over the canonical payload | LICENSE_SIGNATURE_INVALID / LICENSE_MALFORMED |
| "Has this customer's subscription expired?" | license_type time semantics + expires_at + grace period | LICENSE_EXPIRED / status grace |
| "Is this edition/feature allowed?" | Edition defaults unioned with features; RequireFeature | LICENSE_FEATURE_DENIED |
| "Are they within seat/usage limits?" | Range-validated limits; CheckLimit | LICENSE_LIMIT_EXCEEDED |
| "Is this running on a licensed device?" | Device binding (none/single/multi) + fingerprint | LICENSE_DEVICE_MISMATCH |
| "Is this build within the covered version range?" | version_constraint with maintenance/covered ceiling (fail-closed) | LICENSE_VERSION_UNSUPPORTED |
| "Has this specific license been revoked?" | Signed offline revocation list | LICENSE_REVOKED |
| "Did someone roll the clock back to dodge expiry?" | Integrity-protected local rollback state (naive rollback only) | LICENSE_CLOCK_ROLLBACK |
subtle.ConstantTimeCompare) for sensitive comparisons.internal/issuer + the CLI.limits range validation, unknown enums rejected, license-file size cap, atomic writes.license_type time semantics enforced: trial/subscription require expires_at; lifetime must not carry one and never expires.Limits (by design): binary patching / reverse engineering, privileged clock
manipulation beyond the rollback heuristic, fingerprint drift, and offline
revocation freshness (a client only knows about revocations in the list it has).
See SECURITY.md and docs/enUS/architecture.md.
CI runs unit tests on Linux, macOS, and Windows, race detection on Linux, and a short fuzz target for envelope parsing. Coverage and benchmark results are generated from the referenced commit; see Quality and Performance for full detail.
e5c6e93, go test ./... -covermode=atomic reports
77.5% total statement coverage (pkg/license 82.9%, pkg/fingerprint
90.7%, internal/issuer 85.4%, cmd/license-tool 70.7%). The CI gate enforces
>= 77%. The Coverage badge above is generated by CI from the same run.
Details in docs/enUS/quality.md.go1.26.6, in-memory validation
of the typical fixture measures ~33904 ns/op, 6072 B/op, 41 allocs/op;
in-memory signature verify ~34552 ns/op; envelope parse ~2738 ns/op
(median of -count=5). These numbers describe the recorded environment rather
than a cross-device guarantee. Full methodology and per-path results (with
environment and commit SHA) live in
docs/enUS/performance.md.Verification has distinct cost/side-effect profiles depending on the path:
Verifier.Verify over an in-memory
envelope. No disk I/O, no policy checks.Manager.LoadAndValidate reads
the license file and may read/write the anti-rollback state file (disk I/O).pkg/fingerprint reads platform hardware
identifiers; cost and availability depend on the host OS.license-tool is the issuer-side CLI (it holds private-key logic and is only
for authorized issuers). Pick whichever install method fits your workflow.
Download a release binary — grab the archive for your OS/arch from the
releases page, extract, and
run license-tool.
Homebrew (macOS / Linux):
brew tap soulteary/tap
brew install soulteary/tap/grantseal
After installation the license-tool command is available globally.
Docker:
docker pull soulteary/grantseal:latest
For full Docker usage (issuer keygen/issue and client verify, with private-key
safety notes) see docs/enUS/README.md.
docs/enUS/README.mddocs/enUS/architecture.mddocs/enUS/quality.mddocs/enUS/performance.mdSECURITY.mdexamples/README.mdTrustedTimeProvider for authoritative time.grantseal 是使用 Go 1.26 纯标准库(crypto/ed25519、crypto/rand、
crypto/sha256、crypto/hmac、crypto/subtle、encoding/json)实现的离线软件
授权库与 CLI,不引入任何第三方依赖。它在签发端生成 Ed25519 签名的授权文件,在
客户端以 fail-closed、绝不 panic 的验证器进行校验。
它的目标是提高伪造与离线篡改的成本,而非让软件不可破解。诚实的威胁模型与边界
见 SECURITY.md。
适合的场景:
不适合的场景:
| 主张 | 你如何验证 |
|---|---|
| 零第三方依赖 | go.mod 无外部模块 require;用 go build ./... 构建即可确认。 |
| 仅 Ed25519、不降级算法 | pkg/license/verifier.go 拒绝任何非 Ed25519 算法;对应错误码 LICENSE_UNSUPPORTED_ALGORITHM。 |
| 私钥绝不链接进客户端 | 客户端只 import pkg/license;签名逻辑在 internal/issuer(不可被 import)。CI 扫描发布物无密钥材料。 |
| 签名覆盖整个 payload | 确定性规范化 JSON 被逐字签名;任何改动都会得到 LICENSE_SIGNATURE_INVALID。见架构文档。 |
| fail-closed,绝不 panic | 每条失败路径都返回稳定的 LICENSE_* 错误码,非法输入不会 panic。 |
| golden 信封向量不含私钥 | 测试向量仅嵌入公钥、规范化 payload 与签名。见架构文档。 |
| 包 / 命令 | 职责 |
|---|---|
pkg/license | 客户端验证(公钥、签名校验、校验编排、fail-closed),绝不含私钥。 |
pkg/fingerprint | 跨平台设备指纹(Linux/macOS/Windows + 回退)。 |
internal/issuer | 签发端私钥逻辑(keygen、签名、签发、撤销列表),通过 Go internal/ 隔离。 |
cmd/license-tool | 签发端 CLI:keygen、public-key、issue、verify、inspect、fingerprint、revoke-list、version。 |
examples/ | 客户端集成与批量签发配置示例,见 examples/README.md。 |
# 签发端:把密钥对生成到 gitignored 目录(私钥留在本地机器)。
# ./_keys 已 gitignore;私钥绝不可提交。
go run ./cmd/license-tool keygen -key-id k1 -out-dir ./_keys
# 签发授权
go run ./cmd/license-tool issue -config examples/issue-config.json \
-key ./_keys/k1-private.key -out customer.lic
# 客户端:验证 + 策略校验
go run ./cmd/license-tool verify -license customer.lic -pubkey ./_keys/k1-public.key \
-product acme-app -version 1.4.0
客户端只内置公钥,绝不接触私钥。务必根据稳定错误码分支处理并提供恢复路径——不要 忽略错误。
ring := license.NewKeyRing()
_ = ring.AddPublicKeyBase64("k1", embeddedPublicKeyB64)
mgr := license.NewManager(ring)
res, err := mgr.LoadAndValidate("customer.lic", license.ValidationContext{
ProductID: "acme-app",
ProductVersion: "1.4.0",
})
if err != nil {
switch license.CodeOf(err) {
case license.CodeExpired:
// 引导用户续期,并展示到期日
case license.CodeDeviceMismatch:
// 展示设备申请码,请用户重新绑定
case license.CodeClockRollback:
// 提示系统时钟异常,禁用与时间相关的功能
default:
// 判为无效;引导重新导入许可文件
}
return
}
if err := res.RequireFeature("api"); err != nil {
// license.CodeOf(err) == license.CodeFeatureDenied
}
if err := res.CheckLimit("max_seats", seatsInUse); err != nil {
// license.CodeOf(err) == license.CodeLimitExceeded
}
完整库指南见 docs/zhCN/README.md,示例见
examples/client/main.go。
internal/issuer + cmd/license-tool):持有私钥,运行
keygen/issue/revoke-list,仅在受信任的签发机器上运行。私钥以 0600 权限
写入,且不加 -force 不会覆盖。pkg/license):只内置公钥,负责验证与门禁。借助 Go 的 internal/
机制,客户端无法 import internal/issuer,私钥逻辑绝不链接进客户端二进制。| 业务需求 | 机制 | 失败时 |
|---|---|---|
| “这份授权是否真实、未被修改?” | 对规范化 payload 的 Ed25519 签名 | LICENSE_SIGNATURE_INVALID / LICENSE_MALFORMED |
| “该客户的订阅是否已到期?” | license_type 时间语义 + expires_at + 宽限期 | LICENSE_EXPIRED / 状态 grace |
| “这个版本/功能是否被允许?” | edition 默认功能与 features 取并集;RequireFeature | LICENSE_FEATURE_DENIED |
| “是否在席位/用量额度内?” | 带范围校验的 limits;CheckLimit | LICENSE_LIMIT_EXCEEDED |
| “是否运行在被授权的设备上?” | 设备绑定(none/single/multi)+ 指纹 | LICENSE_DEVICE_MISMATCH |
| “这个构建是否在覆盖的版本范围内?” | 带维护/覆盖上限的 version_constraint(fail-closed) | LICENSE_VERSION_UNSUPPORTED |
| “这份具体授权是否已被撤销?” | 签名的离线撤销列表 | LICENSE_REVOKED |
| “有人把时钟回拨以规避到期吗?” | 完整性保护的本地回拨状态(仅朴素回拨) | LICENSE_CLOCK_ROLLBACK |
subtle.ConstantTimeCompare)。internal/issuer 与 CLI。limits 范围校验、拒绝未知枚举、许可文件大小上限、原子写入。license_type 时间语义:trial/subscription 必须带 expires_at;
lifetime 不得带且永不过期。固有边界: 二进制修补 / 逆向工程、超出回拨启发式的特权时钟篡改、指纹漂移,以及
离线撤销新鲜度(客户端只知道它手中列表里的撤销)。详见 SECURITY.md
与 docs/zhCN/architecture.md。
CI 在 Linux、macOS 和 Windows 上运行测试,在 Linux 上执行竞态检测,并对授权信封解析 执行短时 fuzz。覆盖率与性能数据均由所标注提交实测生成,详见《质量说明》与《性能基准》。
e5c6e93 上,go test ./... -covermode=atomic 的总语句覆盖率为
77.5%(pkg/license 82.9%、pkg/fingerprint 90.7%、internal/issuer
85.4%、cmd/license-tool 70.7%)。CI 门禁强制 >= 77%。顶部 Coverage 徽章由 CI
基于同一次运行生成。详见 docs/zhCN/quality.md。go1.26.6 环境中,典型授权样本的纯内存完整
校验约为 33904 ns/op、6072 B/op、41 allocs/op;纯内存验签约 34552 ns/op;
信封解析约 2738 ns/op(-count=5 中位数)。该结果用于说明测试环境中的实现成本,
不承诺所有设备获得相同数值。完整方法与各路径结果(含环境与 commit SHA)见
docs/zhCN/performance.md。验证在不同路径上有各异的开销与副作用:
Verifier.Verify,无磁盘 I/O、无策略校验。Manager.LoadAndValidate 读取许可文件,并可能
读写防回拨状态文件(涉及磁盘 I/O)。pkg/fingerprint 读取平台硬件标识;开销与可用性取决于宿主
操作系统。license-tool 是签发端 CLI(含私钥逻辑,仅供授权签发方使用)。可按需选择安装方式。
下载发布二进制 —— 从发布页下载对应
OS/架构的压缩包,解压后运行 license-tool。
Homebrew(macOS / Linux):
brew tap soulteary/tap
brew install soulteary/tap/grantseal
安装后即可全局使用 license-tool 命令。
Docker:
docker pull soulteary/grantseal:latest
完整 Docker 用法(签发端 keygen/issue 与客户端 verify,含私钥安全提示)见
docs/zhCN/README.md。
docs/zhCN/README.mddocs/zhCN/architecture.mddocs/zhCN/quality.mddocs/zhCN/performance.mdSECURITY.mdexamples/README.mdTrustedTimeProvider 获取权威时间。Content type
Image
Digest
sha256:631ec2c63…
Size
2 MB
Last updated
25 days ago
docker pull soulteary/grantseal