Metadata-Version: 2.4
Name: transaierp-model-license
Version: 1.0.2
Summary: Offline Ed25519 verification for TransAIERP model licenses
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: cryptography>=41

# TransAI Model License SDK for Python

离线验证 TransAIERP 签发的 `.lic` 授权文件，包括 Ed25519 签名、有效期、
资源标识、模型版本标识以及 `.AIT` 文件的 SHA-256 摘要。

## 安装

需要 Python 3.10+。安装时会自动安装 `cryptography>=41`。

```powershell
python -m pip install transaierp-model-license
```

安装包名为 `transaierp-model-license`，导入名为 `model_license`。
本 SDK 不负责购买、下载、解密或加载模型，不会联网查询订单。

## 接入前准备

| 配置或文件 | 来源和要求 |
| --- | --- |
| `.AIT` 文件 | 用户购买后下载的原始模型包，不能先解压、转换或重新打包再计算摘要 |
| `.lic` 文件 | ERP 为对应资源和授权期限签发的文件 |
| Ed25519 公钥 | 由运营方通过可信渠道提供，与 ERP 签名私钥配对 |
| `resource_id` | 应用本次需要加载的资源 ID |
| `package_id` | 应用本次需要加载的模型版本标识，必须与服务端签发值一致 |

公钥必须是 32 字节原始公钥，或该公钥的 **无 `=` 填充 Base64URL 字符串**。
PEM 文本、证书和十六进制字符串不能直接传入。不要向用户分发签名私钥。
不要允许授权文件或不可信下载地址自行指定“可信公钥”。

ERP 当前的 `package_id` 由版本记录 ID、连字符和版本字符串组成。
它不是文件名；应从自己的可信发布配置取得，不能从未经验证的 `.lic` 中抄取作为预期值。

## 最小接入示例

此示例不需要网络，但需要真实的授权文件、公钥和对应模型包。
示例中的环境变量由接入方设置，不是 SDK 自动读取的固定配置。

```python
import os
from pathlib import Path

from model_license import LicenseError, sha256, verify

# 大文件的分块哈希示例见下文。
model_bytes = Path(os.environ["MODEL_PACKAGE_PATH"]).read_bytes()
license_bytes = Path(os.environ["MODEL_LICENSE_PATH"]).read_bytes()
public_key = os.environ["MODEL_LICENSE_PUBLIC_KEY"].strip()

try:
    claims = verify(
        license_bytes,
        public_key,
        resource_id=int(os.environ["MODEL_RESOURCE_ID"]),
        package_id=os.environ["MODEL_PACKAGE_ID"],
        package_sha256=sha256(model_bytes),
    )
except LicenseError as exc:
    raise SystemExit(f"Model authorization failed: {exc}") from exc

print("Authorized resource:", claims.resource_id)
print("Valid until:", claims.valid_until or "perpetual")
# 仅在此处之后，将上述已验证的 model_bytes 交给你自己的解密/模型加载器。
```

必须将 **实际模型文件的摘要** 传给 `package_sha256`。仅验证 `.lic` 签名不能确认
当前加载的是它所授权的模型。验证失败时，不得继续加载。

上述流程只在调用时检查有效期。若业务要求运行中的模型到期也停止使用，
接入程序需要在使用期间重新检查；SDK 不会启动定时器或自动终止模型。

## 接口参考

### `verify(data, public_key, *, now=None, resource_id=None, package_id=None, package_sha256=None) -> Claims`

| 参数 | 类型 / 默认值 | 作用 |
| --- | --- | --- |
| `data` | `bytes` 或 `str`，必填 | 整个 `.lic` 文件的 UTF-8 JSON 内容，不是文件路径 |
| `public_key` | `bytes` 或 `str`，必填 | 32 字节原始公钥，或其无填充 Base64URL 编码 |
| `now` | 带时区的 `datetime` / `None` | 默认当前 UTC 时间；覆盖值仅用于可控测试 |
| `resource_id` | `int` / `None` | 非空时要求资源 ID 相等 |
| `package_id` | `str` / `None` | 非空时要求模型版本标识相等 |
| `package_sha256` | `str` / `None` | 实际模型包的 64 位十六进制 SHA-256，接受大小写输入 |

