mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
docs: complete implementation plan for selfhosted licensing SDK (25 tasks, 4 phases)
This commit is contained in:
@@ -0,0 +1,575 @@
|
|||||||
|
# 自研授权 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<i64>,
|
||||||
|
pub features: HashMap<String, bool>,
|
||||||
|
pub device_count: u32,
|
||||||
|
pub max_devices: u32,
|
||||||
|
pub heartbeat_due: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActivateResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub device_id: String,
|
||||||
|
pub license_payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeartbeatResponse {
|
||||||
|
pub valid: bool,
|
||||||
|
pub lease_until: Option<i64>,
|
||||||
|
pub update_available: bool,
|
||||||
|
pub new_license_payload: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ActivateResponse, LicenseError>;
|
||||||
|
fn check_license(&self, ctx: &CraftContext) -> Result<LicenseStatus, LicenseError>;
|
||||||
|
fn heartbeat(&self, ctx: &CraftContext) -> Result<HeartbeatResponse, LicenseError>;
|
||||||
|
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<Box<dyn Provider>>,
|
||||||
|
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::<serde_json::Value>(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<dyn Provider> = 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<ActivateResponse, LicenseError> {
|
||||||
|
Err(LicenseError::Network("online activation not yet implemented".into()))
|
||||||
|
}
|
||||||
|
fn check_license(&self, _ctx: &CraftContext) -> Result<LicenseStatus, LicenseError> {
|
||||||
|
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<HeartbeatResponse, LicenseError> {
|
||||||
|
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<ActivateResponse, LicenseError>
|
||||||
|
{
|
||||||
|
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<HeartbeatResponse, LicenseError>
|
||||||
|
{
|
||||||
|
// 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** | |
|
||||||
Reference in New Issue
Block a user