92 lines
3.9 KiB
Markdown
92 lines
3.9 KiB
Markdown
# chongming-license
|
||
|
||
机器码授权共享包:硬件指纹采集、license 载荷格式、Ed25519 验签与插件白名单判定。
|
||
网关与厂商侧签发工具共用同一份实现 —— 尤其是**规范化 JSON 只有一处实现**,
|
||
避免"自己签的自己验不过"。
|
||
|
||
## 设计要点
|
||
|
||
- **纯离线**:设备用内置公钥验签,运行期零联网。与现有 air-gapped 部署形态
|
||
(Zenoh localhost、Caddy `tls internal`、无 phone-home)一致。
|
||
- **非对称**:Ed25519。注意根仓 `resources/*.key` 那套 `.mbank` 验签是
|
||
**HMAC 对称**且密钥与银行同目录发布,只能算防篡改/混淆,**不能**作为厂商
|
||
信任根 —— 授权必须用非对称,公钥可安全内置。
|
||
- **N-of-M 容错**:license 里存各来源的原始指纹,校验按"命中数达阈值"判定,
|
||
而非把所有来源哈希成一个值 —— 否则换一块网卡就直接失效。
|
||
展示给客户的仍是单个 `machine_code`。
|
||
- **weak 来源**:`/etc/machine-id` 在同镜像批量刷机场景下**所有机器相同**,
|
||
故标记 `weak` 且**不参与判定阈值**(只作诊断参考),避免它给攻击者白送一票。
|
||
|
||
## 采集来源
|
||
|
||
| 来源 | 位置 | 说明 |
|
||
| --- | --- | --- |
|
||
| `board_serial` | `/proc/device-tree/serial-number`、DMI `board_serial` | 逐机唯一,最稳定 |
|
||
| `emmc_cid` | `/sys/block/mmcblk*/device/cid` | 芯片出厂唯一,不随系统重装变化 |
|
||
| `cpu_serial` | `/proc/cpuinfo` 的 `Serial` | ARM 常见;部分型号全 0(会被剔除) |
|
||
| `mac` | `/sys/class/net/*/address` | 取字典序最小者:插入/移除其它网卡不改变它 |
|
||
| `product_uuid` | `/sys/class/dmi/id/product_uuid` | x86 有效,ARM 通常缺失 |
|
||
| `machine_id` | `/etc/machine-id` | ⚠ weak,见上 |
|
||
|
||
## 文件格式
|
||
|
||
```json
|
||
{
|
||
"payload": {
|
||
"v": 1,
|
||
"lic_id": "L2026-0001",
|
||
"customer": "某某科技",
|
||
"machine_code": "A3F9-1B77-9E04-2C8D-6F31-0A55",
|
||
"sources": {"board_serial": "...", "emmc_cid": "...", "mac": "..."},
|
||
"plugins": ["aibox_sophon_adapter", "ip_sound_column"],
|
||
"not_before": "2026-09-01",
|
||
"expires_at": "2027-09-01",
|
||
"issued_at": "2026-09-01T10:00:00Z",
|
||
"note": ""
|
||
},
|
||
"alg": "ed25519",
|
||
"kid": "vendor-2026",
|
||
"sig": "<base64,覆盖 payload 的规范化 JSON>"
|
||
}
|
||
```
|
||
|
||
签名覆盖 payload **原始字节的规范化形式**(`canonical_json`:键排序、无空白),
|
||
因此载荷内不允许浮点数(float 文本表示存在多种合法写法)。校验顺序是
|
||
先验签、后解析模型 —— 反过来会让默认值填充/字段顺序改变字节导致验签失败。
|
||
|
||
## 用法
|
||
|
||
网关侧:
|
||
|
||
```python
|
||
from chongming_license import LicenseManager
|
||
|
||
mgr = LicenseManager(
|
||
license_path="license.lic",
|
||
public_key=config["license"]["public_key"],
|
||
clock_store=cache, # chongming_cache.Cache,存时间高水位
|
||
enforce=is_packaged(), # 打包产物强制校验;开发/测试放行
|
||
)
|
||
decision = mgr.check_plugin("aibox_sophon_adapter")
|
||
if not decision.allowed:
|
||
... # 不加载该插件
|
||
```
|
||
|
||
厂商侧(`chongming-cli`,私钥只在这边):
|
||
|
||
```bash
|
||
uv run chongming-license keygen -o license_private.pem # 一次性生成密钥对
|
||
uv run chongming-license issue --machine-code <码> --sources-json <采集导出> \
|
||
--plugins aibox_sophon_adapter,ip_sound_column \
|
||
--customer 某某科技 --expires 2027-09-01 -k license_private.pem -o license.lic
|
||
```
|
||
|
||
## 诚实的边界
|
||
|
||
离线 + root 的天花板是**阻止随手拷贝**,不是阻止逆向:有 root 者可改二进制
|
||
跳过校验,或 `LD_PRELOAD` 伪造指纹读取。`clock` 模块的时间回拨检测同理 ——
|
||
删掉缓存文件即失效,它只是抬高随手改时间的门槛。
|
||
|
||
真正难以绕过的是**签名**:伪造 license 需要厂商私钥,而公钥可明文放置
|
||
(换公钥只会验签失败,无法据以伪造)。
|