Sign inSign up

soulteary/grantseal

By soulteary

Updated 25 days ago

GrantSeal signs software rights, not secrets.

Image
0

2.6K

soulteary/grantseal repository overview

grantseal

CI Release Go Report Card Coverage License Go Version

English | 简体中文 — full docs: English | 中文文档Changelog


English

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.

Is grantseal for you?

A good fit when you:

  • Ship software that must run offline or air-gapped and still gate features, seats/limits, editions, expiry, or device binding.
  • Want licenses that are cryptographically signed (Ed25519) so a customer cannot mint or edit their own.
  • Prefer a zero-dependency Go library you can embed and audit, with stable, machine-readable error codes for your UX.

Not a fit when you:

  • Need to guarantee software cannot be cracked. A determined attacker can patch the client binary to skip verification — inherent to all offline licensing.
  • Want a hosted license server, per-request online activation, or metering. This library is offline-first; online activation is on the Roadmap.
  • Rely on the system clock being trustworthy against a root/admin adversary. Rollback detection catches naive backward jumps, not a privileged attacker.
Evidence, not slogans
ClaimHow you can verify it
Zero third-party dependenciesgo.mod has no require block for external modules; build with go build ./....
Ed25519-only, no algorithm downgradepkg/license/verifier.go rejects any non-Ed25519 algorithm; see error LICENSE_UNSUPPORTED_ALGORITHM.
Private keys never link into a clientClients import pkg/license; signing lives under internal/issuer (unimportable). CI scans release artifacts for key material.
Signature covers the whole payloadDeterministic canonical JSON is signed verbatim; any edit yields LICENSE_SIGNATURE_INVALID. See architecture.
Fail-closed, never panicsThe verifier returns a stable LICENSE_* code on every failure path and does not panic on malformed input.
Golden envelope vectors carry no private keyTest vectors embed only public keys, canonical payloads, and signatures. See architecture.
Components
Package / CommandRole
pkg/licenseClient-side verification only. Public keys, signature checking, validation orchestration, fail-closed. Never contains private keys.
pkg/fingerprintCross-platform device fingerprint (Linux/macOS/Windows + fallback).
internal/issuerIssuer-side private-key logic (keygen, signing, issuing, revocation lists). Isolated via Go internal/.
cmd/license-toolIssuer CLI: keygen, public-key, issue, verify, inspect, fingerprint, revoke-list, version.
examples/Client integration & batch-issue config examples. See examples/README.md.
Quick start
# 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
Minimal client integration

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.

