zhuyuan-ice-gl-zy001/ZCODE-DEV-20260826/public-spine/shell-frozen-src/src-tauri/src/module_package_runtime.rs

1765 lines
67 KiB
Rust

//! Signed, account-scoped HoloLake module package runtime.
//!
//! A repository is never executable input. The runtime accepts only an immutable
//! detached-minisign package, validates a declarative manifest, persists lifecycle
//! state in SQLite and writes a hash-chained receipt for every mutation. Package
//! mount does not load native code or arbitrary webview JavaScript.
use base64::{engine::general_purpose::STANDARD, Engine as _};
use minisign_verify::{PublicKey, Signature};
use ring::digest::{digest, SHA256};
use rusqlite::{params, Connection, OptionalExtension, Transaction};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::AppHandle;
use uuid::Uuid;
const CONTRACT_RAW: &str = include_str!("../../contracts/module-package-runtime.json");
const RELEASE_TRUST_RAW: &str = include_str!("../release-trust.json");
const CONTRACT_SCHEMA: &str = "hololake.module-package-runtime/v1";
const PACKAGE_SCHEMA: &str = "hololake.module-package/v1";
const HOST_VERSION: &str = "0.5.0";
const MAX_PACKAGE_BYTES: u64 = 16 * 1024 * 1024;
const NATIVE_COMPOSITION_NUMBER: &str = "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001";
const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod"
);
const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig"
);
const CHANNEL_WORKBENCH_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001";
const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod"
);
const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig"
);
const PERSONA_CHANNEL_BODY_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod"
);
const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig"
);
const EDUCATION_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001";
const EDUCATION_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod"
);
const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig"
);
const WEB_NOVEL_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001";
const WEB_NOVEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod"
);
const WEB_NOVEL_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig"
);
const WEB_NOVEL_OUTLINE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001";
const WEB_NOVEL_OUTLINE_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod"
);
const WEB_NOVEL_OUTLINE_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig"
);
const WEB_NOVEL_GRID_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001";
const WEB_NOVEL_GRID_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod"
);
const WEB_NOVEL_GRID_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig"
);
const WEB_NOVEL_STORYWORLD_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001";
const WEB_NOVEL_STORYWORLD_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod"
);
const WEB_NOVEL_STORYWORLD_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig"
);
const WEB_NOVEL_DELIVERY_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001";
const WEB_NOVEL_DELIVERY_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod"
);
const WEB_NOVEL_DELIVERY_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig"
);
const MOBILE_SYNC_NUMBER: &str = "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001";
const MOBILE_SYNC_PACKAGE: &[u8] =
include_bytes!("../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod");
const MOBILE_SYNC_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig"
);
const DYNAMIC_WORLD_SURFACE_NUMBER: &str = "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001";
const DYNAMIC_WORLD_SURFACE_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod"
);
const DYNAMIC_WORLD_SURFACE_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod.sig"
);
struct BundledModuleSource {
module_number: &'static str,
package: &'static [u8],
signature: &'static str,
}
const BUNDLED_MODULES: &[BundledModuleSource] = &[
BundledModuleSource {
module_number: NATIVE_COMPOSITION_NUMBER,
package: NATIVE_COMPOSITION_PACKAGE,
signature: NATIVE_COMPOSITION_SIGNATURE,
},
BundledModuleSource {
module_number: CHANNEL_WORKBENCH_NUMBER,
package: CHANNEL_WORKBENCH_PACKAGE,
signature: CHANNEL_WORKBENCH_SIGNATURE,
},
BundledModuleSource {
module_number: PERSONA_CHANNEL_BODY_NUMBER,
package: PERSONA_CHANNEL_BODY_PACKAGE,
signature: PERSONA_CHANNEL_BODY_SIGNATURE,
},
BundledModuleSource {
module_number: EDUCATION_WORKBENCH_NUMBER,
package: EDUCATION_WORKBENCH_PACKAGE,
signature: EDUCATION_WORKBENCH_SIGNATURE,
},
BundledModuleSource {
module_number: WEB_NOVEL_WORKBENCH_NUMBER,
package: WEB_NOVEL_WORKBENCH_PACKAGE,
signature: WEB_NOVEL_WORKBENCH_SIGNATURE,
},
BundledModuleSource {
module_number: WEB_NOVEL_OUTLINE_NUMBER,
package: WEB_NOVEL_OUTLINE_PACKAGE,
signature: WEB_NOVEL_OUTLINE_SIGNATURE,
},
BundledModuleSource {
module_number: WEB_NOVEL_GRID_NUMBER,
package: WEB_NOVEL_GRID_PACKAGE,
signature: WEB_NOVEL_GRID_SIGNATURE,
},
BundledModuleSource {
module_number: WEB_NOVEL_STORYWORLD_NUMBER,
package: WEB_NOVEL_STORYWORLD_PACKAGE,
signature: WEB_NOVEL_STORYWORLD_SIGNATURE,
},
BundledModuleSource {
module_number: WEB_NOVEL_DELIVERY_NUMBER,
package: WEB_NOVEL_DELIVERY_PACKAGE,
signature: WEB_NOVEL_DELIVERY_SIGNATURE,
},
BundledModuleSource {
module_number: MOBILE_SYNC_NUMBER,
package: MOBILE_SYNC_PACKAGE,
signature: MOBILE_SYNC_SIGNATURE,
},
BundledModuleSource {
module_number: DYNAMIC_WORLD_SURFACE_NUMBER,
package: DYNAMIC_WORLD_SURFACE_PACKAGE,
signature: DYNAMIC_WORLD_SURFACE_SIGNATURE,
},
];
#[derive(Debug, Deserialize)]
struct RuntimeContract {
schema: String,
record_id: String,
package_schema: String,
host: HostContract,
registered_adapters: Vec<String>,
lifecycle: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct HostContract {
version: String,
maximum_package_bytes: u64,
arbitrary_native_code: bool,
arbitrary_webview_javascript: bool,
declarative_payload_only: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ModulePackage {
schema: String,
manifest: ModuleManifest,
payload: Value,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ModuleManifest {
module_number: String,
registration_class: String,
display_name: String,
version: String,
minimum_host_version: String,
adapter: String,
content_digest: String,
permissions: Vec<String>,
user_data_schema: String,
self_test: ModuleSelfTest,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ModuleSelfTest {
kind: String,
expected_content_digest: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VerifyModulePackageInput {
pub(crate) package_path: String,
pub(crate) signature_path: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct InstallModulePackageInput {
pub(crate) package_path: String,
pub(crate) signature_path: String,
pub(crate) expected_package_sha256: String,
pub(crate) human_confirmed_permission_expansion: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ModuleNumberInput {
pub(crate) module_number: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifiedModulePackage {
pub(crate) state: String,
pub(crate) package_sha256: String,
pub(crate) module_number: String,
pub(crate) display_name: String,
pub(crate) version: String,
pub(crate) adapter: String,
pub(crate) permissions: Vec<String>,
pub(crate) signature_verified: bool,
pub(crate) package_code_executed: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InstalledModule {
pub(crate) module_number: String,
pub(crate) display_name: String,
pub(crate) version: String,
pub(crate) adapter: String,
pub(crate) state: String,
pub(crate) package_sha256: String,
pub(crate) previous_package_sha256: Option<String>,
pub(crate) permissions: Vec<String>,
pub(crate) user_data_preserved: bool,
pub(crate) updated_at_unix_ms: u64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleRuntimeSnapshot {
pub(crate) schema: &'static str,
pub(crate) state: &'static str,
pub(crate) host_version: &'static str,
pub(crate) package_code_executed: bool,
pub(crate) modules: Vec<InstalledModule>,
pub(crate) receipt_count: u64,
pub(crate) latest_receipt_hash: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModuleMutationReceipt {
receipt_id: String,
event: String,
module_number: String,
state: String,
package_sha256: String,
previous_receipt_hash: String,
receipt_hash: String,
observed_at_unix_ms: u64,
user_data_preserved: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ActivateBundledModuleInput {
module_number: String,
human_confirmed_permission_expansion: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BundledModuleDescriptor {
module_number: String,
display_name: String,
version: String,
adapter: String,
permissions: Vec<String>,
package_sha256: String,
installed_state: String,
signature_verified: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BundledModuleActivation {
schema: &'static str,
state: String,
module_number: String,
receipts: Vec<ModuleMutationReceipt>,
snapshot: ModuleRuntimeSnapshot,
}
fn now_unix_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0)
}
fn sha256_hex(bytes: &[u8]) -> String {
digest(&SHA256, bytes)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn is_sha256(value: &str) -> bool {
value.len() == 64 && value.chars().all(|item| item.is_ascii_hexdigit())
}
fn canonical_value(value: &Value) -> Value {
match value {
Value::Array(values) => Value::Array(values.iter().map(canonical_value).collect()),
Value::Object(object) => {
let mut keys = object.keys().collect::<Vec<_>>();
keys.sort();
let mut canonical = Map::new();
for key in keys {
canonical.insert(key.clone(), canonical_value(&object[key]));
}
Value::Object(canonical)
}
_ => value.clone(),
}
}
fn payload_digest(value: &Value) -> Result<String, String> {
let bytes = serde_json::to_vec(&canonical_value(value))
.map_err(|error| format!("HOLOLAKE_MODULE_PAYLOAD_INVALID: {error}"))?;
Ok(sha256_hex(&bytes))
}
fn contract() -> Result<RuntimeContract, String> {
let contract: RuntimeContract = serde_json::from_str(CONTRACT_RAW)
.map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_CONTRACT_INVALID: {error}"))?;
if contract.schema != CONTRACT_SCHEMA
|| contract.record_id != "HLP-MODULE-PACKAGE-RUNTIME-001"
|| contract.package_schema != PACKAGE_SCHEMA
|| contract.host.version != HOST_VERSION
|| contract.host.maximum_package_bytes != MAX_PACKAGE_BYTES
|| contract.host.arbitrary_native_code
|| contract.host.arbitrary_webview_javascript
|| !contract.host.declarative_payload_only
|| contract.registered_adapters.len() != 7
|| contract.lifecycle.len() != 6
{
return Err("HOLOLAKE_MODULE_RUNTIME_CONTRACT_BOUNDARY_INVALID".into());
}
Ok(contract)
}
fn decode_outer_base64(value: &str, label: &str) -> Result<String, String> {
let bytes = STANDARD
.decode(value.trim())
.map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?;
String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}"))
}
fn verify_signature_raw(
package: &[u8],
signature_raw: &str,
trust_raw: &str,
) -> Result<(), String> {
let trust: Value = serde_json::from_str(trust_raw)
.map_err(|error| format!("HOLOLAKE_MODULE_TRUST_INVALID: {error}"))?;
if trust.get("state").and_then(Value::as_str) != Some("PROVISIONED") {
return Err("HOLOLAKE_MODULE_TRUST_UNPROVISIONED".into());
}
let encoded_key = trust
.get("publicKey")
.and_then(Value::as_str)
.ok_or("HOLOLAKE_MODULE_TRUST_KEY_MISSING")?;
let public_key = PublicKey::decode(&decode_outer_base64(encoded_key, "HOLOLAKE_MODULE_KEY")?)
.map_err(|error| format!("HOLOLAKE_MODULE_KEY_INVALID: {error}"))?;
let signature = Signature::decode(&decode_outer_base64(
signature_raw,
"HOLOLAKE_MODULE_SIGNATURE",
)?)
.map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_INVALID: {error}"))?;
public_key
.verify(package, &signature, true)
.map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_REJECTED: {error}"))
}
fn valid_number(value: &str, registration_class: &str) -> bool {
let prefix = match registration_class {
"OFFICIAL_LIGHTHOUSE" => "HLP-MOD-OFFICIAL-",
"PRIVATE_CHANNEL_LOCAL" => "HLP-MOD-LOCAL-",
_ => return false,
};
value.starts_with(prefix)
&& value.len() <= 96
&& value
.chars()
.all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-')
}
fn version_triplet(value: &str) -> Option<(u64, u64, u64)> {
let parts = value.split('.').collect::<Vec<_>>();
if parts.len() != 3 {
return None;
}
Some((
parts[0].parse().ok()?,
parts[1].parse().ok()?,
parts[2].parse().ok()?,
))
}
fn validate_package(package: &ModulePackage, contract: &RuntimeContract) -> Result<(), String> {
let manifest = &package.manifest;
let permissions = manifest.permissions.iter().collect::<BTreeSet<_>>();
if package.schema != PACKAGE_SCHEMA
|| !valid_number(&manifest.module_number, &manifest.registration_class)
|| manifest.display_name.trim().is_empty()
|| manifest.display_name.len() > 120
|| version_triplet(&manifest.version).is_none()
|| version_triplet(&manifest.minimum_host_version).is_none()
|| version_triplet(&manifest.minimum_host_version) > version_triplet(HOST_VERSION)
|| !contract.registered_adapters.contains(&manifest.adapter)
|| !is_sha256(&manifest.content_digest)
|| permissions.len() != manifest.permissions.len()
|| manifest.permissions.len() > 32
|| manifest.permissions.iter().any(|permission| {
permission.is_empty()
|| permission.len() > 80
|| !permission
.chars()
.all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '_')
})
|| manifest.user_data_schema.trim().is_empty()
|| manifest.self_test.kind != "DECLARATIVE_SCHEMA_V1"
|| manifest.self_test.expected_content_digest != manifest.content_digest
|| !package.payload.is_object()
|| payload_digest(&package.payload)? != manifest.content_digest
{
return Err("HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID".into());
}
Ok(())
}
fn read_verified_package(
package_path: &Path,
signature_path: &Path,
trust_raw: &str,
) -> Result<(Vec<u8>, ModulePackage, String), String> {
let metadata = fs::symlink_metadata(package_path)
.map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_METADATA_FAILED: {error}"))?;
let signature_metadata = fs::symlink_metadata(signature_path)
.map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_METADATA_FAILED: {error}"))?;
if metadata.file_type().is_symlink()
|| signature_metadata.file_type().is_symlink()
|| !metadata.is_file()
|| !signature_metadata.is_file()
|| metadata.len() == 0
|| metadata.len() > MAX_PACKAGE_BYTES
|| signature_metadata.len() == 0
|| signature_metadata.len() > 16 * 1024
{
return Err("HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID".into());
}
let bytes = fs::read(package_path)
.map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_READ_FAILED: {error}"))?;
let signature_raw = fs::read_to_string(signature_path)
.map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_READ_FAILED: {error}"))?;
read_verified_package_bytes(&bytes, &signature_raw, trust_raw)
}
fn read_verified_package_bytes(
bytes: &[u8],
signature_raw: &str,
trust_raw: &str,
) -> Result<(Vec<u8>, ModulePackage, String), String> {
verify_signature_raw(bytes, signature_raw, trust_raw)?;
let package: ModulePackage = serde_json::from_slice(bytes)
.map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_JSON_INVALID: {error}"))?;
validate_package(&package, &contract()?)?;
let package_sha256 = sha256_hex(bytes);
Ok((bytes.to_vec(), package, package_sha256))
}
fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> {
BUNDLED_MODULES
.iter()
.find(|source| source.module_number == module_number)
.map(|source| (source.package, source.signature))
.ok_or_else(|| "HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into())
}
fn runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
crate::authenticated_storage::account_storage_root(app, "module-runtime-v1")
}
fn open_db(root: &Path) -> Result<Connection, String> {
fs::create_dir_all(root)
.map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_ROOT_FAILED: {error}"))?;
let connection = Connection::open(root.join("module-runtime.sqlite3"))
.map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_OPEN_FAILED: {error}"))?;
connection
.busy_timeout(std::time::Duration::from_secs(5))
.map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_TIMEOUT_FAILED: {error}"))?;
connection
.execute_batch(
"PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS installed_modules(
module_number TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
version TEXT NOT NULL,
adapter TEXT NOT NULL,
state TEXT NOT NULL,
package_sha256 TEXT NOT NULL,
previous_package_sha256 TEXT,
permissions_json TEXT NOT NULL,
manifest_json TEXT NOT NULL,
updated_at_unix_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS module_receipts(
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_id TEXT NOT NULL UNIQUE,
event TEXT NOT NULL,
module_number TEXT NOT NULL,
state TEXT NOT NULL,
package_sha256 TEXT NOT NULL,
previous_receipt_hash TEXT NOT NULL,
receipt_hash TEXT NOT NULL,
observed_at_unix_ms INTEGER NOT NULL
);",
)
.map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_SCHEMA_FAILED: {error}"))?;
Ok(connection)
}
fn known_state(state: &str) -> bool {
matches!(
state,
"INSTALLED_DORMANT"
| "MOUNTED_PENDING_SELF_TEST"
| "ACTIVE"
| "DORMANT"
| "ROLLBACK_PENDING_SELF_TEST"
| "FAILED_CLOSED"
)
}
fn verify_receipt_chain(connection: &Connection) -> Result<(), String> {
let mut statement = connection
.prepare(
"SELECT receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM module_receipts ORDER BY sequence",
)
.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_PREPARE_FAILED: {error}"))?;
let rows = statement
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, String>(4)?,
row.get::<_, String>(5)?,
row.get::<_, String>(6)?,
row.get::<_, u64>(7)?,
))
})
.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_QUERY_FAILED: {error}"))?;
let mut previous = "0".repeat(64);
for row in rows {
let (receipt_id, event, module_number, state, package_sha256, linked, stored, observed) =
row.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_ROW_FAILED: {error}"))?;
let expected = sha256_hex(
format!(
"HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{linked}\n{observed}"
)
.as_bytes(),
);
if linked != previous
|| stored != expected
|| !is_sha256(&stored)
|| !is_sha256(&package_sha256)
|| !known_state(&state)
{
return Err("HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID".into());
}
previous = stored;
}
Ok(())
}
fn verify_persisted_modules(connection: &Connection, root: &Path) -> Result<(), String> {
let mut statement = connection
.prepare(
"SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json FROM installed_modules ORDER BY module_number",
)
.map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_PREPARE_FAILED: {error}"))?;
let rows = statement
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, String>(4)?,
row.get::<_, String>(5)?,
row.get::<_, Option<String>>(6)?,
row.get::<_, String>(7)?,
row.get::<_, String>(8)?,
))
})
.map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_QUERY_FAILED: {error}"))?;
for row in rows {
let (
number,
display,
version,
adapter,
state,
digest,
previous,
permissions_raw,
manifest_raw,
) = row.map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_ROW_FAILED: {error}"))?;
if !known_state(&state) || !is_sha256(&digest) {
return Err("HOLOLAKE_MODULE_PERSISTED_STATE_INVALID".into());
}
let manifest: ModuleManifest = serde_json::from_str(&manifest_raw)
.map_err(|_| "HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID".to_string())?;
let permissions: Vec<String> = serde_json::from_str(&permissions_raw)
.map_err(|_| "HOLOLAKE_MODULE_PERSISTED_PERMISSIONS_INVALID".to_string())?;
let (_, package, verified_digest) = read_verified_package(
&package_file(root, &digest),
&signature_file(root, &digest),
RELEASE_TRUST_RAW,
)?;
if verified_digest != digest
|| package.manifest != manifest
|| manifest.module_number != number
|| manifest.display_name != display
|| manifest.version != version
|| manifest.adapter != adapter
|| manifest.permissions != permissions
{
return Err("HOLOLAKE_MODULE_PERSISTED_IDENTITY_INVALID".into());
}
if let Some(previous) = previous {
if !is_sha256(&previous) {
return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_INVALID".into());
}
let (_, previous_package, previous_digest) = read_verified_package(
&package_file(root, &previous),
&signature_file(root, &previous),
RELEASE_TRUST_RAW,
)?;
if previous_digest != previous || previous_package.manifest.module_number != number {
return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_IDENTITY_INVALID".into());
}
}
}
Ok(())
}
fn package_file(root: &Path, digest: &str) -> PathBuf {
root.join("packages").join(format!("{digest}.ghmod"))
}
fn signature_file(root: &Path, digest: &str) -> PathBuf {
root.join("packages").join(format!("{digest}.ghmod.sig"))
}
fn copy_atomic(source: &Path, target: &Path) -> Result<(), String> {
let parent = target
.parent()
.ok_or("HOLOLAKE_MODULE_PACKAGE_TARGET_INVALID")?;
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_STORE_FAILED: {error}"))?;
if target.exists() {
return Ok(());
}
let temporary = parent.join(format!(".install-{}", Uuid::new_v4()));
fs::copy(source, &temporary)
.map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COPY_FAILED: {error}"))?;
fs::rename(&temporary, target)
.map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COMMIT_FAILED: {error}"))
}
fn previous_receipt_hash(transaction: &Transaction<'_>) -> Result<String, String> {
transaction
.query_row(
"SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1",
[],
|row| row.get(0),
)
.optional()
.map(|value| value.unwrap_or_else(|| "0".repeat(64)))
.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_READ_FAILED: {error}"))
}
fn append_receipt(
transaction: &Transaction<'_>,
event: &str,
module_number: &str,
state: &str,
package_sha256: &str,
observed_at_unix_ms: u64,
) -> Result<ModuleMutationReceipt, String> {
let receipt_id = format!("HLP-MOD-RCPT-{}", Uuid::new_v4());
let previous = previous_receipt_hash(transaction)?;
let receipt_hash = sha256_hex(
format!(
"HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{previous}\n{observed_at_unix_ms}"
)
.as_bytes(),
);
transaction
.execute(
"INSERT INTO module_receipts(receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms)
VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
params![receipt_id, event, module_number, state, package_sha256, previous, receipt_hash, observed_at_unix_ms],
)
.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_WRITE_FAILED: {error}"))?;
Ok(ModuleMutationReceipt {
receipt_id,
event: event.into(),
module_number: module_number.into(),
state: state.into(),
package_sha256: package_sha256.into(),
previous_receipt_hash: previous,
receipt_hash,
observed_at_unix_ms,
user_data_preserved: true,
})
}
fn permissions_from_json(raw: &str) -> BTreeSet<String> {
serde_json::from_str::<Vec<String>>(raw)
.unwrap_or_default()
.into_iter()
.collect()
}
fn materialize_bundled_source(
root: &Path,
module_number: &str,
package: &[u8],
signature: &str,
) -> Result<(PathBuf, PathBuf), String> {
let source_root = root.join("bundled-sources");
fs::create_dir_all(&source_root)
.map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_ROOT_FAILED: {error}"))?;
let digest = sha256_hex(package);
let package_path = source_root.join(format!("{module_number}-{digest}.ghmod"));
let signature_path = source_root.join(format!("{module_number}-{digest}.ghmod.sig"));
for (path, bytes) in [
(&package_path, package),
(&signature_path, signature.as_bytes()),
] {
if path.exists() {
let current = fs::read(path)
.map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_READ_FAILED: {error}"))?;
if current != bytes {
return Err("HOLOLAKE_BUNDLED_MODULE_SOURCE_TAMPERED".into());
}
continue;
}
let temporary = source_root.join(format!(".bundled-{}", Uuid::new_v4()));
fs::write(&temporary, bytes)
.map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_WRITE_FAILED: {error}"))?;
fs::rename(&temporary, path)
.map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_COMMIT_FAILED: {error}"))?;
}
Ok((package_path, signature_path))
}
pub async fn get_bundled_module_catalog(
app: AppHandle,
) -> Result<Vec<BundledModuleDescriptor>, String> {
let root = runtime_root(&app)?;
let snapshot = snapshot_at(&root)?;
BUNDLED_MODULES
.iter()
.map(|source| {
let (_, package, package_sha256) =
read_verified_package_bytes(source.package, source.signature, RELEASE_TRUST_RAW)?;
if package.manifest.module_number != source.module_number {
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
}
let installed_state = snapshot
.modules
.iter()
.find(|module| module.module_number == source.module_number)
.map(|module| module.state.clone())
.unwrap_or_else(|| "NOT_INSTALLED".into());
Ok(BundledModuleDescriptor {
module_number: package.manifest.module_number,
display_name: package.manifest.display_name,
version: package.manifest.version,
adapter: package.manifest.adapter,
permissions: package.manifest.permissions,
package_sha256,
installed_state,
signature_verified: true,
})
})
.collect()
}
pub async fn activate_bundled_module(
app: AppHandle,
input: ActivateBundledModuleInput,
) -> Result<BundledModuleActivation, String> {
let root = runtime_root(&app)?;
let (bytes, signature) = bundled_package(&input.module_number)?;
let (_, package, package_sha256) =
read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?;
if package.manifest.module_number != input.module_number {
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
}
let mut receipts = Vec::new();
let before = snapshot_at(&root)?;
let installed = before
.modules
.iter()
.find(|module| module.module_number == input.module_number);
if installed
.is_some_and(|module| module.state == "ACTIVE" && module.package_sha256 == package_sha256)
{
return Ok(BundledModuleActivation {
schema: "hololake.bundled-module-activation/v1",
state: "ALREADY_ACTIVE".into(),
module_number: input.module_number,
receipts,
snapshot: before,
});
}
let needs_install = installed
.map(|module| module.package_sha256 != package_sha256 || module.state == "FAILED_CLOSED")
.unwrap_or(true);
if needs_install {
let (package_path, signature_path) =
materialize_bundled_source(&root, &input.module_number, bytes, signature)?;
receipts.push(install_module_package_at(
&root,
&InstallModulePackageInput {
package_path: package_path.to_string_lossy().into_owned(),
signature_path: signature_path.to_string_lossy().into_owned(),
expected_package_sha256: package_sha256,
human_confirmed_permission_expansion: input.human_confirmed_permission_expansion,
},
RELEASE_TRUST_RAW,
now_unix_ms(),
)?);
}
let current = snapshot_at(&root)?
.modules
.into_iter()
.find(|module| module.module_number == input.module_number)
.ok_or("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_MISSING")?;
match current.state.as_str() {
"INSTALLED_DORMANT" | "DORMANT" => {
receipts.push(transition(
&root,
&input.module_number,
&["INSTALLED_DORMANT", "DORMANT"],
"MOUNTED_PENDING_SELF_TEST",
"MOUNT",
)?);
receipts.push(self_test_module_at(
&root,
&input.module_number,
now_unix_ms(),
)?);
}
"MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" => {
receipts.push(self_test_module_at(
&root,
&input.module_number,
now_unix_ms(),
)?);
}
"ACTIVE" => {}
_ => return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_INVALID".into()),
}
let snapshot = snapshot_at(&root)?;
if !snapshot.modules.iter().any(|module| {
module.module_number == input.module_number
&& module.adapter == package.manifest.adapter
&& module.state == "ACTIVE"
}) {
return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_NOT_ACTIVE".into());
}
Ok(BundledModuleActivation {
schema: "hololake.bundled-module-activation/v1",
state: "ACTIVE".into(),
module_number: input.module_number,
receipts,
snapshot,
})
}
pub(crate) fn require_active_module_adapter(
app: &AppHandle,
module_number: &str,
adapter: &str,
) -> Result<(), String> {
let snapshot = snapshot_at(&runtime_root(app)?)?;
if snapshot.modules.iter().any(|module| {
module.module_number == module_number
&& module.adapter == adapter
&& module.state == "ACTIVE"
}) {
Ok(())
} else {
Err("HOLOLAKE_MODULE_NOT_ACTIVE".into())
}
}
pub async fn verify_module_package(
app: AppHandle,
input: VerifyModulePackageInput,
) -> Result<VerifiedModulePackage, String> {
let _ = runtime_root(&app)?;
let (_, package, package_sha256) = read_verified_package(
Path::new(&input.package_path),
Path::new(&input.signature_path),
RELEASE_TRUST_RAW,
)?;
Ok(VerifiedModulePackage {
state: "VERIFIED_NOT_INSTALLED".into(),
package_sha256,
module_number: package.manifest.module_number,
display_name: package.manifest.display_name,
version: package.manifest.version,
adapter: package.manifest.adapter,
permissions: package.manifest.permissions,
signature_verified: true,
package_code_executed: false,
})
}
pub async fn install_module_package(
app: AppHandle,
input: InstallModulePackageInput,
) -> Result<ModuleMutationReceipt, String> {
install_module_package_at(
&runtime_root(&app)?,
&input,
RELEASE_TRUST_RAW,
now_unix_ms(),
)
}
fn install_module_package_at(
root: &Path,
input: &InstallModulePackageInput,
trust_raw: &str,
observed: u64,
) -> Result<ModuleMutationReceipt, String> {
if !is_sha256(&input.expected_package_sha256) {
return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_INVALID".into());
}
let (bytes, package, digest) = read_verified_package(
Path::new(&input.package_path),
Path::new(&input.signature_path),
trust_raw,
)?;
if digest != input.expected_package_sha256 {
return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_MISMATCH".into());
}
let mut connection = open_db(root)?;
let existing: Option<(String, String)> = connection
.query_row(
"SELECT package_sha256, permissions_json FROM installed_modules WHERE module_number=?1",
params![package.manifest.module_number],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.map_err(|error| format!("HOLOLAKE_MODULE_EXISTING_READ_FAILED: {error}"))?;
let requested = package
.manifest
.permissions
.iter()
.cloned()
.collect::<BTreeSet<_>>();
let expansion = existing
.as_ref()
.map(|(_, raw)| !requested.is_subset(&permissions_from_json(raw)))
.unwrap_or(!requested.is_empty());
if expansion && !input.human_confirmed_permission_expansion {
return Err("HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION".into());
}
copy_atomic(Path::new(&input.package_path), &package_file(root, &digest))?;
copy_atomic(
Path::new(&input.signature_path),
&signature_file(root, &digest),
)?;
if fs::read(package_file(root, &digest))
.map_err(|error| format!("HOLOLAKE_MODULE_STORED_PACKAGE_READ_FAILED: {error}"))?
!= bytes
{
return Err("HOLOLAKE_MODULE_STORED_PACKAGE_MISMATCH".into());
}
if fs::read(signature_file(root, &digest))
.map_err(|error| format!("HOLOLAKE_MODULE_STORED_SIGNATURE_READ_FAILED: {error}"))?
!= fs::read(&input.signature_path)
.map_err(|error| format!("HOLOLAKE_MODULE_SOURCE_SIGNATURE_READ_FAILED: {error}"))?
{
return Err("HOLOLAKE_MODULE_STORED_SIGNATURE_MISMATCH".into());
}
fs::create_dir_all(root.join("user-data").join(&package.manifest.module_number))
.map_err(|error| format!("HOLOLAKE_MODULE_USER_DATA_ROOT_FAILED: {error}"))?;
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_TRANSACTION_FAILED: {error}"))?;
let permissions_json = serde_json::to_string(&package.manifest.permissions)
.map_err(|error| format!("HOLOLAKE_MODULE_PERMISSIONS_INVALID: {error}"))?;
let manifest_json = serde_json::to_string(&package.manifest)
.map_err(|error| format!("HOLOLAKE_MODULE_MANIFEST_INVALID: {error}"))?;
let previous = existing.as_ref().map(|(value, _)| value.as_str());
transaction
.execute(
"INSERT INTO installed_modules(module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json,updated_at_unix_ms)
VALUES(?1,?2,?3,?4,'INSTALLED_DORMANT',?5,?6,?7,?8,?9)
ON CONFLICT(module_number) DO UPDATE SET display_name=excluded.display_name,version=excluded.version,adapter=excluded.adapter,state='INSTALLED_DORMANT',package_sha256=excluded.package_sha256,previous_package_sha256=installed_modules.package_sha256,permissions_json=excluded.permissions_json,manifest_json=excluded.manifest_json,updated_at_unix_ms=excluded.updated_at_unix_ms",
params![package.manifest.module_number, package.manifest.display_name, package.manifest.version, package.manifest.adapter, digest, previous, permissions_json, manifest_json, observed],
)
.map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(
&transaction,
"INSTALL",
&package.manifest.module_number,
"INSTALLED_DORMANT",
&digest,
observed,
)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_COMMIT_FAILED: {error}"))?;
Ok(receipt)
}
fn transition(
root: &Path,
module_number: &str,
allowed_from: &[&str],
target: &str,
event: &str,
) -> Result<ModuleMutationReceipt, String> {
let mut connection = open_db(root)?;
let row: (String, String) = connection
.query_row(
"SELECT state,package_sha256 FROM installed_modules WHERE module_number=?1",
params![module_number],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.map_err(|error| format!("HOLOLAKE_MODULE_STATE_READ_FAILED: {error}"))?
.ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?;
if !allowed_from.contains(&row.0.as_str()) {
return Err("HOLOLAKE_MODULE_LIFECYCLE_TRANSITION_INVALID".into());
}
let observed = now_unix_ms();
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_BEGIN_FAILED: {error}"))?;
transaction
.execute(
"UPDATE installed_modules SET state=?1,updated_at_unix_ms=?2 WHERE module_number=?3 AND state=?4",
params![target, observed, module_number, row.0],
)
.map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(&transaction, event, module_number, target, &row.1, observed)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_COMMIT_FAILED: {error}"))?;
Ok(receipt)
}
pub async fn mount_module(
app: AppHandle,
input: ModuleNumberInput,
) -> Result<ModuleMutationReceipt, String> {
transition(
&runtime_root(&app)?,
&input.module_number,
&["INSTALLED_DORMANT", "DORMANT"],
"MOUNTED_PENDING_SELF_TEST",
"MOUNT",
)
}
pub async fn self_test_module(
app: AppHandle,
input: ModuleNumberInput,
) -> Result<ModuleMutationReceipt, String> {
self_test_module_at(&runtime_root(&app)?, &input.module_number, now_unix_ms())
}
fn self_test_module_at(
root: &Path,
module_number: &str,
observed: u64,
) -> Result<ModuleMutationReceipt, String> {
let mut connection = open_db(root)?;
let row: (String, String, Option<String>) = connection
.query_row(
"SELECT state,package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1",
params![module_number],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.optional()
.map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_STATE_FAILED: {error}"))?
.ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?;
if !matches!(
row.0.as_str(),
"MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST"
) {
return Err("HOLOLAKE_MODULE_SELF_TEST_STATE_INVALID".into());
}
let verification = read_verified_package(
&package_file(root, &row.1),
&signature_file(root, &row.1),
RELEASE_TRUST_RAW,
);
let success = verification.as_ref().is_ok_and(|(_, package, digest)| {
digest == &row.1 && package.manifest.module_number == module_number
});
let rollback_package = row.2.as_ref().and_then(|previous| {
read_verified_package(
&package_file(root, previous),
&signature_file(root, previous),
RELEASE_TRUST_RAW,
)
.ok()
.filter(|(_, package, digest)| {
digest == previous && package.manifest.module_number == module_number
})
});
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_BEGIN_FAILED: {error}"))?;
if success {
transaction
.execute(
"UPDATE installed_modules SET state='ACTIVE',updated_at_unix_ms=?1 WHERE module_number=?2",
params![observed, module_number],
)
.map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(
&transaction,
"SELF_TEST_PASS",
module_number,
"ACTIVE",
&row.1,
observed,
)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_COMMIT_FAILED: {error}"))?;
return Ok(receipt);
}
let (target_state, target_digest) = if let Some((_, previous_package, previous)) =
rollback_package
{
let permissions_json = serde_json::to_string(&previous_package.manifest.permissions)
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?;
let manifest_json = serde_json::to_string(&previous_package.manifest)
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?;
transaction
.execute(
"UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=NULL,permissions_json=?5,manifest_json=?6,updated_at_unix_ms=?7 WHERE module_number=?8",
params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, permissions_json, manifest_json, observed, module_number],
)
.map_err(|error| format!("HOLOLAKE_MODULE_AUTO_ROLLBACK_WRITE_FAILED: {error}"))?;
("ROLLBACK_PENDING_SELF_TEST", previous)
} else {
transaction
.execute(
"UPDATE installed_modules SET state='FAILED_CLOSED',updated_at_unix_ms=?1 WHERE module_number=?2",
params![observed, module_number],
)
.map_err(|error| format!("HOLOLAKE_MODULE_FAIL_CLOSED_WRITE_FAILED: {error}"))?;
("FAILED_CLOSED", row.1)
};
let _ = append_receipt(
&transaction,
"SELF_TEST_FAIL",
module_number,
target_state,
&target_digest,
observed,
)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_FAILURE_COMMIT_FAILED: {error}"))?;
Err(verification
.err()
.unwrap_or_else(|| "HOLOLAKE_MODULE_SELF_TEST_IDENTITY_MISMATCH".into()))
}
pub async fn unmount_module(
app: AppHandle,
input: ModuleNumberInput,
) -> Result<ModuleMutationReceipt, String> {
if input.module_number == MOBILE_SYNC_NUMBER {
crate::mobile_sync::stop_for_unmount(&app)?;
}
transition(
&runtime_root(&app)?,
&input.module_number,
&["ACTIVE", "MOUNTED_PENDING_SELF_TEST", "FAILED_CLOSED"],
"DORMANT",
"UNMOUNT",
)
}
pub async fn rollback_module(
app: AppHandle,
input: ModuleNumberInput,
) -> Result<ModuleMutationReceipt, String> {
let root = runtime_root(&app)?;
let mut connection = open_db(&root)?;
let row: (String, Option<String>) = connection
.query_row(
"SELECT package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1",
params![input.module_number],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_READ_FAILED: {error}"))?
.ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?;
let previous = row.1.ok_or("HOLOLAKE_MODULE_ROLLBACK_NOT_AVAILABLE")?;
let (_, previous_package, verified_previous) = read_verified_package(
&package_file(&root, &previous),
&signature_file(&root, &previous),
RELEASE_TRUST_RAW,
)?;
if verified_previous != previous
|| previous_package.manifest.module_number != input.module_number
{
return Err("HOLOLAKE_MODULE_ROLLBACK_PACKAGE_IDENTITY_INVALID".into());
}
let permissions_json = serde_json::to_string(&previous_package.manifest.permissions)
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?;
let manifest_json = serde_json::to_string(&previous_package.manifest)
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?;
let observed = now_unix_ms();
let transaction = connection
.transaction()
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_BEGIN_FAILED: {error}"))?;
transaction
.execute(
"UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=?5,permissions_json=?6,manifest_json=?7,updated_at_unix_ms=?8 WHERE module_number=?9",
params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, row.0, permissions_json, manifest_json, observed, input.module_number],
)
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(
&transaction,
"ROLLBACK",
&input.module_number,
"ROLLBACK_PENDING_SELF_TEST",
&previous,
observed,
)?;
transaction
.commit()
.map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_COMMIT_FAILED: {error}"))?;
Ok(receipt)
}
pub async fn get_module_runtime_snapshot(app: AppHandle) -> Result<ModuleRuntimeSnapshot, String> {
let root = runtime_root(&app)?;
snapshot_at(&root)
}
fn snapshot_at(root: &Path) -> Result<ModuleRuntimeSnapshot, String> {
let connection = open_db(root)?;
verify_receipt_chain(&connection)?;
verify_persisted_modules(&connection, root)?;
let mut statement = connection
.prepare(
"SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,updated_at_unix_ms FROM installed_modules ORDER BY module_number",
)
.map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_PREPARE_FAILED: {error}"))?;
let modules = statement
.query_map([], |row| {
let permissions_json: String = row.get(7)?;
Ok(InstalledModule {
module_number: row.get(0)?,
display_name: row.get(1)?,
version: row.get(2)?,
adapter: row.get(3)?,
state: row.get(4)?,
package_sha256: row.get(5)?,
previous_package_sha256: row.get(6)?,
permissions: serde_json::from_str(&permissions_json).unwrap_or_default(),
user_data_preserved: true,
updated_at_unix_ms: row.get(8)?,
})
})
.map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_QUERY_FAILED: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_ROW_FAILED: {error}"))?;
let receipt_count: u64 = connection
.query_row("SELECT COUNT(*) FROM module_receipts", [], |row| row.get(0))
.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_COUNT_FAILED: {error}"))?;
let latest_receipt_hash = connection
.query_row(
"SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1",
[],
|row| row.get(0),
)
.optional()
.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_TAIL_FAILED: {error}"))?
.unwrap_or_else(|| "0".repeat(64));
Ok(ModuleRuntimeSnapshot {
schema: "hololake.module-runtime-snapshot/v1",
state: "ACTIVE_SIGNED_DECLARATIVE_PACKAGES_ONLY",
host_version: HOST_VERSION,
package_code_executed: false,
modules,
receipt_count,
latest_receipt_hash,
})
}
pub(crate) fn start_on_application_open(app: &AppHandle) -> Result<(), String> {
let _ = contract()?;
let root = runtime_root(app);
match root {
Ok(root) => {
let _ = snapshot_at(&root)?;
Ok(())
}
Err(error) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok(()),
Err(error) => Err(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn signed_fixture() -> (PathBuf, PathBuf, String) {
let package = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod");
let signature = package.with_extension("ghmod.sig");
let digest = sha256_hex(&fs::read(&package).unwrap());
(package, signature, digest)
}
fn install_fixture(root: &Path, observed: u64) -> String {
let (package, signature, digest) = signed_fixture();
install_module_package_at(
root,
&InstallModulePackageInput {
package_path: package.to_string_lossy().into_owned(),
signature_path: signature.to_string_lossy().into_owned(),
expected_package_sha256: digest.clone(),
human_confirmed_permission_expansion: true,
},
RELEASE_TRUST_RAW,
observed,
)
.unwrap();
digest
}
#[test]
fn contract_keeps_packages_declarative_and_lifecycle_closed() {
let contract = contract().unwrap();
assert_eq!(contract.registered_adapters.len(), 7);
assert!(!contract.host.arbitrary_native_code);
assert!(!contract.host.arbitrary_webview_javascript);
assert!(contract.host.declarative_payload_only);
}
#[test]
fn package_number_classes_and_content_digest_are_exact() {
let payload = serde_json::json!({"views":[],"entry":"home"});
let digest = payload_digest(&payload).unwrap();
let package = ModulePackage {
schema: PACKAGE_SCHEMA.into(),
manifest: ModuleManifest {
module_number: "HLP-MOD-LOCAL-0001".into(),
registration_class: "PRIVATE_CHANNEL_LOCAL".into(),
display_name: "Local module".into(),
version: "1.0.0".into(),
minimum_host_version: "0.5.0".into(),
adapter: "native-composition-v1".into(),
content_digest: digest.clone(),
permissions: vec!["KNOWLEDGE_READ".into()],
user_data_schema: "hololake.module-data/local/v1".into(),
self_test: ModuleSelfTest {
kind: "DECLARATIVE_SCHEMA_V1".into(),
expected_content_digest: digest,
},
},
payload,
};
let runtime_contract = contract().unwrap();
validate_package(&package, &runtime_contract).unwrap();
let mut invalid = package;
invalid.manifest.module_number = "HLP-DONOR-CAND-0001".into();
assert_eq!(
validate_package(&invalid, &runtime_contract).unwrap_err(),
"HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID"
);
}
#[test]
fn tampered_persisted_manifest_fails_closed_without_touching_user_data() {
let temporary = tempfile::tempdir().unwrap();
let root = temporary.path();
let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001";
install_fixture(root, 1);
fs::write(
root.join("user-data")
.join(module_number)
.join("human-data.json"),
b"preserve",
)
.unwrap();
let connection = open_db(root).unwrap();
connection
.execute(
"UPDATE installed_modules SET manifest_json='{}' WHERE module_number=?1",
params![module_number],
)
.unwrap();
drop(connection);
assert!(snapshot_at(root)
.unwrap_err()
.starts_with("HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID"));
assert_eq!(
fs::read(
root.join("user-data")
.join(module_number)
.join("human-data.json")
)
.unwrap(),
b"preserve"
);
}
#[test]
fn tampered_receipt_chain_fails_closed_on_restart() {
let temporary = tempfile::tempdir().unwrap();
install_fixture(temporary.path(), 1);
let connection = open_db(temporary.path()).unwrap();
connection
.execute(
"UPDATE module_receipts SET receipt_hash=?1 WHERE sequence=1",
params!["f".repeat(64)],
)
.unwrap();
drop(connection);
assert_eq!(
snapshot_at(temporary.path()).unwrap_err(),
"HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID"
);
}
#[test]
fn real_product_signed_fixture_verifies_without_executing_package_code() {
let (fixture, signature, _) = signed_fixture();
let (_, package, package_sha256) =
read_verified_package(&fixture, &signature, RELEASE_TRUST_RAW).unwrap();
assert_eq!(
package.manifest.module_number,
"HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"
);
assert!(is_sha256(&package_sha256));
}
#[test]
fn signed_package_install_mount_self_test_restart_and_unmount_are_receipted() {
let (fixture, signature, expected) = signed_fixture();
let temporary = tempfile::tempdir().unwrap();
let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001";
let mut input = InstallModulePackageInput {
package_path: fixture.to_string_lossy().into_owned(),
signature_path: signature.to_string_lossy().into_owned(),
expected_package_sha256: expected,
human_confirmed_permission_expansion: false,
};
assert_eq!(
install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 100)
.unwrap_err(),
"HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION"
);
input.human_confirmed_permission_expansion = true;
install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 101).unwrap();
transition(
temporary.path(),
module_number,
&["INSTALLED_DORMANT"],
"MOUNTED_PENDING_SELF_TEST",
"MOUNT",
)
.unwrap();
self_test_module_at(temporary.path(), module_number, 103).unwrap();
let user_data = temporary
.path()
.join("user-data")
.join(module_number)
.join("human-data.json");
fs::write(&user_data, b"preserve-after-unmount").unwrap();
transition(
temporary.path(),
module_number,
&["ACTIVE"],
"DORMANT",
"UNMOUNT",
)
.unwrap();
let reopened = snapshot_at(temporary.path()).unwrap();
assert_eq!(reopened.modules.len(), 1);
assert_eq!(reopened.modules[0].state, "DORMANT");
assert_eq!(reopened.receipt_count, 4);
assert_eq!(fs::read(user_data).unwrap(), b"preserve-after-unmount");
}
#[test]
fn bundled_native_composition_package_completes_installed_lifecycle() {
let temporary = tempfile::tempdir().unwrap();
let (_, package, digest) = read_verified_package_bytes(
NATIVE_COMPOSITION_PACKAGE,
NATIVE_COMPOSITION_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, NATIVE_COMPOSITION_NUMBER);
assert_eq!(package.manifest.adapter, "native-composition-v1");
let (package_path, signature_path) = materialize_bundled_source(
temporary.path(),
NATIVE_COMPOSITION_NUMBER,
NATIVE_COMPOSITION_PACKAGE,
NATIVE_COMPOSITION_SIGNATURE,
)
.unwrap();
install_module_package_at(
temporary.path(),
&InstallModulePackageInput {
package_path: package_path.to_string_lossy().into_owned(),
signature_path: signature_path.to_string_lossy().into_owned(),
expected_package_sha256: digest,
human_confirmed_permission_expansion: true,
},
RELEASE_TRUST_RAW,
200,
)
.unwrap();
transition(
temporary.path(),
NATIVE_COMPOSITION_NUMBER,
&["INSTALLED_DORMANT"],
"MOUNTED_PENDING_SELF_TEST",
"MOUNT",
)
.unwrap();
self_test_module_at(temporary.path(), NATIVE_COMPOSITION_NUMBER, 202).unwrap();
let snapshot = snapshot_at(temporary.path()).unwrap();
assert!(snapshot.modules.iter().any(|module| {
module.module_number == NATIVE_COMPOSITION_NUMBER
&& module.adapter == "native-composition-v1"
&& module.state == "ACTIVE"
}));
assert_eq!(snapshot.receipt_count, 3);
}
#[test]
fn bundled_channel_workbench_has_an_independent_verified_identity() {
let (_, package, digest) = read_verified_package_bytes(
CHANNEL_WORKBENCH_PACKAGE,
CHANNEL_WORKBENCH_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, CHANNEL_WORKBENCH_NUMBER);
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 4);
assert!(is_sha256(&digest));
assert_eq!(BUNDLED_MODULES.len(), 11);
}
#[test]
fn bundled_persona_channel_body_is_signed_without_claiming_persona_binding() {
let (_, package, digest) = read_verified_package_bytes(
PERSONA_CHANNEL_BODY_PACKAGE,
PERSONA_CHANNEL_BODY_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, PERSONA_CHANNEL_BODY_NUMBER);
assert_eq!(package.manifest.adapter, "persona-channel-body-v1");
assert_eq!(package.manifest.permissions.len(), 7);
assert_eq!(
package.payload["adapterConfig"]["personaBindingClaimed"],
false
);
assert!(is_sha256(&digest));
}
#[test]
fn bundled_education_workbench_is_an_official_signed_module() {
let (_, package, digest) = read_verified_package_bytes(
EDUCATION_WORKBENCH_PACKAGE,
EDUCATION_WORKBENCH_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, EDUCATION_WORKBENCH_NUMBER);
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
assert_eq!(package.manifest.adapter, "education-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 8);
assert_eq!(
package.payload["adapterConfig"]["importDefaultScope"],
"UNASSIGNED"
);
assert!(is_sha256(&digest));
}
#[test]
fn bundled_web_novel_family_uses_one_signed_lifecycle_and_exact_numbers() {
let expected = [
(
WEB_NOVEL_WORKBENCH_NUMBER,
WEB_NOVEL_WORKBENCH_PACKAGE,
WEB_NOVEL_WORKBENCH_SIGNATURE,
8usize,
),
(
WEB_NOVEL_OUTLINE_NUMBER,
WEB_NOVEL_OUTLINE_PACKAGE,
WEB_NOVEL_OUTLINE_SIGNATURE,
2usize,
),
(
WEB_NOVEL_GRID_NUMBER,
WEB_NOVEL_GRID_PACKAGE,
WEB_NOVEL_GRID_SIGNATURE,
2usize,
),
(
WEB_NOVEL_STORYWORLD_NUMBER,
WEB_NOVEL_STORYWORLD_PACKAGE,
WEB_NOVEL_STORYWORLD_SIGNATURE,
2usize,
),
(
WEB_NOVEL_DELIVERY_NUMBER,
WEB_NOVEL_DELIVERY_PACKAGE,
WEB_NOVEL_DELIVERY_SIGNATURE,
2usize,
),
];
for (number, bytes, signature, permission_count) in expected {
let (_, package, digest) =
read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW).unwrap();
assert_eq!(package.manifest.module_number, number);
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
assert_eq!(package.manifest.adapter, "web-novel-workbench-v1");
assert_eq!(package.manifest.permissions.len(), permission_count);
assert!(is_sha256(&digest));
}
}
#[test]
fn bundled_mobile_sync_is_signed_and_keeps_the_desktop_as_root_node() {
let (_, package, digest) = read_verified_package_bytes(
MOBILE_SYNC_PACKAGE,
MOBILE_SYNC_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, MOBILE_SYNC_NUMBER);
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
assert_eq!(package.manifest.adapter, "mobile-sync-v1");
assert_eq!(package.manifest.permissions.len(), 5);
assert_eq!(
package.payload["adapterConfig"]["desktopRole"],
"USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE"
);
assert_eq!(
package.payload["adapterConfig"]["iosClientPackaging"],
"PENDING_SEPARATE_ADMISSION"
);
assert!(is_sha256(&digest));
}
#[test]
fn bundled_dynamic_world_surface_is_signed_and_cannot_change_routes() {
let (_, package, digest) = read_verified_package_bytes(
DYNAMIC_WORLD_SURFACE_PACKAGE,
DYNAMIC_WORLD_SURFACE_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, DYNAMIC_WORLD_SURFACE_NUMBER);
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
assert_eq!(
package.manifest.adapter,
"dynamic-language-world-surface-v1"
);
assert_eq!(package.manifest.permissions.len(), 2);
assert_eq!(
package.payload["adapterConfig"]["routingPermissionOrFactMutation"],
false
);
assert_eq!(
package.payload["adapterConfig"]["legacyTraditionalWorkbench"],
"REJECTED_DUPLICATE_SHELL_AND_SIMULATED_STATE"
);
assert!(is_sha256(&digest));
}
#[cfg(unix)]
#[test]
fn symlinked_package_input_is_rejected_before_signature_processing() {
use std::os::unix::fs::symlink;
let temporary = tempfile::tempdir().unwrap();
let source = temporary.path().join("source.ghmod");
let link = temporary.path().join("link.ghmod");
let signature = temporary.path().join("dummy.sig");
fs::write(&source, b"{}").unwrap();
fs::write(&signature, b"not-a-signature").unwrap();
symlink(&source, &link).unwrap();
assert_eq!(
read_verified_package(&link, &signature, RELEASE_TRUST_RAW).unwrap_err(),
"HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID"
);
}
}