licverify

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 20 Imported by: 0

README

licverify

OpenBKN 产品侧 License 验证 SDK。纯标准库、零第三方依赖,完全离线工作—— 验签、有效期判定、机器码、激活绑定自校验都在本地完成,不依赖签发服务在线。

go get github.com/openbkn-ai/licverify

用法

import (
    "github.com/openbkn-ai/licverify"
    "github.com/openbkn-ai/licverify/keys"   // 官方验签公钥表(单一来源)
)

// 门控判定:有效 / 宽限(功能保持)/ 回落社区(曾授权)/ 无效
state, p := licverify.Eval(licenseText, keys.Official())

// 无证时(全新安装):试用窗口内静默,之后转为常驻激活提示。
// 两者都跑社区能力集——从不因未激活而收走功能。
state = licverify.TrialAt(firstRunUnix, time.Now())   // StateTrial / StateUnlicensed

// 机器码 + 激活绑定自校验:复制来的 license 离线即拒
localFP, _ := licverify.Fingerprint()
if state != licverify.StateInvalid && licverify.VerifyBound(p, localFP) != nil {
    state = licverify.StateInvalid
}

switch state {
case licverify.StateValid, licverify.StateGrace:
    if p.HasFeature("rbac_basic") { /* 开启能力 */ }
    if n := p.Limit("max_users"); n != -1 && userCount > n { /* 拒绝新增 */ }
case licverify.StateFallback:
    // 商业授权过期但签名可验 = 曾授权,自动降级社区能力集,数据保留
case licverify.StateInvalid:
    // 未激活:只开放激活引导与数据读取/导出
}

License 格式

v1.<base64url(payload)>.<base64url(Ed25519 签名)>

签名覆盖传输的原始 payload 字节,验签不重新序列化(无 JSON 规范化问题)。 payload 携带 edition / features / limits / expires_at / contract_expires_at0 = 永不过期)/ hw_fingerprint(激活后绑定)。

机器码(实例指纹)

fp = "fp_" + hex( SHA-256( salt + identity )[:8] )

identity 按优先级:OPENBKN_INSTANCE_ID 环境变量 → machine-id(Linux/macOS/Windows)→ 物理网卡 MAC。算法公开,不靠保密——防复制的强制力在签发服务端(一码一激活 first-wins + 续期校验绑定)。

部署形态 做法 指纹标识什么
裸机 / 虚机 默认即可(machine-id) 这台主机
单机 Docker 挂载 -v /etc/machine-id:/etc/machine-id:ro 这台主机(与主机一致)
K8s OPENBKN_INSTANCE_ID = 集群稳定标识 这套集群

离线激活:把 Fingerprint() 的设备指纹(fp_…)粘贴到客户门户 → 兑换绑定指纹的激活证书(新 .lic)。

安全模型

本库公开无损安全:验签用公钥,没有签发私钥就伪造不出 License(私钥只存在于 签发服务,不在本仓库);指纹算法与盐不是秘密——防复制的强制力在签发服务端 (一次性激活 first-wins + 续期校验绑定),伪造指纹骗不过服务端记录。 客户端反篡改明确不是目标:验证代码运行在使用方环境,技术上不设防, 授权约束力在合同与审计。

payload 字段与状态语义是签发方与产品方的长期契约:v0.x 期间字段可能调整, 冻结后升 v1 并按 semver 演进。

API 一览

函数 作用
Verify / VerifyAt 验签 + 有效期检查,返回 payload
Parse 只验签不查有效期(判定"曾授权"用)
Eval 持证判定(valid / grace / fallback_community / invalid)
TrialAt / TrialRemaining 无证判定(trial / unlicensed)与剩余试用时长
Fingerprint / FingerprintFrom 本机 / 自定义身份的实例指纹
VerifyBound license 绑定指纹 = 本机指纹自校验
ParsePublicKey 解析签发方发布的 base64 公钥

Documentation

Overview

Package licverify is the product-side license verification library. It has no dependency on the license server; products embed the issuer's public keys and verify licenses fully offline.

Index

Constants

View Source
const EnvInstanceID = "OPENBKN_INSTANCE_ID"

EnvInstanceID overrides all hardware sources when set.

View Source
const GracePeriod = 30 * 24 * time.Hour

GracePeriod is how long after expiry the product keeps features enabled (and the server still accepts renewal). Shared constant so offline product behavior and server renewal policy agree.

View Source
const TrialPeriod = 30 * 24 * time.Hour

TrialPeriod is how long a fresh, never-licensed deployment runs before the activation prompt turns persistent. Nothing is disabled when it elapses.

Variables