三个匹配参数省略时，对应匹配检查会被跳过。生产接入建议全部提供。
时间区间是 `valid_from <= now < valid_until`；`valid_until=None` 表示永久授权，
但仍要满足 `valid_from`。到期瞬间即不再有效。

成功返回不可变的 `Claims`；签名错误、格式错误、公钥错误、时间不合法、
尚未生效、已过期和匹配失败均抛出 `LicenseError`，不会返回 `False`。

### `Claims`

由 `verify()` 返回的冻结 dataclass。使用属性访问，例如 `claims.resource_id`。
自行调用 `Claims(...)` 只构造数据，**不会验证授权**。

| 属性 | 类型 | 含义 |
| --- | --- | --- |
| `schema_version` | `int` | 授权声明版本，当前为 `1` |
| `license_id` | `str` | 授权文件标识 |
| `resource_id` | `int` | 资源 ID |
| `package_id` | `str` | 模型版本标识 |
| `package_sha256` | `str` | 模型包的 SHA-256，小写十六进制 |
| `issued_at` | `str` | 签发时间，UTC `Z` 格式 |
| `valid_from` | `str` | 生效时间，UTC `Z` 格式 |
| `valid_until` | `str` 或 `None` | 到期时间，UTC `Z` 格式；`None` 表示永久 |

时间字段保留为字符串，并非 Python `datetime` 对象。

### `sha256(package: bytes) -> str`

计算字节内容的 SHA-256，返回 64 位小写十六进制字符串。
不读取文件、不校验签名，也不判断是否获得授权。

大模型不宜一次读入内存，可以用标准库分块计算，结果再传给 `verify()`：

```python
import hashlib
import os
from pathlib import Path

from model_license import verify

digest = hashlib.sha256()
with open(os.environ["MODEL_PACKAGE_PATH"], "rb") as source:
    for chunk in iter(lambda: source.read(1024 * 1024), b""):
        digest.update(chunk)

claims = verify(
    Path(os.environ["MODEL_LICENSE_PATH"]).read_bytes(),
    os.environ["MODEL_LICENSE_PUBLIC_KEY"].strip(),
    resource_id=int(os.environ["MODEL_RESOURCE_ID"]),
    package_id=os.environ["MODEL_PACKAGE_ID"],
    package_sha256=digest.hexdigest(),
)
```

如果哈希后再按路径加载模型，接入程序必须防止文件在哈希和加载之间被替换。

### `LicenseError`

`ValueError` 的子类。统一捕获它来拒绝无效授权，不要依赖具体错误文字作为稳定错误码。
读取文件产生的 `OSError`、环境变量缺失等调用方错误需要应用单独处理。

## 常见问题

- `invalid license`：检查文件完整性、公钥是否配对、编码是否正确，以及依赖是否安装。
- `license expired or not active`：检查授权期限和系统时间，生产环境不要修改 `now` 绕过校验。
- `resource mismatch` / `package mismatch`：确认 `.AIT`、`.lic` 和发布配置对应同一资源版本。
- `package digest mismatch`：模型字节不一致，重新下载对应版本，不要跳过摘要检查。
- SDK 依赖本机时间，不提供在线撤销、退款、设备绑定或抗客户端篡改能力。

## 安装后离线查看文档

README 正文随发布包保存在安装元数据中，无需额外下载文档文件：

```powershell
python -m model_license
python -c "from importlib.metadata import metadata; print(metadata('transaierp-model-license')['Description'])"
python -m pydoc model_license
```

也可以在 Python 或 IDE 中使用 `help(verify)`、`help(Claims)`。

## 开发检查

在源码仓库的 `model-license-sdk/python` 目录执行：

```powershell
python -m pip install -e .
python -m unittest discover -s tests -v
```

测试依赖仓库的 `../testdata/verification.json`；安装后调用 SDK 不需要此文件。
