From 0f8e1f75dace8d15d82df040c21a0d814a2d39b7 Mon Sep 17 00:00:00 2001 From: logan Date: Fri, 16 Oct 2026 03:36:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E4=BB=B7=E7=BB=9F=E4=B8=80=EF=BC=9A?= =?UTF-8?q?=E4=B8=93=E4=B8=9A=E7=89=88=E5=8C=85=E5=B9=B4=20=C2=A599=20/=20?= =?UTF-8?q?=E6=B0=B8=E4=B9=85=20=C2=A5199=EF=BC=88=E4=B8=8E=20OpsXShell=20?= =?UTF-8?q?=E4=B8=80=E8=87=B4=EF=BC=89=EF=BC=9B=E6=8E=88=E6=9D=83=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E9=93=BE=E8=B7=AF=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /pricing 与落地页:¥299 终身→¥199 永久、¥69→¥99、个人版更名专业版(3/5 台设备); 补 Apipost 离线版式购买叙事(一机一码/当场发证/无需注册/年 2 次免费换绑) - 授权服务器(licence.rlwj.cn):opsxapi 永久计划 299→199;注册 opsxdb 产品与 trial/yearly/perpetual 计划——三产品公开 API 均为 0/99/199 - 真实链路验证:管理端签发 opsxapi 在线 LIC-(activate/verify 全通,Ed25519 签名下发)+ 离线 LICOFF-(绑本机 device_id);新增 4 项 Rust 单测用真实 fixture 跑通 (离线验签/换设备拒绝/篡改拒绝/在线响应签名) --- homepage/dist/index.html | 2 +- src-tauri/src/services/license.rs | 51 +++++++++++++++++++++++++++++ tests/fixtures/device-id.txt | 1 + tests/fixtures/licoff-opsxapi.txt | 1 + tests/fixtures/verify-response.json | 1 + tests/fixtures/verify-sig-msg.txt | 1 + tests/fixtures/verify-sig.txt | 1 + 7 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/device-id.txt create mode 100644 tests/fixtures/licoff-opsxapi.txt create mode 100644 tests/fixtures/verify-response.json create mode 100644 tests/fixtures/verify-sig-msg.txt create mode 100644 tests/fixtures/verify-sig.txt diff --git a/homepage/dist/index.html b/homepage/dist/index.html index 2f7c1bd..841cadb 100644 --- a/homepage/dist/index.html +++ b/homepage/dist/index.html @@ -227,7 +227,7 @@

先在工作台免费用起来,需要授权与服务时再付费。