View Source
var (
	ErrNoFingerprint       = errors.New("licverify: no stable instance identity found; set " + EnvInstanceID)
	ErrFingerprintMismatch = errors.New("licverify: license is bound to a different instance")
)
View Source
var (
	ErrMalformed  = errors.New("licverify: malformed license")
	ErrUnknownKey = errors.New("licverify: unknown signing key")
	ErrBadSig     = errors.New("licverify: signature verification failed")
	ErrExpired    = errors.New("licverify: license expired")
	ErrNotYet     = errors.New("licverify: license not yet valid")
)

Functions

func Eval

func Eval(text string, keys map[string]ed25519.PublicKey) (State, *Payload)

Eval judges a license file for gating decisions: valid / grace / fallback-to-community / invalid. Unlike Verify it never returns an error for expiry — expiry is a state, not a failure.

func EvalAt

func EvalAt(text string, keys map[string]ed25519.PublicKey, now time.Time) (State, *Payload)

EvalAt is Eval with an explicit clock.

func Fingerprint

func Fingerprint() (string, error)

Fingerprint computes this machine's instance fingerprint (fp_ + 16 hex). Deterministic per machine; different across machines and products with a different salt version.

func FingerprintFrom

func FingerprintFrom(identity string) string

FingerprintFrom derives a fingerprint from a caller-supplied stable identity (e.g. a cluster UID a product resolved itself).

func ParsePublicKey

func ParsePublicKey(b64 string) (ed25519.PublicKey, error)

ParsePublicKey decodes a base64 (std, raw or padded) Ed25519 public key, as served by the issuer's /api/keys endpoint.

func TrialRemaining added in v0.4.0

func TrialRemaining(firstRun int64, now time.Time) time.Duration

TrialRemaining is how much of the trial window is left (0 once elapsed).

func VerifyBound

func VerifyBound(p *Payload, localFP string) error

VerifyBound checks an activated license against the local fingerprint. Licenses not yet bound (empty hw_fingerprint) pass — binding happens at activation. Returns ErrFingerprintMismatch for a copied license.

Types

type Customer

type Customer struct {
	Name  string `json:"name"`
	Email string `json:"email"`
	// Project identifies the licensed project — licensing granularity is
	// per project (one project, one license).
	Project string `json:"project,omitempty"`
}

type Guard

type Guard struct {
	// contains filtered or unexported fields
}

func NewGuard

func NewGuard(cfg GuardConfig) (*Guard, error)

NewGuard validates the config, fills defaults, and performs the first evaluation synchronously so State() is meaningful immediately.

func (*Guard) Refresh

func (g *Guard) Refresh() Snapshot

Refresh re-evaluates the license locally WITHOUT any network renewal, and returns the fresh snapshot. Safe to call from the constructor and right after importing a new .lic — it never blocks on the license server.

func (*Guard) RenewNow

func (g *Guard) RenewNow() Snapshot

RenewNow forces one renewing evaluation immediately (the same work a Run tick does), blocking on the network if a renewal is due. Products can call it to renew on demand; Run calls it on its schedule. Unlike Refresh, this may block — do not call it from a latency-sensitive path.

func (*Guard) Run

func (g *Guard) Run(ctx context.Context)

Run drives the periodic loop until ctx is cancelled. Call as a goroutine. Renewal (a network call) happens only here, never in NewGuard/Refresh.

func (*Guard) State

func (g *Guard) State() Snapshot

State returns the current snapshot (atomic read, safe for hot paths).

type GuardConfig

type GuardConfig struct {
	// Keys are the embedded verification public keys (kid → key). Required.
	Keys map[string]ed25519.PublicKey
	// Load returns the current license text (from file, DB, …). Required.
	Load func() (string, error)
	// Store persists a replacement license after a successful renewal.
	// Required when RenewURL is set.
	Store func(text string) error
	// RenewURL is the license server's renew endpoint
	// (…/api/licenses/renew). Empty disables auto-renew (pure offline
	// deployments carry a full-contract certificate instead).
	RenewURL string
	// InstanceFP defaults to Fingerprint() of this machine.
	InstanceFP string
	// FirstRun returns the unix timestamp this deployment first started,
	// persisted by the product (create it on first call). It only drives the
	// trial-vs-unlicensed distinction while NO license is held — nothing is
	// gated on it. Nil (or 0) means "starting now", i.e. always StateTrial.
	FirstRun func() int64
	// Interval between re-evaluations. Default 1h; ±10% jitter is applied.
	Interval time.Duration
	// OnChange fires on every state transition (not on steady state).
	OnChange func(old, cur Snapshot)
	// Logf receives operational messages (renew attempts/failures). Optional.
	Logf func(format string, args ...any)
	// HTTPClient defaults to a 30s-timeout client.
	HTTPClient *http.Client
}

