Files
craftlabs-authorization-sdk/docs/superpowers/plans/2026-05-18-selfhosted-licensing-sdk.md

22 KiB
Raw Permalink Blame History

自研授权 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: 实现自研软件授权 SDKRust native + Java SDK + 平台签发后端),与比特安索双线共存,Provider 可扩展架构。

Architecture: 单 Rust cdylibcraftlabs_auth_core)通过 Provider trait 路由。许可证 AES-256-GCM 加密载荷 + RSA-256 签名。签发走 delivery-platform-api:8080SDK 在线交互走 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 详细步骤已写入 Gitcommit d7469afspec)及之前。


Task 1.7: trait_provider.rsProvider 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 checkgit commit -m "feat(rust): add Provider trait abstraction"

Task 1.8: 重构 lib.rsC 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 testgit 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.javaRSA 签名 + 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 compilegit commit -m "feat(platform): add LicenseSigner for RSA+AES license issuance"

Task 1.13-1.14: Entity/Mapper + LicenseService + LicenseController

Files: Create 5 组 Entity+Mapperpersistence/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.rsHTTP 请求/响应序列化)

Files: Create native/craft-core/src/provider_selfhosted/protocol.rs

Serialize/Deserialize DTOsActivateRequest, 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.rsactivate() 中调用 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/activateBearer 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