22 KiB
自研授权 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 + 添加依赖
[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
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
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 实现
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 新增字段
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
public record FeatureMapping(
@JsonProperty("bitanswerFeatureId") Integer bitanswerFeatureId,
@JsonProperty("bitanswerFeatureName") String bitanswerFeatureName,
@JsonProperty("selfhostedFeatureKey") String selfhostedFeatureKey) {} // ★ 新增
- Step 3: Schema 扩展 selfhosted properties
"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
@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 密钥对
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 离线验签通过
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
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
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
// 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: 最终集成验证
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 |