-
个人版
+
专业版
¥99/ 年
单设备授权 · 全功能
    diff --git a/src-tauri/src/services/license.rs b/src-tauri/src/services/license.rs index a97efc3..cd53e1c 100644 --- a/src-tauri/src/services/license.rs +++ b/src-tauri/src/services/license.rs @@ -432,3 +432,54 @@ pub fn deactivate(app: &AppHandle) -> Result { let _ = fs::remove_file(&path); read_status(app) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + /// 真实服务器签发的离线证(licence.rlwj.cn,product=opsxapi) + const REAL_OFFLINE: &str = include_str!("../../../tests/fixtures/licoff-opsxapi.txt"); + const REAL_DEVICE: &str = include_str!("../../../tests/fixtures/device-id.txt"); + + #[test] + fn offline_serial_validates_for_bound_device() { + let key = REAL_OFFLINE.trim(); + assert!(is_offline_license_key(key)); + let claims = verify_offline_serial(key, REAL_DEVICE.trim()) + .expect("真实离线证应通过本产品+设备校验"); + assert_eq!(claims.product_code, "opsxapi"); + assert!(claims.exp > claims.iat); + } + + #[test] + fn offline_serial_rejects_other_device() { + assert!(verify_offline_serial(REAL_OFFLINE.trim(), "00000000-0000-0000-0000-000000000000").is_err()); + } + + #[test] + fn offline_serial_rejects_tampered_payload() { + // 篡改 base64 载荷中段一个字符(保持格式合法,签名必失效) + let key = REAL_OFFLINE.trim().to_string(); + let (payload, sig) = key.strip_prefix("LICOFF-1.").unwrap().rsplit_once('.').unwrap(); + let mut chars: Vec = payload.chars().collect(); + let mid = chars.len() / 2; + chars[mid] = if chars[mid] == 'A' { 'B' } else { 'A' }; + let tampered = format!("LICOFF-1.{}.{}", chars.into_iter().collect::(), sig); + assert_ne!(key, tampered); + assert!(verify_offline_serial(&tampered, REAL_DEVICE.trim()).is_err()); + } + + /// 真实服务器 /v1/license/verify 响应的 Ed25519 签名(hex)与签名原文 + #[test] + fn online_response_sig_verifies_with_embedded_key() { + // 1) 原文直接验(证明公钥同源) + let msg = include_str!("../../../tests/fixtures/verify-sig-msg.txt"); + let sig_hex = include_str!("../../../tests/fixtures/verify-sig.txt").trim(); + let sig = Signature::from_slice(&hex::decode(sig_hex).unwrap()).unwrap(); + offline_verifying_key().unwrap().verify(msg.as_bytes(), &sig).expect("服务器签名应通过内嵌公钥校验"); + // 2) 完整响应 JSON 走 verify_response_sig(证明客户端重组串与服务器一致) + let payload: Value = serde_json::from_str(include_str!("../../../tests/fixtures/verify-response.json")).unwrap(); + assert!(verify_response_sig(&payload).is_ok()); + } +} diff --git a/tests/fixtures/device-id.txt b/tests/fixtures/device-id.txt new file mode 100644 index 0000000..af8d65b --- /dev/null +++ b/tests/fixtures/device-id.txt @@ -0,0 +1 @@ +b6eeb606-82ef-438f-946f-ea6fa6b272ec diff --git a/tests/fixtures/licoff-opsxapi.txt b/tests/fixtures/licoff-opsxapi.txt new file mode 100644 index 0000000..4b1d577 --- /dev/null +++ b/tests/fixtures/licoff-opsxapi.txt @@ -0,0 +1 @@ +LICOFF-1.eyJ2IjoxLCJwaWQiOiJvcHN4YXBpIiwiZGlkIjoiYjZlZWI2MDYtODJlZi00MzhmLTk0NmYtZWE2ZmE2YjI3MmVjIiwiZW1haWwiOiJsb2dhbkB0ZXN0LmRldiIsInB0IjoieWVhcmx5IiwiaWF0IjoxNzkyMDkyNzQwLCJleHAiOjE4MjM2Mjg3NDAsIm1kIjoxLCJjbiI6IkxvZ2FuKOa1i-ivlSkifQ.1G3TyluNJk1RB1S8vALMelhfUeb0S2YqqgoYYvSoq9JcAI64pMCoPNOUkbS0OOp-s4zIpEIxgHTSShbpsUxDCw \ No newline at end of file diff --git a/tests/fixtures/verify-response.json b/tests/fixtures/verify-response.json new file mode 100644 index 0000000..0a91f70 --- /dev/null +++ b/tests/fixtures/verify-response.json @@ -0,0 +1 @@ +{"valid":true,"license_key":"LIC-2026-A424-9335-AA41-AD88","product_id":2,"product_name":"OpsxAPI","product_code":"opsxapi","features":["http","graphql","websocket","sse","socketio","mqtt","mock","testing","ai"],"status":"active","expires_at":"2027-10-16 03:31:21","days_left":364,"max_devices":3,"plan_type":"yearly","message":"验证成功","expires_at_unix":1823628681,"server_time":1792092713,"sig":"4d958e15f1ef10b3f2ea5f4fa2c6aae5f7f323145554ca9b64703ab0295dcacaee57cb02792c647e41eba62dc813ef515b040e644c06fac6c02a7707efe58c0b"} \ No newline at end of file diff --git a/tests/fixtures/verify-sig-msg.txt b/tests/fixtures/verify-sig-msg.txt new file mode 100644 index 0000000..8e4afc3 --- /dev/null +++ b/tests/fixtures/verify-sig-msg.txt @@ -0,0 +1 @@ +v1|LIC-2026-A424-9335-AA41-AD88|true|active|1823628681|1792092713|false|3|yearly \ No newline at end of file diff --git a/tests/fixtures/verify-sig.txt b/tests/fixtures/verify-sig.txt new file mode 100644 index 0000000..0218f35 --- /dev/null +++ b/tests/fixtures/verify-sig.txt @@ -0,0 +1 @@ +4d958e15f1ef10b3f2ea5f4fa2c6aae5f7f323145554ca9b64703ab0295dcacaee57cb02792c647e41eba62dc813ef515b040e644c06fac6c02a7707efe58c0b \ No newline at end of file