Metadata-Version: 2.5
Name: chongming-license
Version: 0.1.0
Summary: chongming 机器码授权：硬件指纹采集、license 载荷与 Ed25519 验签、插件白名单判定（网关与签发工具共用）
Requires-Python: >=3.12
Requires-Dist: cryptography
Requires-Dist: orjson
Requires-Dist: pydantic
Description-Content-Type: text/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 需要厂商私钥，而公钥可明文放置
（换公钥只会验签失败，无法据以伪造）。