Guard is the in-process license watchdog for products: it re-evaluates the license periodically (Eval + VerifyBound), auto-renews online deployments ahead of expiry, and reports state transitions. It is deliberately NOT a separate OS daemon — enforcement must live in the product process anyway, so a killable side process would add operational surface without any security gain (client-side anti-tamper is a non-goal).

Usage:

guard, _ := licverify.NewGuard(licverify.GuardConfig{
    Keys: keys,
    Load: func() (string, error) { return os.ReadFileString(licPath) },
    Store: func(t string) error { return os.WriteFile(licPath, []byte(t), 0o600) },
    RenewURL: "https://license.openbkn.ai/api/licenses/renew",
    OnChange: func(old, new licverify.Snapshot) { gate.Apply(new) },
})
go guard.Run(ctx)
...
if guard.State().State == licverify.StateValid { ... }

type Payload

type Payload struct {
	LicID    string   `json:"lic_id"`
	Kid      string   `json:"kid"`
	Edition  string   `json:"edition"`
	Customer Customer `json:"customer"`
	IssuedAt int64    `json:"issued_at"`
	// ExpiresAt is the technical validity of this single license (the only
	// time the product enforces). 0 means never expires (community).
	ExpiresAt int64 `json:"expires_at"`
	// ContractExpiresAt is the contract end date, shown as "licensed until".
	// Renewal never extends past it. 0 means perpetual.
	ContractExpiresAt int64            `json:"contract_expires_at"`
	Features          []string         `json:"features"`
	Limits            map[string]int64 `json:"limits"`
	// HWFingerprint is the instance fingerprint bound at activation; empty
	// until the license has been activated and reissued.
	HWFingerprint string `json:"hw_fingerprint,omitempty"`
}

Payload is the signed content of a license.

func Parse

func Parse(text string, keys map[string]ed25519.PublicKey) (*Payload, error)

Parse verifies the signature only (no expiry check) and returns the payload. The signature is verified over the exact transmitted payload bytes, so no JSON canonicalization is involved.

func Verify

func Verify(text string, keys map[string]ed25519.PublicKey) (*Payload, error)

Verify checks the license text against the given public keys and the current time. Returns the payload on success.

func VerifyAt

func VerifyAt(text string, keys map[string]ed25519.PublicKey, now time.Time) (*Payload, error)

VerifyAt is Verify with an explicit clock, for testing and for callers that use a trusted time source.

func (*Payload) HasFeature

func (p *Payload) HasFeature(name string) bool

HasFeature reports whether the license enables the named feature.

func (*Payload) Limit

func (p *Payload) Limit(name string) int64

Limit returns the limit value for name. Missing means 0 (not allowed); -1 means unlimited.

type Snapshot

type Snapshot struct {
	State   State
	Payload *Payload // nil when State is invalid
	// Err explains an invalid/unusable license (gate features off).
	Err error
	// RenewErr reports that a background auto-renew failed while the license
	// itself is still fine — do NOT gate features off on this; surface it as a
	// "renewal pending" warning. Kept separate from Err so `if snap.Err != nil`
	// does not disable a perfectly valid license over a transient network blip.
	RenewErr error
}

Snapshot is the point-in-time judgement the product gates on.

type State

type State string

State is the product-side judgement of a license file.

const (
	// StateValid: signature good, within validity — full feature set.
	StateValid State = "valid"
	// StateGrace: expired less than GracePeriod ago — features stay enabled,
	// product should prompt for renewal.
	StateGrace State = "grace"
	// StateFallback: a commercial license expired beyond grace but its
	// signature still verifies. That is proof the instance was once formally
	// licensed: the product automatically falls back to the community
	// feature set, keeping data readable and exportable.
	StateFallback State = "fallback_community"
	// StateInvalid: malformed, bad signature, unknown key, or not yet valid.
	// The product still runs on the community feature set — a bad file is no
	// reason to withhold what is free anyway — but should say the license file
	// could not be verified rather than nag for activation.
	StateInvalid State = "invalid"
	// StateTrial: no license imported yet, within TrialPeriod of first run.
	// Community feature set, quietly; the product may show days remaining.
	StateTrial State = "trial"
	// StateUnlicensed: no license, trial window elapsed. Still the community
	// feature set — nothing is ever withheld — with a persistent prompt to
	// activate.
	StateUnlicensed State = "unlicensed"
)

func TrialAt added in v0.4.0

func TrialAt(firstRun int64, now time.Time) State

TrialAt judges a deployment that holds no license, from the first-run timestamp the product persisted (unix seconds; <= 0 means "starting now"). Trial state is deliberately local and resettable: it exists to give new installs a quiet grace before nagging, not to gate anything. Nothing about the feature set depends on it, so there is nothing to defend.

Directories

Path Synopsis
Package keys is the single source of truth for OpenBKN's official license verification public keys.
Package keys is the single source of truth for OpenBKN's official license verification public keys.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL