modify: 授权

This commit is contained in:
2026-09-21 14:03:47 +08:00
parent 66e82c02e7
commit 8a3dc8036c
19 changed files with 2826 additions and 30 deletions
+358
View File
@@ -0,0 +1,358 @@
"""chongming-license 库层测试:指纹、机器码、验签、容错匹配、时钟防线。
运行方式:`python test/test_license_verify.py`,或 `uv run pytest test/test_license_verify.py`。
"""
import base64
import json
import sys
import tempfile
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from cryptography.hazmat.primitives import serialization # noqa: E402
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey # noqa: E402
from chongming_license import ( # noqa: E402
REASON_CLOCK_ROLLBACK,
REASON_EXPIRED,
REASON_MACHINE_MISMATCH,
REASON_NOT_YET_VALID,
REASON_OK,
REASON_PLUGIN_NOT_LICENSED,
REASON_SIGNATURE_INVALID,
ClockGuard,
LicenseManager,
MemoryClockStore,
RequestCodeError,
decode_request_code,
encode_request_code,
machine_code,
match_fingerprints,
normalize,
read_license,
sign_payload,
)
failures: list[str] = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
# ---------------------------------------------------------------------------
# 夹具
# ---------------------------------------------------------------------------
# 模拟真实盒子的指纹:4 个可信来源 + 1 个 weak(同镜像刷机会重复的 machine-id)
HW = {
"board_serial": "sn123456789",
"emmc_cid": "1a2b3c4d",
"cpu_serial": "0000deadbeef",
"mac": "aabbccddeeff",
"machine_id": "sharedbyallunits",
}
def _keypair():
priv = Ed25519PrivateKey.generate()
pub = base64.b64encode(
priv.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
return pub, priv
def _write_license(tmp: Path, priv, sources=None, plugins=None, **over) -> str:
src = HW if sources is None else sources
payload = {
"v": 1,
"lic_id": "L-TEST",
"customer": "测试客户",
# sources 为空表示"只签机器码"的授权(无逐来源容错),此时机器码必须由调用方给定
"machine_code": over.pop("machine_code") if not src else machine_code(src),
"sources": src,
"plugins": plugins if plugins is not None else ["aibox_sophon_adapter"],
"not_before": None,
"expires_at": (date.today() + timedelta(days=365)).isoformat(),
"issued_at": "2026-09-20T00:00:00Z",
"note": "",
}
payload.update(over)
doc = {"payload": payload, "alg": "ed25519", "kid": "test",
"sig": sign_payload(payload, priv)}
path = tmp / "license.lic"
path.write_text(json.dumps(doc), encoding="utf-8")
return str(path)
# ---------------------------------------------------------------------------
# 指纹与机器码
# ---------------------------------------------------------------------------
def test_normalize_rejects_placeholders():
"""占位符/全 0/全 f/过短一律视为无效。"""
for bad in ["", " ", "unknown", "None", "0000000000000000", "ffffffffffff",
"ab", "00000000-0000-0000-0000-000000000000"]:
check(f"normalize 拒绝 {bad!r}", normalize(bad) == "", f"got {normalize(bad)!r}")
check("normalize 保留合法值", normalize("SN-1234-ABCD") == "sn1234abcd",
normalize("SN-1234-ABCD"))
check("normalize 分隔符差异归一", normalize("AA:BB:CC:DD:EE:FF") == normalize("aabbccddeeff"))
def test_machine_code_deterministic_and_weak_excluded():
"""机器码确定,且不受 weak 来源影响。"""
a = machine_code(HW)
b = machine_code(dict(reversed(list(HW.items()))))
check("机器码与来源顺序无关", a == b)
check("机器码分组格式", len(a.split("-")) == 6 and len(a.replace("-", "")) == 24, a)
other = dict(HW, machine_id="anothervalue")
check("weak 来源变化不影响机器码", machine_code(other) == a)
strong_changed = dict(HW, mac="112233445566")
check("可信来源变化会改变机器码", machine_code(strong_changed) != a)
# ---------------------------------------------------------------------------
# 容错匹配
# ---------------------------------------------------------------------------
def test_tolerance_single_component_swap():
"""换一块网卡 / 换一颗 eMMC 仍通过;整盘拷到别的机器不通过。"""
same = match_fingerprints(HW, dict(HW))
check("同机全命中", same.ok and same.matched == 4 and same.total == 4, same.reason)
nic = match_fingerprints(HW, dict(HW, mac="112233445566"))
check("换网卡仍通过(3/4)", nic.ok, nic.reason)
emmc = match_fingerprints(HW, dict(HW, emmc_cid="ffffffff"))
check("换 eMMC 仍通过(3/4)", emmc.ok, emmc.reason)
two = match_fingerprints(HW, dict(HW, mac="112233445566", emmc_cid="ffffffff"))
check("同时换网卡与 eMMC 不通过(2/4)", not two.ok, two.reason)
copied = match_fingerprints(
HW,
{"board_serial": "x1", "emmc_cid": "x2", "cpu_serial": "x3", "mac": "x4",
"machine_id": "sharedbyallunits"},
)
check("整盘拷贝到另一台机器不通过(weak 来源不得计票)",
not copied.ok and copied.matched == 0, copied.reason)
check("判定未降级", not copied.degraded, str(copied.degraded))
def test_tolerance_degraded_when_only_weak():
"""仅有 weak 来源时降级判定,并显式标记。"""
r = match_fingerprints({"machine_id": "shared"}, {"machine_id": "shared"})
check("仅 weak 来源时降级通过", r.ok and r.degraded, r.reason)
# ---------------------------------------------------------------------------
# 验签与判定
# ---------------------------------------------------------------------------
def test_sign_verify_roundtrip_and_tamper():
tmp = Path(tempfile.mkdtemp())
pub, priv = _keypair()
path = _write_license(tmp, priv)
st = read_license(path, pub, sources=HW)
check("正常授权校验通过", st.ok and st.code == REASON_OK, st.message)
check("解析出授权插件", st.licensed_plugins == {"aibox_sophon_adapter"},
str(st.licensed_plugins))
# 篡改白名单(典型的"自己加一个插件")→ 签名失效
doc = json.loads(Path(path).read_text())
doc["payload"]["plugins"] = ["aibox_sophon_adapter", "doubao"]
bad = tmp / "tampered.lic"
bad.write_text(json.dumps(doc))
st2 = read_license(str(bad), pub, sources=HW)
check("篡改白名单被识破", not st2.ok and st2.code == REASON_SIGNATURE_INVALID,
st2.message)
# 换公钥(非本厂商签发)
other_pub, _ = _keypair()
st3 = read_license(path, other_pub, sources=HW)
check("非本厂商公钥验签失败",
not st3.ok and st3.code == REASON_SIGNATURE_INVALID, st3.message)
# 字段重排序不影响验签(签名覆盖规范化 JSON)
doc = json.loads(Path(path).read_text())
doc["payload"] = dict(reversed(list(doc["payload"].items())))
reordered = tmp / "reordered.lic"
reordered.write_text(json.dumps(doc))
st4 = read_license(str(reordered), pub, sources=HW)
check("载荷字段重排序不影响验签", st4.ok, st4.message)
def test_expiry_and_not_before():
tmp = Path(tempfile.mkdtemp())
pub, priv = _keypair()
expired = _write_license(
tmp, priv, expires_at=(date.today() - timedelta(days=1)).isoformat()
)
st = read_license(expired, pub, sources=HW)
check("过期授权被拒", not st.ok and st.code == REASON_EXPIRED, st.message)
# 到期当天仍有效(含首尾当天)
today = _write_license(tmp, priv, expires_at=date.today().isoformat())
st2 = read_license(today, pub, sources=HW)
check("到期当天仍然有效", st2.ok, st2.message)
future = _write_license(
tmp, priv, not_before=(date.today() + timedelta(days=5)).isoformat()
)
st3 = read_license(future, pub, sources=HW)
check("未生效授权被拒", not st3.ok and st3.code == REASON_NOT_YET_VALID, st3.message)
never = _write_license(tmp, priv, expires_at=None)
st4 = read_license(never, pub, sources=HW)
check("无到期日视为永久有效", st4.ok, st4.message)
def test_machine_mismatch_and_plugin_whitelist():
tmp = Path(tempfile.mkdtemp())
pub, priv = _keypair()
path = _write_license(tmp, priv, plugins=["ip_sound_column"])
st = read_license(path, pub, sources=HW)
check("本机授权有效", st.ok, st.message)
mgr = LicenseManager(license_path=path, public_key=pub, min_match=3,
clock_store=MemoryClockStore(), collector=lambda: HW)
check("白名单内插件放行", mgr.check_plugin("ip_sound_column").allowed)
denied = mgr.check_plugin("doubao")
check("白名单外插件拒绝",
not denied.allowed and denied.code == REASON_PLUGIN_NOT_LICENSED, denied.message)
other_machine = dict(HW, mac="999999999999", cpu_serial="888888888888")
st2 = read_license(path, pub, sources=other_machine)
check("机器码不符拒绝",
not st2.ok and st2.code == REASON_MACHINE_MISMATCH, st2.message)
def test_machine_code_only_license_fallback():
"""只签机器码(无逐来源指纹)时退化为精确匹配。"""
tmp = Path(tempfile.mkdtemp())
pub, priv = _keypair()
path = _write_license(tmp, priv, sources={}, machine_code=machine_code(HW))
st = read_license(path, pub, sources=HW)
check("仅机器码的授权:本机通过", st.ok, st.message)
st2 = read_license(path, pub, sources=dict(HW, mac="000000000001"))
check("仅机器码的授权:异机拒绝", not st2.ok and st2.code == REASON_MACHINE_MISMATCH,
st2.message)
# ---------------------------------------------------------------------------
# 申请码
# ---------------------------------------------------------------------------
def test_request_code_roundtrip():
code = encode_request_code(HW)
check("申请码前缀", code.startswith("CM1-"), code[:12])
data = decode_request_code(code)
check("申请码解析出全部来源", data["sources"] == HW, str(data["sources"]))
check("申请码含机器码", data["machine_code"] == machine_code(HW))
wrapped = code[:30] + "\n " + code[30:] + " "
check("申请码容忍折行与空白", decode_request_code(wrapped)["sources"] == HW)
try:
decode_request_code(code[:-1] + ("0" if code[-1] != "0" else "1"))
check("申请码校验和可检出抄写错误", False, "未抛异常")
except RequestCodeError as exc:
check("申请码校验和可检出抄写错误", True, str(exc))
try:
decode_request_code("not-a-code")
check("非法申请码被拒", False, "未抛异常")
except RequestCodeError:
check("非法申请码被拒", True)
# ---------------------------------------------------------------------------
# 时钟防线
# ---------------------------------------------------------------------------
def test_clock_guard():
store = MemoryClockStore()
guard = ClockGuard(store, tolerance_seconds=3600)
now = datetime(2026, 9, 20, 12, 0, tzinfo=timezone.utc)
check("首次检查通过并建立水位", guard.check(now).ok)
later = now + timedelta(hours=5)
check("时间前进正常", guard.check(later).ok)
# 回退 30 分钟:容差内,放行
slight = later - timedelta(minutes=30)
check("容差内回退放行", guard.check(slight).ok)
# 回退 10 小时:超过容差,拒绝
rollback = later - timedelta(hours=10)
r = guard.check(rollback)
check("明显回拨被检出", not r.ok and r.rollback_seconds > 0, r.reason)
# 水位未被回退覆盖
check("回拨后水位未被拉低", guard.check(later).ok)
def test_clock_guard_reseeds_when_far_ahead():
"""水位远超当前时间(RTC 异常)时重新起算,避免把设备永久卡死。"""
store = MemoryClockStore()
guard = ClockGuard(store, tolerance_seconds=3600, reseed_if_ahead_days=90)
guard.check(datetime(2027, 1, 1, tzinfo=timezone.utc))
r = guard.check(datetime(2026, 9, 20, tzinfo=timezone.utc))
check("大幅领先判为时钟异常而非攻击", r.ok and "异常" in r.reason, r.reason)
def test_manager_blocks_on_clock_rollback():
"""时钟回拨时管理器的判定整体中止。"""
tmp = Path(tempfile.mkdtemp())
pub, priv = _keypair()
path = _write_license(tmp, priv, plugins=["ip_sound_column"])
store = MemoryClockStore()
far_future = datetime.now(timezone.utc) + timedelta(days=2)
ClockGuard(store, tolerance_seconds=3600).check(far_future)
mgr = LicenseManager(license_path=path, public_key=pub, clock_store=store,
collector=lambda: HW)
st = mgr.reload()
check("时钟回拨时授权判定中止",
not st.ok and st.code == REASON_CLOCK_ROLLBACK, st.message)
check("时钟回拨时插件被拒", not mgr.check_plugin("ip_sound_column").allowed)
def test_manager_disabled_mode_reports_reality():
"""enforce=False(开发/测试)时放行,但状态仍如实反映真实校验结果。"""
tmp = Path(tempfile.mkdtemp())
pub, priv = _keypair()
path = _write_license(tmp, priv, plugins=[])
mgr = LicenseManager(license_path=path, public_key=pub, enforce=False,
collector=lambda: HW)
check("未启用时插件放行", mgr.check_plugin("doubao").allowed)
check("未启用时仍如实报告授权有效", mgr.status.ok, mgr.status.message)
def main():
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
for fn in tests:
print(f"\n--- {fn.__name__} ---")
fn()
print("\n" + "=" * 56)
if failures:
print(f"❌ {len(failures)} 项未通过: {failures}")
return 1
print("✅ 全部通过")
return 0
if __name__ == "__main__":
sys.exit(main())