Issuer vs. client boundary
  • Issuer side (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.
  • Client side (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.
What business questions it answers
Business needMechanismOn failure
"Is this license genuine and unmodified?"Ed25519 signature over the canonical payloadLICENSE_SIGNATURE_INVALID / LICENSE_MALFORMED
"Has this customer's subscription expired?"license_type time semantics + expires_at + grace periodLICENSE_EXPIRED / status grace
"Is this edition/feature allowed?"Edition defaults unioned with features; RequireFeatureLICENSE_FEATURE_DENIED
"Are they within seat/usage limits?"Range-validated limits; CheckLimitLICENSE_LIMIT_EXCEEDED
"Is this running on a licensed device?"Device binding (none/single/multi) + fingerprintLICENSE_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 listLICENSE_REVOKED
"Did someone roll the clock back to dodge expiry?"Integrity-protected local rollback state (naive rollback only)LICENSE_CLOCK_ROLLBACK
Security model (summary)
  • Ed25519 signatures only. PKCS#1v1.5, MD5, SHA-1, ECB and home-grown crypto are forbidden.
  • Signature covers the complete canonical payload (deterministic sorted-key JSON): it provides origin authentication and integrity, not confidentiality — payloads are readable.
  • Constant-time comparison (subtle.ConstantTimeCompare) for sensitive comparisons.
  • Private key never appears in client code, binaries, git, logs, or test fixtures; signing is confined to 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.
  • Validation results are read-only; the verifier is fail-closed and never panics.

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.

Testing, coverage & performance

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.

  • Coverage: on commit 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.
  • Performance: on Apple M5 / darwin/arm64 / 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:

  1. In-memory signature verify onlyVerifier.Verify over an in-memory envelope. No disk I/O, no policy checks.
  2. Full policy validation — signature verify plus enum/time/device/version policy checks against in-memory input.
  3. File load + rollback state persistenceManager.LoadAndValidate reads the license file and may read/write the anti-rollback state file (disk I/O).
  4. Device fingerprint collectionpkg/fingerprint reads platform hardware identifiers; cost and availability depend on the host OS.
Install

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.

Documentation
Roadmap
  • Inject a network-backed TrustedTimeProvider for authoritative time.
  • Fetch signed revocation lists / public-key updates over a signed OTA channel.
  • A device re-binding endpoint so users can move licenses between machines.

简体中文

grantseal 是使用 Go 1.26 纯标准库crypto/ed25519crypto/randcrypto/sha256crypto/hmaccrypto/subtleencoding/json)实现的离线软件 授权库与 CLI,不引入任何第三方依赖。它在签发端生成 Ed25519 签名的授权文件,在 客户端以 fail-closed、绝不 panic 的验证器进行校验。

它的目标是提高伪造与离线篡改的成本,而非让软件不可破解。诚实的威胁模型与边界 见 SECURITY.md

是否适合你?

适合的场景:

  • 软件需要离线/隔离网络运行,同时仍要对功能、席位/额度、版本(edition)、 到期、设备绑定做门禁。
  • 希望授权文件经 Ed25519 密码学签名,客户无法自行伪造或修改。
  • 偏好零依赖、可嵌入且可审计的 Go 库,并提供稳定、机器可读的错误码用于 UX。

不适合的场景:

  • 需要保证软件不被破解。有决心的攻击者可以修改客户端二进制以跳过校验——这是所有 离线授权固有的局限。
  • 需要托管的授权服务器、逐次在线激活或计量。本库以离线为先;在线激活见 路线图
  • 依赖系统时钟对抗 root/admin 级攻击者。回拨检测只能捕获朴素的时间回拨,无法防住 特权攻击者。
证据而非口号
主张你如何验证
零第三方依赖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:keygenpublic-keyissueverifyinspectfingerprintrevoke-listversion
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 取并集;RequireFeatureLICENSE_FEATURE_DENIED
“是否在席位/用量额度内?”带范围校验的 limitsCheckLimitLICENSE_LIMIT_EXCEEDED
“是否运行在被授权的设备上?”设备绑定(none/single/multi)+ 指纹LICENSE_DEVICE_MISMATCH
“这个构建是否在覆盖的版本范围内?”带维护/覆盖上限的 version_constraint(fail-closed)LICENSE_VERSION_UNSUPPORTED
“这份具体授权是否已被撤销?”签名的离线撤销列表LICENSE_REVOKED
“有人把时钟回拨以规避到期吗?”完整性保护的本地回拨状态(仅朴素回拨)LICENSE_CLOCK_ROLLBACK
安全模型(摘要)
  • 仅使用 Ed25519 签名。禁止 PKCS#1v1.5、MD5、SHA-1、ECB 及自制算法。
  • 签名覆盖完整规范化 payload(键排序的确定性 JSON):提供来源认证与完整性, 而非保密性——payload 可被读取。
  • 敏感比较使用常量时间比较(subtle.ConstantTimeCompare)。
  • 私钥绝不出现在客户端代码、二进制、git、日志或测试固定数据中;签名仅存在于 internal/issuer 与 CLI。
  • limits 范围校验、拒绝未知枚举、许可文件大小上限、原子写入。
  • 强制 license_type 时间语义trial/subscription 必须带 expires_atlifetime 不得带且永不过期。
  • 验证结果只读;验证器 fail-closed绝不 panic

固有边界: 二进制修补 / 逆向工程、超出回拨启发式的特权时钟篡改、指纹漂移,以及 离线撤销新鲜度(客户端只知道它手中列表里的撤销)。详见 SECURITY.mddocs/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
  • 性能: 在 Apple M5 / darwin/arm64 / go1.26.6 环境中,典型授权样本的纯内存完整 校验约为 33904 ns/op6072 B/op41 allocs/op;纯内存验签约 34552 ns/op; 信封解析约 2738 ns/op-count=5 中位数)。该结果用于说明测试环境中的实现成本, 不承诺所有设备获得相同数值。完整方法与各路径结果(含环境与 commit SHA)见 docs/zhCN/performance.md

验证在不同路径上有各异的开销与副作用:

  1. 纯内存验签 —— 对内存中的信封调用 Verifier.Verify,无磁盘 I/O、无策略校验。
  2. 完整策略校验 —— 验签之外再对内存输入做枚举/时间/设备/版本策略校验。
  3. 文件读取 + 回拨状态落盘 —— Manager.LoadAndValidate 读取许可文件,并可能 读写防回拨状态文件(涉及磁盘 I/O)。
  4. 设备指纹采集 —— 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

文档导航
路线图
  • 注入基于网络的 TrustedTimeProvider 获取权威时间。
  • 通过签名 OTA 通道下发撤销列表 / 公钥更新。
  • 增加设备解绑/重绑接口,便于用户迁移设备。

Tag summary

Content type

Image

Digest

sha256:631ec2c63

Size

2 MB

Last updated

25 days ago

docker pull soulteary/grantseal