# 自研授权 SDK 实施计划(完整版) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. **Goal:** 实现自研软件授权 SDK(Rust native + Java SDK + 平台签发后端),与比特安索双线共存,Provider 可扩展架构。 **Architecture:** 单 Rust cdylib(craftlabs_auth_core)通过 Provider trait 路由。许可证 AES-256-GCM 加密载荷 + RSA-256 签名。签发走 delivery-platform-api:8080,SDK 在线交互走 license-webhook-ingress:8081。 **Tech Stack:** Rust (craft-core, cdylib/staticlib), Java 17 (Maven), Spring Boot 3.4.x, PostgreSQL 15 + MyBatis-Plus, Flyway **Reference Spec:** `docs/superpowers/specs/2026-05-18-selfhosted-licensing-sdk-design.md` --- ## Phase 0-1 速览(Phase 1 完整步骤见下方任务列表) Phase 0(构建准备)+ Phase 1(离线核心:Rust crypto/device/cache/license 模块 + trait_provider + lib.rs 重构 + Java 配置扩展 + Schema + 数据库迁移 + LicenseSigner + LicenseController)共计 15 个任务。 --- ## Task 0.1: 更新 Cargo.toml **Files:** Modify `native/craft-core/Cargo.toml` - [ ] **Step 1: 重命名 lib + 添加依赖** ```toml [lib] crate-type = ["cdylib", "staticlib"] name = "craftlabs_auth_core" [dependencies] obfstr = "0.4" sha2 = "0.10" libloading = "0.8" once_cell = "1" rsa = { version = "0.9", features = ["sha2"] } aes-gcm = "0.10" hkdf = "0.12" base64 = "0.22" serde = { version = "1", features = ["derive"] } serde_json = "1" chrono = { version = "0.4", features = ["serde"] } [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.52", features = ["Win32_System_Diagnostics_Debug"] } [build-dependencies] sha2 = "0.10" [features] default = ["security-hardening"] security-hardening = [] ``` - [ ] **Step 2: `cargo check --manifest-path native/craft-core/Cargo.toml`** - [ ] **Step 3: `git commit -m "build(native): rename lib to craftlabs_auth_core, add selfhosted deps"`** --- ## Task 1.1-1.6: Rust 离线核心模块(Part 1 覆盖) 以下 6 个任务的完整代码见 Part 1(文档长度原因拆分)。Part 1 commit 记录: | 任务 | 文件 | 内容 | |------|------|------| | 1.1 | `error.rs` | 扩展错误码(加密/签名/许可证状态) | | 1.2 | `crypto.rs` | HKDF 密钥派生 + AES-256-GCM 加解密 + RSA-SHA256 验签 | | 1.3 | `device.rs` | 4 层硬件指纹采集(Linux DMI/machine-id/FS/MAC),stability_score | | 1.4 | `provider_selfhosted/cache.rs` | 本地加密缓存(AES 加密写入磁盘 + 心跳 checkpoint) | | 1.5 | `provider_selfhosted/license.rs` | 验签→解密→时间校验→离线宽限期 | | 1.6 | `provider_selfhosted/mod.rs` | SelfHostedProvider struct + 初始化/离线校验/hasFeature | > Part 1 详细步骤已写入 Git:commit `d7469af`(spec)及之前。 --- ## Task 1.7: trait_provider.rs(Provider trait + 路由) **Files:** Create `native/craft-core/src/trait_provider.rs` - [ ] **Step 1: 创建 trait_provider.rs** ```rust use std::collections::HashMap; use crate::{CraftContext, LicenseInfo, error::LicenseError}; pub struct LicenseStatus { pub licensed: bool, pub not_after: Option, pub features: HashMap, pub device_count: u32, pub max_devices: u32, pub heartbeat_due: Option, } pub struct ActivateResponse { pub success: bool, pub device_id: String, pub license_payload: Vec, } pub struct HeartbeatResponse { pub valid: bool, pub lease_until: Option, pub update_available: bool, pub new_license_payload: Option>, } pub trait Provider: Send + Sync { fn initialize(&mut self, ctx: &CraftContext, config_json: &str) -> Result<(), LicenseError>; fn activate(&self, ctx: &CraftContext, license_key: &str) -> Result; fn check_license(&self, ctx: &CraftContext) -> Result; fn heartbeat(&self, ctx: &CraftContext) -> Result; fn has_feature(&self, ctx: &CraftContext, name: &str) -> bool; fn release(&mut self, ctx: &CraftContext) -> Result<(), LicenseError>; fn get_license_info(&self, ctx: &CraftContext) -> LicenseInfo; fn close(&mut self); } ``` - [ ] **Step 2: `cargo check` → `git commit -m "feat(rust): add Provider trait abstraction"`** --- ## Task 1.8: 重构 lib.rs(C ABI 路由到 Provider trait) **Files:** Modify `native/craft-core/src/lib.rs` - [ ] **Step 1: 替换 lib.rs** ```rust use std::ffi::CStr; use std::os::raw::c_char; use std::ptr; mod trait_provider; mod error; mod crypto; mod device; mod provider_selfhosted; mod security; mod session; pub use trait_provider::{Provider, ActivateResponse, HeartbeatResponse, LicenseStatus}; pub use error::LicenseError; pub struct CraftContext { pub provider: Option>, pub initialized: bool, } #[repr(C)] pub struct CraftResult { pub success: i32, pub message: *const c_char } #[repr(C)] pub struct LicenseInfo { pub is_licensed: i32, pub expiration_date: i64, pub feature_names: *const *const c_char, pub feature_values: *const i32, pub feature_count: i32, } impl CraftContext { pub fn new() -> Self { CraftContext { provider: None, initialized: false } } } unsafe fn c_str_to_string(ptr: *const c_char) -> String { if ptr.is_null() { String::new() } else { CStr::from_ptr(ptr).to_string_lossy().into_owned() } } static OK_MSG: &[u8] = b"ok\0"; fn ok_result() -> CraftResult { CraftResult { success: 1, message: OK_MSG.as_ptr() as *const c_char } } fn craft_fail() -> CraftResult { static FAIL: &[u8] = b"failure\0"; CraftResult { success: 0, message: FAIL.as_ptr() as *const c_char } } fn parse_provider(config: &str) -> String { serde_json::from_str::(config).ok() .and_then(|v| v.get("provider").and_then(|p| p.as_str().map(|s| s.to_string()))) .unwrap_or_else(|| "selfhosted".to_string()) } #[no_mangle] pub extern "C" fn craft_initialize(config_json: *const c_char) -> *mut CraftContext { let config_str = unsafe { c_str_to_string(config_json) }; let mut ctx = Box::new(CraftContext::new()); let mut provider: Box = match parse_provider(&config_str).as_str() { "selfhosted" => Box::new(provider_selfhosted::SelfHostedProvider::new()), _ => Box::new(provider_selfhosted::SelfHostedProvider::new()), }; if let Err(_) = provider.initialize(&ctx, &config_str) { ctx.initialized = false; } else { ctx.provider = Some(provider); ctx.initialized = true; } Box::into_raw(ctx) } #[no_mangle] pub extern "C" fn craft_activate(handle: *mut CraftContext, license_key: *const c_char, _: *const c_char) -> CraftResult { if handle.is_null() { return craft_fail(); } let ctx = unsafe { &*handle }; let key = unsafe { c_str_to_string(license_key) }; ctx.provider.as_ref().and_then(|p| p.activate(ctx, &key).ok()).map_or_else(craft_fail, |_| ok_result()) } #[no_mangle] pub extern "C" fn craft_check_license(handle: *mut CraftContext) -> CraftResult { if handle.is_null() { return craft_fail(); } let ctx = unsafe { &*handle }; ctx.provider.as_ref().and_then(|p| p.check_license(ctx).ok()) .map_or_else(craft_fail, |s| if s.licensed { ok_result() } else { craft_fail() }) } #[no_mangle] pub extern "C" fn craft_get_license_info(handle: *mut CraftContext) -> *mut LicenseInfo { if handle.is_null() { return ptr::null_mut(); } let ctx = unsafe { &*handle }; ctx.provider.as_ref().map(|p| p.get_license_info(ctx)) .map_or(ptr::null_mut(), |info| Box::into_raw(Box::new(info))) } #[no_mangle] pub extern "C" fn craft_free_license_info(info: *mut LicenseInfo) { if !info.is_null() { unsafe { drop(Box::from_raw(info)); } } } #[no_mangle] pub extern "C" fn craft_has_feature(handle: *mut CraftContext, feature_name: *const c_char) -> i32 { if handle.is_null() { return 0; } let ctx = unsafe { &*handle }; let name = unsafe { c_str_to_string(feature_name) }; ctx.provider.as_ref().map_or(0, |p| if p.has_feature(ctx, &name) { 1 } else { 0 }) } #[no_mangle] pub extern "C" fn craft_release(handle: *mut CraftContext) -> CraftResult { if handle.is_null() { return craft_fail(); } let ctx = unsafe { &mut *handle }; ctx.provider.as_mut().and_then(|p| p.release(ctx).ok()); ok_result() } #[no_mangle] pub extern "C" fn craft_heartbeat(handle: *mut CraftContext) -> CraftResult { if handle.is_null() { return craft_fail(); } let ctx = unsafe { &*handle }; ctx.provider.as_ref().and_then(|p| p.heartbeat(ctx).ok()).map_or_else(craft_fail, |_| ok_result()) } #[no_mangle] pub extern "C" fn craft_destroy(handle: *mut CraftContext) { if !handle.is_null() { unsafe { let ctx = &mut *handle; if let Some(ref mut p) = ctx.provider { p.close(); } drop(Box::from_raw(handle)); } } } ``` - [ ] **Step 2: 在 provider_selfhosted/mod.rs 追加 Provider trait 实现** ```rust use crate::trait_provider::{Provider, ActivateResponse, HeartbeatResponse, LicenseStatus}; impl Provider for SelfHostedProvider { fn initialize(&mut self, _ctx: &CraftContext, config_json: &str) -> Result<(), LicenseError> { let cfg: serde_json::Value = serde_json::from_str(config_json).map_err(|_| LicenseError::InvalidFormat("config"))?; let sh = cfg.get("selfhosted").ok_or(LicenseError::ConfigMissing("selfhosted"))?; let base_url = sh.get("baseUrl").and_then(|v| v.as_str()).unwrap_or("").to_string(); let tenant_key = sh.get("tenantKey").and_then(|v| v.as_str()).unwrap_or("").to_string(); let ogd = sh.get("offlineGraceDays").and_then(|v| v.as_u64()).unwrap_or(7) as u32; let hih = sh.get("heartbeatIntervalHours").and_then(|v| v.as_u64()).unwrap_or(24) as u32; let pubkey = option_env!("CRAFTLABS_SELFHOSTED_PUBKEY").unwrap_or("").to_string(); self.initialize(base_url, tenant_key, ogd, hih, pubkey) } fn activate(&self, _ctx: &CraftContext, _lk: &str) -> Result { Err(LicenseError::Network("online activation not yet implemented".into())) } fn check_license(&self, _ctx: &CraftContext) -> Result { self.check_license_offline()?; let c = self.cache.license.as_ref().ok_or(LicenseError::NoCachedLicense)?; Ok(LicenseStatus { licensed: true, not_after: c.not_after, features: c.features.clone(), device_count: 0, max_devices: c.max_devices, heartbeat_due: None }) } fn heartbeat(&self, _ctx: &CraftContext) -> Result { Err(LicenseError::Network("heartbeat not yet implemented".into())) } fn has_feature(&self, _ctx: &CraftContext, name: &str) -> bool { self.has_feature_offline(name) } fn release(&mut self, _ctx: &CraftContext) -> Result<(), LicenseError> { Ok(()) } fn get_license_info(&self, _ctx: &CraftContext) -> LicenseInfo { self.get_license_info_offline() } fn close(&mut self) { let _ = self.persist_cache(); } } ``` - [ ] **Step 3: `cargo check` + `cargo test` → `git commit -m "feat(rust): refactor C ABI to route through Provider trait"`** --- ## Task 1.9-1.10: Java 配置 + Schema 扩展 **Files:** Modify `SelfhostedConfigSection.java`, `FeatureMapping.java`, `schemas/craftlabs-auth-config.schema.json`, `examples/config/school.selfhosted.json` - [ ] **Step 1: SelfhostedConfigSection 新增字段** ```java public record SelfhostedConfigSection( @JsonProperty("baseUrl") String baseUrl, @JsonProperty("tenantKey") String tenantKey, @JsonProperty("offlineGraceDays") Integer offlineGraceDays, // ★ 新增 @JsonProperty("heartbeatIntervalHours") Integer heartbeatIntervalHours, // ★ 新增 @JsonProperty("publicKeyPem") String publicKeyPem) { // ★ 新增 public SelfhostedConfigSection { offlineGraceDays = offlineGraceDays != null ? offlineGraceDays : 7; heartbeatIntervalHours = heartbeatIntervalHours != null ? heartbeatIntervalHours : 24; } } ``` - [ ] **Step 2: FeatureMapping 新增 selfhostedFeatureKey** ```java public record FeatureMapping( @JsonProperty("bitanswerFeatureId") Integer bitanswerFeatureId, @JsonProperty("bitanswerFeatureName") String bitanswerFeatureName, @JsonProperty("selfhostedFeatureKey") String selfhostedFeatureKey) {} // ★ 新增 ``` - [ ] **Step 3: Schema 扩展 selfhosted properties** ```json "offlineGraceDays": { "type": "integer", "minimum": 0, "maximum": 365, "default": 7 }, "heartbeatIntervalHours": { "type": "integer", "minimum": 1, "maximum": 720, "default": 24 }, "publicKeyPem": { "type": "string" } ``` 同步扩展 `features.additionalProperties` 增加 `selfhostedFeatureKey`。 - [ ] **Step 4: 更新 `examples/config/school.selfhosted.json`** - [ ] **Step 5: `mvn compile -pl craftlabs-auth-core` + Schema 校验 → `git commit -m "feat: extend Java config and Schema for selfhosted SDK"`** --- ## Task 1.11: 平台数据库迁移(Flyway V2) **Files:** Create `services/delivery-platform-api/src/main/resources/db/migration/V2__selfhosted_licensing.sql` 核心 6 张表:`platform_license_policies`, `platform_license_keys`, `platform_licenses`, `platform_license_features`, `platform_license_activations`, `platform_license_heartbeats`。完整 DDL 见设计文档 §3.6。 - [ ] **Step: `docker compose up -d postgres` + `mvn flyway:migrate` → commit** --- ## Task 1.12: LicenseSigner.java(RSA 签名 + AES 加密签发) **Files:** Create `services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/license/LicenseSigner.java` ```java @Service public class LicenseSigner { private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String EMBEDDED_SALT = "craftlabs-license-salt-v1-2026-05"; public String sign(SignedLicensePayload payload, PlatformLicenseKey key) throws Exception { byte[] payloadJson = MAPPER.writeValueAsBytes(payload); byte[] aesKey = deriveAesKey(EMBEDDED_SALT, payload.getLicenseId()); byte[] encrypted = aesGcmEncrypt(aesKey, payloadJson); String payloadB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted); PrivateKey privateKey = loadPrivateKey(key.getPrivateKey()); Signature sig = Signature.getInstance("SHA256withRSA"); sig.initSign(privateKey); sig.update(encrypted); String sigB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(sig.sign()); LicenseDocument doc = LicenseDocument.builder() .version(1).licenseId(payload.getLicenseId()) .payload(payloadB64) .signature(SignatureBlock.builder().algorithm("RS256").keyId(key.getKeyId()).value(sigB64).build()) .build(); return MAPPER.writeValueAsString(doc); } private byte[] deriveAesKey(String salt, String licenseId) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(salt.getBytes("UTF-8"), "HmacSHA256")); byte[] prk = mac.doFinal(new byte[0]); mac.init(new SecretKeySpec(prk, "HmacSHA256")); mac.update(licenseId.getBytes("UTF-8")); mac.update((byte) 0x01); return mac.doFinal(); } private byte[] aesGcmEncrypt(byte[] key, byte[] plaintext) throws Exception { byte[] nonce = new byte[12]; new SecureRandom().nextBytes(nonce); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce)); byte[] ct = cipher.doFinal(plaintext); byte[] r = new byte[12 + ct.length]; System.arraycopy(nonce, 0, r, 0, 12); System.arraycopy(ct, 0, r, 12, ct.length); return r; } } ``` - [ ] **Step: `mvn compile` → `git commit -m "feat(platform): add LicenseSigner for RSA+AES license issuance"`** --- ## Task 1.13-1.14: Entity/Mapper + LicenseService + LicenseController **Files:** Create 5 组 Entity+Mapper(`persistence/license/`)+ LicenseService + LicenseController 核心 API: ``` POST /api/v1/licenses → 签发许可证(需 LICENSE_OPS 角色) GET /api/v1/licenses/{id} → 查询详情 POST /api/v1/licenses/{id}/revoke → 吊销 ``` SecurityConfig 新增:`.requestMatchers("/api/v1/licenses/**").hasAnyRole("LICENSE_OPS", "ADMIN")` - [ ] **Step: `mvn compile` + `mvn test -Dtest=LicenseControllerTest` → commit** --- ## Task 1.15: Phase 1 集成验证 - [ ] **Step 1: 生成 RSA 密钥对** ```bash openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 openssl rsa -pubout -in private_key.pem -out public_key.pem ``` - [ ] **Step 2: `export CRAFTLABS_SELFHOSTED_PUBKEY="$(cat public_key.pem)"`** - [ ] **Step 3: `cargo test --manifest-path native/craft-core/Cargo.toml` → all pass** - [ ] **Step 4: 启动服务 + curl 签发 → Rust 离线验签通过** ```bash curl -X POST http://localhost:8080/api/v1/licenses \ -H "Authorization: Bearer $TOKEN" -d '{"tenantId":"test",...}' # 将返回的 license.json 保存 → Rust 集成测 validate ``` - [ ] **Step 5: Commit** --- ## Phase 2: 在线激活(3 个任务) ### Task 2.1: protocol.rs(HTTP 请求/响应序列化) **Files:** Create `native/craft-core/src/provider_selfhosted/protocol.rs` Serialize/Deserialize DTOs:`ActivateRequest`, `ActivateResponseBody`, `HeartbeatRequest/Response`, `CheckRequest/Response`, `ReleaseRequest/Response`。HMAC 签名工具函数 `build_hmac_signature`。 新增依赖:`reqwest = "0.12"`, `hex = "0.4"`, `hmac = "0.12"`, `rand = "0.8"`, `tokio = "1"` - [ ] **Step: `cargo check` → commit** ### Task 2.2: activate.rs(在线激活 HTTPS 请求) **Files:** Create `native/craft-core/src/provider_selfhosted/activate.rs` ```rust pub async fn online_activate(config: &SelfHostedConfig, fp: &DeviceFingerprint, license_key: &str) -> Result { let req = protocol::ActivateRequest { license_key: license_key.to_string(), device_fingerprint: /* fp */ }; let body = serde_json::to_string(&req).unwrap(); // POST /license/v1/activate + HMAC headers ... match resp.status() { 200 => Ok(parse_body), 409 => Err(DeviceLimitReached), 403 => Err(LicenseRevoked), } } ``` 在 `provider_selfhosted/mod.rs` 的 `activate()` 中调用 `tokio::runtime::Handle::current().block_on(online_activate(...))`。 - [ ] **Step: `cargo test` → commit** ### Task 2.3: Webhook 侧 LicenseController + ActivateService **Files:** Create license endpoints in `services/license-webhook-ingress` - `NonceValidator.java`: Nonce 去重(ConcurrentHashMap + 5min 窗口) - `LicenseActivateService.java`: 查许可证状态 + 终端配额 + 设备匹配 + 下发生效 - `LicenseController.java`: `POST /license/v1/activate`(Bearer tenantKey + HMAC header 校验) - [ ] **Step: `mvn compile` + `mvn test` → commit** --- ## Phase 3: 心跳 + 离线兜底(2 个任务) ### Task 3.1: heartbeat.rs(在线心跳 + 吊销检测) **Files:** Create `native/craft-core/src/provider_selfhosted/heartbeat.rs` ```rust pub async fn online_heartbeat(config: &SelfHostedConfig, device_hash: &str, license_key: &str) -> Result { // POST /license/v1/heartbeat → // 200: 更新租约 + 可选下发新许可证 // 410: LicenseRevoked } ``` 更新 mod.rs heartbeat() 实现。 ### Task 3.2: Webhook HeartbeatService + CheckService + ReleaseService **Files:** Append to webhook `LicenseController` - `POST /license/v1/heartbeat` — 更新 `last_heartbeat`,返回租约续期时间 - `POST /license/v1/check` — 在线校验许可证有效性 - `POST /license/v1/release` — 释放终端占用 --- ## Phase 4: 完善与生产加固(5 个任务) ### Task 4.1: build.rs 嵌入 RSA 公钥 **Files:** Modify `native/craft-core/build.rs`, Create `native/craft-core/embedded/pubkey.pem` ```rust // build.rs fn main() { let pubkey = fs::read_to_string("embedded/pubkey.pem").unwrap_or_default(); println!("cargo:rustc-env=CRAFTLABS_SELFHOSTED_PUBKEY={}", pubkey.trim()); println!("cargo:rerun-if-changed=embedded/pubkey.pem"); } ``` 将生产公钥放入 `embedded/pubkey.pem`(不提交私钥)。 ### Task 4.2: SelfHostedAuthProvider 加载正确库 **Files:** Modify `java/craftlabs-auth-selfhosted/.../SelfHostedAuthProvider.java` 改 `System.loadLibrary("craftlabs_auth_bitanswer")` → `System.loadLibrary("craftlabs_auth_core")`。 ### Task 4.3: MultiProviderSmokeTest **Files:** Create `java/craftlabs-auth-tests/.../MultiProviderSmokeTest.java` 测试:初始化 selfhosted → 离线校验 → close → 初始化 bitanswer → close,双线不冲突。 ### Task 4.4: CI 适配 - `ci-native.yml`: 更新 artifact name,确保 `embedded/pubkey.pem` 存在 - `ci-platform.yml`: 新增 license 模块测试 Job ### Task 4.5: 最终集成验证 ```bash cargo test --manifest-path native/craft-core/Cargo.toml # Rust 全量 mvn -f java/pom.xml verify # Java SDK mvn -f services/pom.xml verify # 平台 docker compose -f services/docker-compose.yml up -d postgres # 起库 # curl 签发 → Rust 离线验签 → curl 在线激活 → 终端满 409 → 吊销 410 ``` --- ## 总结 | Phase | 任务数 | 核心交付 | |-------|--------|----------| | P0 | 1 | Cargo.toml 依赖 | | P1 | 14 | Rust 离线核心 + Java 配置 + 平台签发后端 | | P2 | 3 | 在线激活(Rust HTTPS + Webhook 端点) | | P3 | 2 | 心跳 + 离线兜底 + 吊销检测 | | P4 | 5 | build.rs 嵌入 + 测试 + CI | | **合计** | **25** | |