1765 lines
67 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|