//! 持久的验证包暂存。暂存从不启用包或授予权限。 use crate::{ extension_package::Release, workspace::{hash, HostError, Result}, }; use fs2::FileExt; use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; use std::{ fs::{self, File, OpenOptions}, io::{Read, Write}, path::{Path, PathBuf}, }; use uuid::Uuid; pub struct Signer<'a> { pub public_key: &'a [u8; 32], pub key_id: &'a str, pub namespace: &'a str, pub revoked: bool, } pub struct Stage<'a> { pub operation_id: &'a str, pub source: &'a str, pub signer: Signer<'a>, pub release: &'a Release, pub withdrawn: bool, pub archive: &'a [u8], } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Receipt { pub operation_id: String, pub package_key: String, pub archive_sha256: String, pub version: String, pub state: String, } #[derive(Debug, Serialize)] pub struct StagedPackage { pub package_key: String, pub source: String, pub namespace: String, pub package_id: String, pub version: String, pub archive_sha256: String, pub state: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PreparedPackage { pub package_key: String, pub directory: String, pub tree_sha256: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct TrustSetting { pub source: String, pub source_id: String, pub namespace: String, pub key_id: String, pub public_key: [u8; 32], pub enabled: bool, } impl TrustSetting { pub fn fingerprint(&self) -> Result { if source(&self.source)? != self.source || self.source_id.is_empty() || self.source_id.len() > 128 || self.source_id.chars().any(char::is_control) || self.namespace.len() < 2 || self.namespace.len() > 64 || !self .namespace .bytes() .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') || !self.namespace.as_bytes()[0].is_ascii_alphanumeric() || self.key_id.is_empty() || self.key_id.len() > 128 || self.key_id.chars().any(char::is_control) || ed25519_dalek::VerifyingKey::from_bytes(&self.public_key).is_err() { return Err(HostError::new("EXTENSION_TRUST_INVALID")); } Ok(hash(&serde_json::to_vec(self).unwrap())) } } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct InstallRequest { pub root_key: String, pub vault_id: String, pub app_version: String, pub platform: String, pub architecture: String, pub configurations: std::collections::BTreeMap, } #[derive(Debug, Serialize)] pub struct InstallPreview { pub fingerprint: String, pub dependencies: crate::extension_dependencies::Plan, pub changes: Vec, } pub struct RuntimePackage { pub package: cap_std::fs::Dir, pub inventory: crate::extension_package::Inventory, pub release: Release, pub manifest: serde_json::Value, pub active: crate::extension_transaction::Active, pub source: String, pub signer_sha256: String, } pub struct ExtensionStore { root: PathBuf, db: Connection, _lock: File, } fn ordinary(path: &Path) -> Result<()> { let metadata = fs::symlink_metadata(path)?; if metadata.file_type().is_symlink() { return Err(HostError::new("EXTENSION_STORE_UNSAFE")); } #[cfg(windows)] { use std::os::windows::fs::MetadataExt; if metadata.file_attributes() & 0x400 != 0 { return Err(HostError::new("EXTENSION_STORE_UNSAFE")); } } if metadata.is_file() { #[cfg(unix)] { use std::os::unix::fs::MetadataExt; if metadata.nlink() != 1 { return Err(HostError::new("EXTENSION_STORE_UNSAFE")); } } #[cfg(windows)] { use std::os::windows::io::AsRawHandle; use windows_sys::Win32::Storage::FileSystem::{ GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, }; let file = File::open(path)?; let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() }; if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 || info.nNumberOfLinks != 1 { return Err(HostError::new("EXTENSION_STORE_UNSAFE")); } } } Ok(()) } fn source(value: &str) -> Result { let mut url = reqwest::Url::parse(value).map_err(|_| HostError::new("EXTENSION_SOURCE_INVALID"))?; if value.len() > 2048 || url.scheme() != "https" || url.host_str().is_none() || !url.username().is_empty() || url.password().is_some() || url.query().is_some() || url.fragment().is_some() { return Err(HostError::new("EXTENSION_SOURCE_INVALID")); } if !url.path().ends_with('/') { url.set_path(&format!("{}/", url.path())); } Ok(url.to_string()) } impl ExtensionStore { /// 从本地暂存包创建锁定预览。这不会取代在线撤销检查。 pub fn dependency_plan( &self, root_key: &str, app_version: &str, platform: &str, architecture: &str, ) -> Result { use crate::extension_dependencies::{plan, Candidate}; let source: String = self.db.query_row( "SELECT source FROM versions WHERE package_key=?1 AND state='staged'", [root_key], |r| r.get(0), )?; let mut statement=self.db.prepare("SELECT package_key,release,signer FROM versions WHERE source=?1 AND state='staged' ORDER BY package_key LIMIT 4097")?; let mut rows = statement.query([&source])?; let mut candidates = Vec::new(); let mut keys = std::collections::BTreeMap::new(); let mut total = 0usize; while let Some(row) = rows.next()? { let key: String = row.get(0)?; let raw: String = row.get(1)?; let signer: Vec = row.get(2)?; total = total.saturating_add(raw.len()); if raw.len() > 256 * 1024 || total > 64 * 1024 * 1024 || candidates.len() >= 4096 { return Err(HostError::new("EXTENSION_DEPENDENCY_COMPLEXITY")); } let public: [u8; 32] = signer .try_into() .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let release: Release = serde_json::from_str(&raw) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; if key != hash( &serde_json::to_vec(&( &source, &release.namespace, &release.package_id, &release.version, )) .unwrap(), ) { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } keys.insert(key.clone(), public); candidates.push(Candidate { package_key: key, source: source.clone(), release, signer_sha256: hash(&public), }); } let result = plan(&candidates, root_key, app_version, platform, architecture)?; for locked in &result.packages { let candidate = candidates .iter() .find(|c| c.package_key == locked.package_key) .ok_or_else(|| HostError::new("EXTENSION_STORE_CORRUPT"))?; let release = &candidate.release; release.verify_package( &keys[&locked.package_key], &release.key_id, &release.namespace, false, false, &self.archive(&locked.package_key)?, )?; } Ok(result) } pub fn staged(&self, offset: u32, limit: u32) -> Result> { if limit == 0 || limit > 200 { return Err(HostError::new("EXTENSION_PAGE_INVALID")); } let mut statement = self.db.prepare("SELECT package_key,source,namespace,package_id,version,archive_hash,state FROM versions ORDER BY package_key LIMIT ?1 OFFSET ?2")?; let rows = statement.query_map(params![limit, offset], |r| { Ok(StagedPackage { package_key: r.get(0)?, source: r.get(1)?, namespace: r.get(2)?, package_id: r.get(3)?, version: r.get(4)?, archive_sha256: r.get(5)?, state: r.get(6)?, }) })?; Ok(rows.collect::>()?) } /// Host 提供现有应用程序拥有的目录,而不是包提供的路径。 pub fn open(root: &Path) -> Result { ordinary(root)?; if !root.is_dir() { return Err(HostError::new("EXTENSION_STORE_UNSAFE")); } let root = root.canonicalize()?; let lock_path = root.join("extensions.lock"); if lock_path.exists() { ordinary(&lock_path)?; } let lock = OpenOptions::new() .create(true) .truncate(false) .read(true) .write(true) .open(lock_path)?; lock.try_lock_exclusive() .map_err(|_| HostError::new("EXTENSION_STORE_BUSY"))?; let objects = root.join("objects"); if objects.exists() { ordinary(&objects)?; } else { fs::create_dir(&objects)?; } let database = root.join("extensions.sqlite3"); if database.exists() { ordinary(&database)?; } for suffix in ["-wal", "-shm", "-journal"] { let sidecar = root.join(format!("extensions.sqlite3{suffix}")); if sidecar.exists() { ordinary(&sidecar)?; } } let mut db = Connection::open(database)?; db.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;")?; let version: i64 = db.query_row("PRAGMA user_version", [], |r| r.get(0))?; if version > 7 { return Err(HostError::new("EXTENSION_SCHEMA_INCOMPATIBLE")); } if (1..7).contains(&version) { let backup = root.join(format!( "extensions.schema{version}.{}.sqlite3", Uuid::new_v4() )); db.execute("VACUUM INTO ?1", [backup.to_string_lossy().as_ref()])?; OpenOptions::new().write(true).open(backup)?.sync_all()?; } db.execute_batch("BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS versions (package_key TEXT PRIMARY KEY,source TEXT NOT NULL,namespace TEXT NOT NULL,package_id TEXT NOT NULL,version TEXT NOT NULL,fingerprint TEXT NOT NULL,release TEXT NOT NULL,manifest TEXT NOT NULL,inventory TEXT NOT NULL,archive_hash TEXT NOT NULL,size INTEGER NOT NULL,signer BLOB NOT NULL,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS stage_operations (id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,receipt TEXT NOT NULL); CREATE TABLE IF NOT EXISTS prepared_packages (package_key TEXT PRIMARY KEY REFERENCES versions(package_key),directory TEXT NOT NULL UNIQUE,tree_sha256 TEXT NOT NULL); CREATE TABLE IF NOT EXISTS extension_active(slot TEXT PRIMARY KEY,target TEXT NOT NULL,revision TEXT NOT NULL,pending_operation TEXT); CREATE TABLE IF NOT EXISTS extension_transactions(id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,before_state TEXT NOT NULL,after_state TEXT NOT NULL,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS extension_trust(source TEXT NOT NULL,namespace TEXT NOT NULL,key_id TEXT NOT NULL,setting TEXT NOT NULL,revision TEXT NOT NULL,PRIMARY KEY(source,namespace,key_id)); CREATE TABLE IF NOT EXISTS extension_blocks(identity TEXT PRIMARY KEY,reason TEXT NOT NULL); CREATE TABLE IF NOT EXISTS extension_confirmations(operation_id TEXT PRIMARY KEY,request_hash TEXT NOT NULL,review_hash TEXT NOT NULL,changes TEXT NOT NULL); CREATE TABLE IF NOT EXISTS extension_uninstalls(id TEXT PRIMARY KEY,slot TEXT NOT NULL,expected_revision TEXT NOT NULL,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS legacy_installations(kind TEXT NOT NULL,package_id TEXT NOT NULL,source_path TEXT NOT NULL,expected_digest TEXT NOT NULL,observed_digest TEXT,ownership TEXT NOT NULL,state TEXT NOT NULL,enabled INTEGER NOT NULL CHECK(enabled=0),permissions TEXT NOT NULL CHECK(permissions='[]'),source_db_digest TEXT NOT NULL,PRIMARY KEY(kind,package_id)); PRAGMA user_version=7; COMMIT;")?; crate::extension_transaction::recover(&mut db)?; Ok(Self { root, db, _lock: lock, }) } /// 只读导入旧 Python 安装记录;导入结果始终禁用、无许可且未受信任。 pub fn import_legacy_installations( &mut self, legacy_data_root: &Path, ) -> Result> { crate::extension_legacy::import(&mut self.db, &self.root, legacy_data_root) } pub fn trust_setting( &self, source_url: &str, namespace: &str, key_id: &str, ) -> Result> { let row:Option<(String,String)>=self.db.query_row("SELECT setting,revision FROM extension_trust WHERE source=?1 AND namespace=?2 AND key_id=?3", params![source(source_url)?,namespace,key_id],|r|Ok((r.get(0)?,r.get(1)?))).optional()?; row.map(|(json, revision)| { let setting: TrustSetting = serde_json::from_str(&json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; if setting.fingerprint()? != revision || setting.source != source(source_url)? || setting.namespace != namespace || setting.key_id != key_id { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } Ok(setting) }) .transpose() } /// 用户确认显示的指纹后调用。并发设置更改需要重新审核;刷新从不调用此方法。 pub fn confirm_trust( &mut self, setting: &TrustSetting, expected_revision: Option<&str>, confirmed_fingerprint: &str, ) -> Result { let revision = setting.fingerprint()?; if confirmed_fingerprint != revision { return Err(HostError::new("EXTENSION_TRUST_CONFIRMATION")); } let old = self.trust_setting(&setting.source, &setting.namespace, &setting.key_id)?; let old_revision = old.as_ref().map(TrustSetting::fingerprint).transpose()?; if old.as_ref() == Some(setting) { return Ok(revision); } if old_revision.as_deref() != expected_revision { return Err(HostError::new("EXTENSION_TRUST_CONFLICT")); } // 防止同一个规范化 URL 在无提示的情况下获得第二个来源标识。 let mut statement = self .db .prepare("SELECT setting FROM extension_trust WHERE source=?1")?; for row in statement.query_map([&setting.source], |r| r.get::<_, String>(0))? { let peer: TrustSetting = serde_json::from_str(&row?) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; if peer.source_id != setting.source_id { return Err(HostError::new("EXTENSION_TRUST_CHANGED")); } } drop(statement); self.db.execute("INSERT INTO extension_trust VALUES (?1,?2,?3,?4,?5) ON CONFLICT(source,namespace,key_id) DO UPDATE SET setting=excluded.setting,revision=excluded.revision", params![setting.source,setting.namespace,setting.key_id,serde_json::to_string(setting).unwrap(),revision])?; Ok(revision) } pub fn confirm_trust_group( &mut self, proposals: &[(TrustSetting, Option, String)], ) -> Result> { if proposals.is_empty() || proposals.len() > 64 { return Err(HostError::new("EXTENSION_TRUST_INVALID")); } let mut identities = std::collections::BTreeSet::new(); for (setting, _, _) in proposals { if setting.source != proposals[0].0.source || setting.source_id != proposals[0].0.source_id || !identities.insert((&setting.namespace, &setting.key_id)) { return Err(HostError::new("EXTENSION_TRUST_INVALID")); } } self.db .execute_batch("SAVEPOINT trust_confirmation_group")?; let result = (|| { let mut revisions = Vec::new(); for (setting, expected, confirmed) in proposals { revisions.push(self.confirm_trust(setting, expected.as_deref(), confirmed)?); } self.db.execute_batch("RELEASE trust_confirmation_group")?; Ok(revisions) })(); if result.is_err() { self.db.execute_batch( "ROLLBACK TO trust_confirmation_group; RELEASE trust_confirmation_group", )?; } result } fn block_identity( source_url: &str, release: &Release, public_key: &[u8; 32], key: bool, ) -> Result { let origin = source(source_url)?; let identity = if key { serde_json::json!([ "key", origin, release.namespace, release.key_id, hash(public_key) ]) } else { serde_json::json!([ "release", origin, release.namespace, release.package_id, release.version ]) }; Ok(hash(&serde_json::to_vec(&identity).unwrap())) } /// 持续拒绝与回滚和更新源同意无关。 pub fn check_not_revoked( &self, source_url: &str, release: &Release, public_key: &[u8; 32], ) -> Result<()> { for key in [true, false] { let identity = Self::block_identity(source_url, release, public_key, key)?; let reason: Option = self .db .query_row( "SELECT reason FROM extension_blocks WHERE identity=?1", [identity], |r| r.get(0), ) .optional()?; if let Some(reason) = reason { if !matches!( reason.as_str(), "EXTENSION_KEY_REVOKED" | "EXTENSION_RELEASE_WITHDRAWN" ) { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } return Err(HostError::new(&reason)); } } Ok(()) } fn remember_revocation( &mut self, source_url: &str, release: &Release, public_key: &[u8; 32], code: &str, ) -> Result<()> { let key = match code { "EXTENSION_KEY_REVOKED" => true, "EXTENSION_RELEASE_WITHDRAWN" => false, _ => return Ok(()), }; let identity = Self::block_identity(source_url, release, public_key, key)?; self.db.execute( "INSERT INTO extension_blocks VALUES (?1,?2) ON CONFLICT(identity) DO NOTHING", params![identity, code], )?; Ok(()) } /// 构建完整的同意有效负载;允许暂存工作,但活动指针、运行实例和权限不受影响。 pub fn installation_preview(&mut self, request: &InstallRequest) -> Result { use crate::extension_transaction::{Change, Target}; let vault = Uuid::parse_str(&request.vault_id) .map_err(|_| HostError::new("VAULT_INVALID"))? .to_string(); if vault != request.vault_id || request.configurations.len() > 200 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let dependencies = self.dependency_plan( &request.root_key, &request.app_version, &request.platform, &request.architecture, )?; for key in request.configurations.keys() { if !dependencies.packages.iter().any(|p| &p.package_key == key) { return Err(HostError::new("EXTENSION_CONFIG_INVALID")); } } let mut changes = Vec::new(); let mut trust_revisions = Vec::new(); for locked in &dependencies.packages { let (json, key): (String, Vec) = self.db.query_row( "SELECT release,signer FROM versions WHERE package_key=?1", [&locked.package_key], |r| Ok((r.get(0)?, r.get(1)?)), )?; let release: Release = serde_json::from_str(&json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let key: [u8; 32] = key .try_into() .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; self.check_not_revoked(&locked.source, &release, &key)?; let trusted = self .trust_setting(&locked.source, &release.namespace, &release.key_id)? .ok_or_else(|| HostError::new("EXTENSION_SOURCE_UNTRUSTED"))?; if !trusted.enabled || trusted.public_key != key { return Err(HostError::new("EXTENSION_SOURCE_UNTRUSTED")); } trust_revisions.push(trusted.fingerprint()?); let configuration = request .configurations .get(&locked.package_key) .cloned() .unwrap_or_else(|| serde_json::json!({})); let archive = self.archive(&locked.package_key)?; let (_, manifest) = release.verify_package( &key, &release.key_id, &release.namespace, false, false, &archive, )?; crate::extension_config::validate(&manifest, &configuration)?; let prepared = self.prepare( &locked.package_key, Signer { public_key: &key, key_id: &release.key_id, namespace: &release.namespace, revoked: false, }, false, )?; let slot = hash( &serde_json::to_vec(&( &vault, &locked.source, &release.namespace, &release.package_id, )) .unwrap(), ); let current = self.active_installation(&slot)?; if current .as_ref() .is_some_and(|p| p.pending_operation.is_some()) { return Err(HostError::new("EXTENSION_TRANSACTION_BUSY")); } changes.push(Change { target: Target { slot, package_key: locked.package_key.clone(), directory: prepared.directory, tree_sha256: prepared.tree_sha256, configuration, }, expected_revision: current.map(|p| p.revision), }); } let bytes = serde_json::to_vec(&(request, &dependencies, &changes, trust_revisions)).unwrap(); if bytes.len() > 4 * 1024 * 1024 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } Ok(InstallPreview { fingerprint: hash(&bytes), dependencies, changes, }) } /// 在线检查之前重新计算准确的已审核有效负载。主窗口确认 UI 必须提供此摘要;仅此方法并不表示同意。 pub async fn install_confirmed( &mut self, operation: &str, request: &InstallRequest, confirmed_fingerprint: &str, ) -> Result { let changes = self.lock_confirmed_plan(operation, request, confirmed_fingerprint)?; self.switch_online(operation, &request.vault_id, &changes) .await } fn lock_confirmed_plan( &mut self, operation: &str, request: &InstallRequest, confirmed_fingerprint: &str, ) -> Result> { Uuid::parse_str(operation).map_err(|_| HostError::new("OPERATION_ID_INVALID"))?; let request_bytes = serde_json::to_vec(request).unwrap(); if request_bytes.len() > 4 * 1024 * 1024 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let request_hash = hash(&request_bytes); let existing:Option<(String,String,String)>=self.db.query_row("SELECT request_hash,review_hash,changes FROM extension_confirmations WHERE operation_id=?1",[operation],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?))).optional()?; if let Some((old_request, old_review, json)) = existing { if old_request != request_hash || old_review != confirmed_fingerprint { return Err(HostError::new("OPERATION_REUSED")); } return serde_json::from_str(&json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT")); } let preview = self.installation_preview(request)?; if preview.fingerprint != confirmed_fingerprint { return Err(HostError::new("EXTENSION_INSTALL_REVIEW_CHANGED")); } self.db.execute( "INSERT INTO extension_confirmations VALUES (?1,?2,?3,?4)", params![ operation, request_hash, confirmed_fingerprint, serde_json::to_string(&preview.changes).unwrap() ], )?; Ok(preview.changes) } pub async fn stage_online( &mut self, operation: &str, source_url: &str, release: &Release, ) -> Result { self.stage_online_checked(operation, source_url, release, || Ok(())) .await } pub async fn stage_online_checked( &mut self, operation: &str, source_url: &str, release: &Release, checkpoint: impl Fn() -> Result<()>, ) -> Result { checkpoint()?; Uuid::parse_str(operation).map_err(|_| HostError::new("OPERATION_ID_INVALID"))?; let origin = source(source_url)?; let trusted = self .trust_setting(&origin, &release.namespace, &release.key_id)? .ok_or_else(|| HostError::new("EXTENSION_SOURCE_UNTRUSTED"))?; if !trusted.enabled { return Err(HostError::new("EXTENSION_SOURCE_UNTRUSTED")); } self.check_not_revoked(&origin, release, &trusted.public_key)?; let client = crate::extension_trust::Client::new(&origin)?; let result = client .check( crate::extension_trust::Pin { source_id: &trusted.source_id, key_id: &trusted.key_id, namespace: &trusted.namespace, public_key: &trusted.public_key, }, release, ) .await; let checked = match result { Ok(checked) => checked, Err(error) => { self.remember_revocation(&origin, release, &trusted.public_key, &error.code)?; return Err(error); } }; let archive = client .download(&checked, release, &trusted.public_key) .await?; self.stage_inner( Stage { operation_id: operation, source: &origin, release, archive: &archive, withdrawn: false, signer: Signer { public_key: &trusted.public_key, key_id: &trusted.key_id, namespace: &trusted.namespace, revoked: false, }, }, |_| checkpoint(), ) } /// 在线安装检查点,只使用已确认的 Host 信任设置。 pub async fn switch_online( &mut self, operation: &str, vault_id: &str, changes: &[crate::extension_transaction::Change], ) -> Result { if changes.is_empty() || changes.len() > 200 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let mut verified = Vec::new(); let mut origin = None; for change in changes { let (source, json, key): (String, String, Vec) = self.db.query_row( "SELECT source,release,signer FROM versions WHERE package_key=?1", [&change.target.package_key], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), )?; if origin.as_ref().is_some_and(|s| s != &source) { return Err(HostError::new("EXTENSION_SOURCE_INVALID")); } origin = Some(source.clone()); let release: Release = serde_json::from_str(&json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let key: [u8; 32] = key .try_into() .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let trusted = self .trust_setting(&source, &release.namespace, &release.key_id)? .ok_or_else(|| HostError::new("EXTENSION_SOURCE_UNTRUSTED"))?; if !trusted.enabled || trusted.public_key != key { return Err(HostError::new("EXTENSION_SOURCE_UNTRUSTED")); } self.check_not_revoked(&source, &release, &key)?; let client = crate::extension_trust::Client::new(&source)?; let checked = client .check( crate::extension_trust::Pin { source_id: &trusted.source_id, key_id: &release.key_id, namespace: &release.namespace, public_key: &key, }, &release, ) .await; let checked = match checked { Ok(checked) => checked, Err(error) => { self.remember_revocation(&source, &release, &key, &error.code)?; return Err(error); } }; verified.push((checked, source, release, key)); } self.switch_prepared_inner(operation, vault_id, changes, || { for (checked, source, release, key) in &verified { checked.matches(source, release, key)?; } Ok(()) }) } /// 安装器策略检查通过后,以原子方式选定已准备的分组。此方法不会停止进程、验证配置结构或颁发许可。 pub fn switch_prepared( &mut self, operation: &str, vault_id: &str, changes: &[crate::extension_transaction::Change], ) -> Result { self.switch_prepared_inner(operation, vault_id, changes, || Ok(())) } fn switch_prepared_inner( &mut self, operation: &str, vault_id: &str, changes: &[crate::extension_transaction::Change], check_fresh: impl FnOnce() -> Result<()>, ) -> Result { use cap_fs_ext::DirExt; let vault = Uuid::parse_str(vault_id) .map_err(|_| HostError::new("VAULT_INVALID"))? .to_string(); if vault != vault_id || changes.is_empty() || changes.len() > 200 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let root = cap_std::fs::Dir::open_ambient_dir(&self.root, cap_std::ambient_authority())?; let prepared = root.open_dir_nofollow("prepared")?; for change in changes { let (source, json, key, directory, tree): (String,String,Vec,String,String) = self.db.query_row( "SELECT v.source,v.release,v.signer,p.directory,p.tree_sha256 FROM versions v JOIN prepared_packages p ON p.package_key=v.package_key WHERE v.package_key=?1", [&change.target.package_key], |r| Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?,r.get(4)?)))?; let release: Release = serde_json::from_str(&json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let public: [u8; 32] = key .try_into() .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; self.check_not_revoked(&source, &release, &public)?; let (inventory, manifest) = release.verify_package( &public, &release.key_id, &release.namespace, false, false, &self.archive(&change.target.package_key)?, )?; crate::extension_config::validate(&manifest, &change.target.configuration)?; let slot = hash( &serde_json::to_vec(&(&vault, &source, &release.namespace, &release.package_id)) .unwrap(), ); if change.target.slot != slot || change.target.directory != directory || change.target.tree_sha256 != tree || Uuid::parse_str(&directory) .map(|v| v.to_string()) .ok() .as_ref() != Some(&directory) { return Err(HostError::new("EXTENSION_INSTALL_CONFLICT")); } if crate::extension_unpack::verify_tree( &prepared.open_dir_nofollow(&directory)?, &inventory, )? != tree { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } } check_fresh()?; crate::extension_transaction::switch(&mut self.db, operation, changes) } pub fn finish_installation( &mut self, operation: &str, healthy: bool, ) -> Result { crate::extension_transaction::finish(&mut self.db, operation, healthy) } pub fn active_installation( &self, slot: &str, ) -> Result> { crate::extension_transaction::active(&self.db, slot) } /// 重新验证活动指针、签名、信任、配置和展开树,并返回只能由 Host 消费的运行材料。 pub fn runtime_package( &self, slot: &str, vault_id: &str, pending_operation: Option<&str>, ) -> Result { use cap_fs_ext::DirExt; let vault = Uuid::parse_str(vault_id) .map_err(|_| HostError::new("VAULT_INVALID"))? .to_string(); if vault != vault_id { return Err(HostError::new("VAULT_INVALID")); } let active = self .active_installation(slot)? .filter(|item| item.pending_operation.as_deref() == pending_operation) .ok_or_else(|| HostError::new("EXTENSION_NOT_ACTIVE"))?; let (source, release_json, signer, manifest_json, directory, tree): ( String, String, Vec, String, String, String, ) = self.db.query_row( "SELECT v.source,v.release,v.signer,v.manifest,p.directory,p.tree_sha256 FROM versions v JOIN prepared_packages p ON p.package_key=v.package_key WHERE v.package_key=?1", [&active.target.package_key], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?)), )?; let release: Release = serde_json::from_str(&release_json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let manifest: serde_json::Value = serde_json::from_str(&manifest_json) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let public: [u8; 32] = signer .try_into() .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let expected_slot = hash( &serde_json::to_vec(&(&vault, &source, &release.namespace, &release.package_id)) .unwrap(), ); if slot != expected_slot || active.target.directory != directory || active.target.tree_sha256 != tree { return Err(HostError::new("EXTENSION_INSTALL_CONFLICT")); } let trusted = self .trust_setting(&source, &release.namespace, &release.key_id)? .ok_or_else(|| HostError::new("EXTENSION_SOURCE_UNTRUSTED"))?; if !trusted.enabled || trusted.public_key != public { return Err(HostError::new("EXTENSION_SOURCE_UNTRUSTED")); } self.check_not_revoked(&source, &release, &public)?; let archive = self.archive(&active.target.package_key)?; let (inventory, verified_manifest) = release.verify_package( &public, &release.key_id, &release.namespace, false, false, &archive, )?; if verified_manifest != manifest { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } crate::extension_config::validate(&manifest, &active.target.configuration)?; let root = cap_std::fs::Dir::open_ambient_dir(&self.root, cap_std::ambient_authority())?; let package = root .open_dir_nofollow("prepared")? .open_dir_nofollow(&directory)?; if crate::extension_unpack::verify_tree(&package, &inventory)? != tree { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } Ok(RuntimePackage { package, inventory, release, manifest, active, source, signer_sha256: hash(&public), }) } pub fn rollback_changes( &self, operation: &str, ) -> Result> { crate::extension_transaction::rollback_changes(&self.db, operation) } pub fn uninstall_active( &mut self, operation: &str, slot: &str, expected_revision: &str, ) -> Result { crate::extension_transaction::uninstall(&mut self.db, operation, slot, expected_revision) } /// 准备经过验证的暂存包。调用者提供当前的签名者/撤销策略;持久准备不会在重放时绕过该策略。 pub fn prepare( &mut self, package_key: &str, signer: Signer<'_>, withdrawn: bool, ) -> Result { self.prepare_inner(package_key, signer, withdrawn, |_| Ok(())) } fn prepare_inner( &mut self, package_key: &str, signer: Signer<'_>, withdrawn: bool, mut checkpoint: impl FnMut(&str) -> Result<()>, ) -> Result { use cap_fs_ext::DirExt; let (json, stored_key): (String, Vec) = self.db.query_row( "SELECT release,signer FROM versions WHERE package_key=?1", [package_key], |r| Ok((r.get(0)?, r.get(1)?)), )?; if stored_key.as_slice() != signer.public_key { return Err(HostError::new("EXTENSION_SIGNER_MISMATCH")); } let release: Release = serde_json::from_str(&json).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let archive = self.archive(package_key)?; let (inventory, _) = release.verify_package( signer.public_key, signer.key_id, signer.namespace, signer.revoked, withdrawn, &archive, )?; let root = cap_std::fs::Dir::open_ambient_dir(&self.root, cap_std::ambient_authority())?; match root.create_dir("prepared") { Ok(()) => {} Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} Err(e) => return Err(e.into()), } #[cfg(unix)] root.try_clone()?.into_std_file().sync_all()?; let staging = root.open_dir_nofollow("prepared")?; let existing: Option<(String, String)> = self .db .query_row( "SELECT directory,tree_sha256 FROM prepared_packages WHERE package_key=?1", [package_key], |r| Ok((r.get(0)?, r.get(1)?)), ) .optional()?; if let Some((directory, tree_sha256)) = existing { // 数据库数据从不提供任意相对路径。 if Uuid::parse_str(&directory) .map(|id| id.to_string()) .ok() .as_ref() != Some(&directory) { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } let actual = crate::extension_unpack::verify_tree( &staging.open_dir_nofollow(&directory)?, &inventory, )?; if actual != tree_sha256 { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } return Ok(PreparedPackage { package_key: package_key.into(), directory, tree_sha256, }); } let prepared = crate::extension_unpack::prepare( &staging, &release, signer.public_key, signer.key_id, signer.namespace, &archive, )?; checkpoint("tree_prepared")?; let receipt = PreparedPackage { package_key: package_key.into(), directory: prepared.directory, tree_sha256: prepared.tree_sha256, }; let transaction = self.db.transaction()?; transaction.execute( "INSERT INTO prepared_packages VALUES (?1,?2,?3)", params![receipt.package_key, receipt.directory, receipt.tree_sha256], )?; checkpoint("preparation_recorded")?; transaction.commit()?; checkpoint("preparation_committed")?; Ok(receipt) } pub fn stage(&mut self, request: Stage<'_>) -> Result { self.stage_inner(request, |_| Ok(())) } fn stage_inner( &mut self, request: Stage<'_>, mut checkpoint: impl FnMut(&str) -> Result<()>, ) -> Result { Uuid::parse_str(request.operation_id) .map_err(|_| HostError::new("OPERATION_ID_INVALID"))?; let source = source(request.source)?; let release = request.release; let (inventory, manifest) = release.verify_package( request.signer.public_key, request.signer.key_id, request.signer.namespace, request.signer.revoked, request.withdrawn, request.archive, )?; let key = hash( serde_json::to_vec(&( &source, &release.namespace, &release.package_id, &release.version, )) .unwrap() .as_slice(), ); let fingerprint = hash( serde_json::to_vec(&(&source, release, request.signer.public_key.as_slice())) .unwrap() .as_slice(), ); if let Some((old, receipt)) = self .db .query_row( "SELECT fingerprint,receipt FROM stage_operations WHERE id=?1", [request.operation_id], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)), ) .optional()? { if old != fingerprint { return Err(HostError::new("OPERATION_REUSED")); } let receipt: Receipt = serde_json::from_str(&receipt) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; self.verify_object(&receipt.archive_sha256, release.size)?; return Ok(receipt); } let old: Option = self .db .query_row( "SELECT fingerprint FROM versions WHERE package_key=?1", [&key], |r| r.get(0), ) .optional()?; if old.is_some_and(|old| old != fingerprint) { return Err(HostError::new("EXTENSION_VERSION_IMMUTABLE")); } let target = self.object_path(&release.sha256)?; if target.exists() { self.verify_object(&release.sha256, release.size)?; } else { let mut file = tempfile::NamedTempFile::new_in(target.parent().unwrap())?; file.write_all(request.archive)?; file.as_file().sync_all()?; file.persist_noclobber(&target) .map_err(|_| HostError::new("EXTENSION_STORE_WRITE_FAILED"))?; #[cfg(unix)] File::open(target.parent().unwrap())?.sync_all()?; } checkpoint("object_stored")?; let receipt = Receipt { operation_id: request.operation_id.into(), package_key: key.clone(), archive_sha256: release.sha256.clone(), version: release.version.clone(), state: "staged".into(), }; let tx = self.db.transaction()?; tx.execute("INSERT OR IGNORE INTO versions VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,'staged')",params![key,source,release.namespace,release.package_id,release.version,fingerprint,serde_json::to_string(release).unwrap(),serde_json::to_string(&manifest).unwrap(),serde_json::to_string(&inventory.files).unwrap(),release.sha256,release.size,request.signer.public_key.as_slice()])?; checkpoint("version_recorded")?; tx.execute( "INSERT INTO stage_operations VALUES (?1,?2,?3)", params![ request.operation_id, fingerprint, serde_json::to_string(&receipt).unwrap() ], )?; checkpoint("receipt_recorded")?; tx.commit()?; checkpoint("committed")?; Ok(receipt) } fn object_path(&self, digest: &str) -> Result { if digest.len() != 64 || !digest .bytes() .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } ordinary(&self.root)?; ordinary(&self.root.join("objects"))?; Ok(self.root.join("objects").join(digest)) } fn verify_object(&self, digest: &str, size: u64) -> Result<()> { let path = self.object_path(digest)?; ordinary(&path)?; crate::payloads::verify(&path, digest, size) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT")) } pub fn stage_receipt(&self, operation: &str) -> Result> { Uuid::parse_str(operation).map_err(|_| HostError::new("OPERATION_ID_INVALID"))?; let json: Option = self .db .query_row( "SELECT receipt FROM stage_operations WHERE id=?1", [operation], |r| r.get(0), ) .optional()?; json.map(|v| { serde_json::from_str(&v).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT")) }) .transpose() } pub fn archive(&self, package_key: &str) -> Result> { let (digest, size): (String, u64) = self.db.query_row( "SELECT archive_hash,size FROM versions WHERE package_key=?1", [package_key], |r| Ok((r.get(0)?, r.get(1)?)), )?; if size > 10 * 1024 * 1024 { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } self.verify_object(&digest, size)?; let mut bytes = Vec::new(); File::open(self.object_path(&digest)?)? .take(size + 1) .read_to_end(&mut bytes)?; if bytes.len() as u64 != size || hash(&bytes) != digest { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } Ok(bytes) } } #[cfg(test)] mod tests { use super::*; use base64::{engine::general_purpose::STANDARD, Engine}; use ed25519_dalek::{Signer as _, SigningKey}; use serde_json::Value; fn fixture() -> (Release, Vec, [u8; 32]) { let mut data: Value = serde_json::from_str(include_str!( "../../src/services/fixtures/community-python-vector.json" )) .unwrap(); for key in ["release_id", "withdrawn", "download_path"] { data["release"].as_object_mut().unwrap().remove(key); } let mut release: Release = serde_json::from_value(data["release"].clone()).unwrap(); let signer = SigningKey::from_bytes(&[7; 32]); release.signature = STANDARD.encode(signer.sign(&release.signed_payload().unwrap()).to_bytes()); ( release, STANDARD .decode(data["archive_base64"].as_str().unwrap()) .unwrap(), signer.verifying_key().to_bytes(), ) } fn request<'a>( id: &'a str, release: &'a Release, archive: &'a [u8], key: &'a [u8; 32], ) -> Stage<'a> { Stage { operation_id: id, source: "https://catalog.example", signer: Signer { public_key: key, key_id: "test-key", namespace: "examples", revoked: false, }, release, withdrawn: false, archive, } } #[test] fn grouped_trust_confirmation_rolls_back_earlier_keys_on_later_conflict() { let temp = tempfile::tempdir().unwrap(); let (_, _, key) = fixture(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let first = TrustSetting { source: "https://catalog.example/".into(), source_id: "catalog".into(), namespace: "examples".into(), key_id: "first".into(), public_key: key, enabled: true, }; let mut second = first.clone(); second.key_id = "second".into(); store .confirm_trust(&second, None, &second.fingerprint().unwrap()) .unwrap(); second.enabled = false; let proposals = vec![ (first.clone(), None, first.fingerprint().unwrap()), (second.clone(), None, second.fingerprint().unwrap()), ]; assert!(store.confirm_trust_group(&proposals).is_err()); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); assert!(store .trust_setting(&first.source, &first.namespace, &first.key_id) .unwrap() .is_none()); let old = store .trust_setting(&second.source, &second.namespace, &second.key_id) .unwrap() .unwrap(); assert!(old.enabled); let mut proposals = proposals; proposals[1].1 = Some(old.fingerprint().unwrap()); store.confirm_trust_group(&proposals).unwrap(); assert!(store .trust_setting(&first.source, &first.namespace, &first.key_id) .unwrap() .is_some()); assert!( !store .trust_setting(&second.source, &second.namespace, &second.key_id) .unwrap() .unwrap() .enabled ); let duplicate = vec![proposals[0].clone(), proposals[0].clone()]; assert!(store.confirm_trust_group(&duplicate).is_err()); } #[test] fn cancelled_stage_reports_durable_commit_race_truthfully() { let (release, archive, key) = fixture(); for boundary in ["object_stored", "receipt_recorded", "committed"] { let temp = tempfile::tempdir().unwrap(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let operation = Uuid::new_v4().to_string(); assert_eq!( store .stage_inner(request(&operation, &release, &archive, &key), |at| { if at == boundary { Err(HostError::new("REQUEST_CANCELLED")) } else { Ok(()) } }) .unwrap_err() .code, "REQUEST_CANCELLED" ); drop(store); let store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!( store.stage_receipt(&operation).unwrap().is_some(), boundary == "committed" ); } } #[tokio::test] async fn installation_preview_binds_target_state_and_rejects_stale_confirmation_before_network() { let temp = tempfile::tempdir().unwrap(); let (release, archive, key) = fixture(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let staged = store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); let setting = TrustSetting { source: "https://catalog.example/".into(), source_id: "catalog".into(), namespace: release.namespace.clone(), key_id: release.key_id.clone(), public_key: key, enabled: true, }; store .confirm_trust(&setting, None, &setting.fingerprint().unwrap()) .unwrap(); let mut req = InstallRequest { root_key: staged.package_key, vault_id: Uuid::new_v4().to_string(), app_version: "1.0.0".into(), platform: "windows".into(), architecture: "x86_64".into(), configurations: Default::default(), }; let preview = store.installation_preview(&req).unwrap(); assert_eq!( preview.fingerprint, store.installation_preview(&req).unwrap().fingerprint ); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!( preview.fingerprint, store.installation_preview(&req).unwrap().fingerprint ); assert_eq!( store .install_confirmed(&Uuid::new_v4().to_string(), &req, "stale") .await .unwrap_err() .code, "EXTENSION_INSTALL_REVIEW_CHANGED" ); req.app_version = "1.0.1".into(); assert_ne!( preview.fingerprint, store.installation_preview(&req).unwrap().fingerprint ); req.app_version = "1.0.0".into(); let confirmed_operation = Uuid::new_v4().to_string(); let locked = store .lock_confirmed_plan(&confirmed_operation, &req, &preview.fingerprint) .unwrap(); let id = Uuid::new_v4().to_string(); store .switch_prepared(&id, &req.vault_id, &preview.changes) .unwrap(); assert!(store.installation_preview(&req).is_err()); store.finish_installation(&id, true).unwrap(); assert_eq!( locked, store .lock_confirmed_plan(&confirmed_operation, &req, &preview.fingerprint) .unwrap() ); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!( locked, store .lock_confirmed_plan(&confirmed_operation, &req, &preview.fingerprint) .unwrap() ); assert!(store .lock_confirmed_plan(&confirmed_operation, &req, "different") .is_err()); assert_ne!( preview.fingerprint, store.installation_preview(&req).unwrap().fingerprint ); assert_eq!( store .install_confirmed(&Uuid::new_v4().to_string(), &req, &preview.fingerprint) .await .unwrap_err() .code, "EXTENSION_INSTALL_REVIEW_CHANGED" ); req.configurations .insert("unknown-package".into(), serde_json::json!({})); assert!(store.installation_preview(&req).is_err()); } #[test] fn revocations_survive_restart_and_consent_without_overblocking_other_releases() { let temp = tempfile::tempdir().unwrap(); let (release, _, key) = fixture(); let source = "https://catalog.example/"; let mut store = ExtensionStore::open(temp.path()).unwrap(); store .remember_revocation(source, &release, &key, "EXTENSION_TRUST_UNAVAILABLE") .unwrap(); store.check_not_revoked(source, &release, &key).unwrap(); store .remember_revocation(source, &release, &key, "EXTENSION_RELEASE_WITHDRAWN") .unwrap(); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); let setting = TrustSetting { source: source.into(), source_id: "catalog".into(), namespace: release.namespace.clone(), key_id: release.key_id.clone(), public_key: key, enabled: true, }; store .confirm_trust(&setting, None, &setting.fingerprint().unwrap()) .unwrap(); assert_eq!( store .check_not_revoked(source, &release, &key) .unwrap_err() .code, "EXTENSION_RELEASE_WITHDRAWN" ); let mut next = release.clone(); next.version = "2.0.0".into(); store.check_not_revoked(source, &next, &key).unwrap(); store .remember_revocation(source, &release, &key, "EXTENSION_KEY_REVOKED") .unwrap(); assert_eq!( store .check_not_revoked(source, &next, &key) .unwrap_err() .code, "EXTENSION_KEY_REVOKED" ); store .check_not_revoked("https://other.example/", &next, &key) .unwrap(); let replacement = SigningKey::from_bytes(&[8; 32]).verifying_key().to_bytes(); store .check_not_revoked(source, &next, &replacement) .unwrap(); assert!(store .check_not_revoked(source, &release, &replacement) .is_err()); drop(store); let store = ExtensionStore::open(temp.path()).unwrap(); assert!(store.check_not_revoked(source, &next, &key).is_err()); } #[tokio::test] async fn online_switch_never_trusts_the_staged_signer_implicitly() { let temp = tempfile::tempdir().unwrap(); let (release, archive, key) = fixture(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let staged = store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); let changes = vec![crate::extension_transaction::Change { target: crate::extension_transaction::Target { slot: "a".repeat(64), package_key: staged.package_key, directory: Uuid::new_v4().to_string(), tree_sha256: "a".repeat(64), configuration: serde_json::json!({}), }, expected_revision: None, }]; let id = Uuid::new_v4().to_string(); let vault = Uuid::new_v4().to_string(); assert_eq!( store .switch_online(&id, &vault, &changes) .await .unwrap_err() .code, "EXTENSION_SOURCE_UNTRUSTED" ); let mut setting = TrustSetting { source: "https://catalog.example/".into(), source_id: "catalog".into(), namespace: "examples".into(), key_id: "test-key".into(), public_key: key, enabled: false, }; let disabled = setting.fingerprint().unwrap(); store.confirm_trust(&setting, None, &disabled).unwrap(); assert_eq!( store .switch_online(&id, &vault, &changes) .await .unwrap_err() .code, "EXTENSION_SOURCE_UNTRUSTED" ); setting.enabled = true; setting.public_key = SigningKey::from_bytes(&[8; 32]).verifying_key().to_bytes(); store .confirm_trust(&setting, Some(&disabled), &setting.fingerprint().unwrap()) .unwrap(); assert_eq!( store .switch_online(&id, &vault, &changes) .await .unwrap_err() .code, "EXTENSION_SOURCE_UNTRUSTED" ); assert_eq!( store .db .query_row("SELECT COUNT(*) FROM extension_transactions", [], |r| r .get::<_, i64>(0)) .unwrap(), 0 ); } #[tokio::test] async fn offline_new_install_is_rejected_before_creating_a_transaction() { use crate::extension_transaction::{Change, Target}; let temp = tempfile::tempdir().unwrap(); let (release, archive, key) = fixture(); let source = "https://127.0.0.1:9/"; let mut store = ExtensionStore::open(temp.path()).unwrap(); let staged = store .stage(Stage { operation_id: &Uuid::new_v4().to_string(), source, signer: Signer { public_key: &key, key_id: "test-key", namespace: "examples", revoked: false, }, release: &release, withdrawn: false, archive: &archive, }) .unwrap(); let setting = TrustSetting { source: source.into(), source_id: "offline-catalog".into(), namespace: "examples".into(), key_id: "test-key".into(), public_key: key, enabled: true, }; store .confirm_trust(&setting, None, &setting.fingerprint().unwrap()) .unwrap(); let prepared = store .prepare( &staged.package_key, Signer { public_key: &key, key_id: "test-key", namespace: "examples", revoked: false, }, false, ) .unwrap(); let vault = Uuid::new_v4().to_string(); let slot = hash( &serde_json::to_vec(&(&vault, source, &release.namespace, &release.package_id)) .unwrap(), ); let changes = [Change { target: Target { slot, package_key: staged.package_key, directory: prepared.directory, tree_sha256: prepared.tree_sha256, configuration: serde_json::json!({}), }, expected_revision: None, }]; let error = store .switch_online(&Uuid::new_v4().to_string(), &vault, &changes) .await .unwrap_err(); assert_eq!(error.code, "EXTENSION_TRUST_UNAVAILABLE"); assert_eq!( store .db .query_row("SELECT COUNT(*) FROM extension_transactions", [], |row| row .get::<_, i64>(0)) .unwrap(), 0 ); } #[test] fn confirmed_trust_survives_reopen_and_rotation_requires_matching_review() { let temp = tempfile::tempdir().unwrap(); let (_, _, key) = fixture(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let mut setting = TrustSetting { source: "https://catalog.example/".into(), source_id: "catalog".into(), namespace: "examples".into(), key_id: "test-key".into(), public_key: key, enabled: true, }; let fingerprint = setting.fingerprint().unwrap(); assert!(store .trust_setting(&setting.source, &setting.namespace, &setting.key_id) .unwrap() .is_none()); assert!(store .confirm_trust(&setting, None, "not-confirmed") .is_err()); store.confirm_trust(&setting, None, &fingerprint).unwrap(); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!( store .trust_setting(&setting.source, &setting.namespace, &setting.key_id) .unwrap(), Some(setting.clone()) ); setting.public_key = SigningKey::from_bytes(&[8; 32]).verifying_key().to_bytes(); let rotated = setting.fingerprint().unwrap(); assert!(store.confirm_trust(&setting, None, &rotated).is_err()); assert!(store .confirm_trust(&setting, Some(&fingerprint), &fingerprint) .is_err()); store .confirm_trust(&setting, Some(&fingerprint), &rotated) .unwrap(); setting.enabled = false; let disabled = setting.fingerprint().unwrap(); store .confirm_trust(&setting, Some(&rotated), &disabled) .unwrap(); assert!( !store .trust_setting(&setting.source, &setting.namespace, &setting.key_id) .unwrap() .unwrap() .enabled ); setting.key_id = "another-key".into(); setting.source_id = "different".into(); assert!(store .confirm_trust(&setting, None, &setting.fingerprint().unwrap()) .is_err()); } #[test] fn prepared_switch_rechecks_content_vault_binding_and_recovers_on_open() { use crate::extension_transaction::{Change, Target}; let temp = tempfile::tempdir().unwrap(); let (release, archive, key) = fixture(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let staged = store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); let prepared = store .prepare( &staged.package_key, Signer { public_key: &key, key_id: "test-key", namespace: "examples", revoked: false, }, false, ) .unwrap(); let vault = Uuid::new_v4().to_string(); let slot = hash( &serde_json::to_vec(&( &vault, "https://catalog.example/", &release.namespace, &release.package_id, )) .unwrap(), ); let changes = vec![Change { target: Target { slot: slot.clone(), package_key: staged.package_key, directory: prepared.directory.clone(), tree_sha256: prepared.tree_sha256, configuration: serde_json::json!({}), }, expected_revision: None, }]; assert!(store .switch_prepared( &Uuid::new_v4().to_string(), &Uuid::new_v4().to_string(), &changes ) .is_err()); let mut invalid = changes.clone(); invalid[0].target.configuration = serde_json::json!({"password":"never-persist-this"}); assert!(store .switch_prepared(&Uuid::new_v4().to_string(), &vault, &invalid) .is_err()); assert_eq!( store .db .query_row("SELECT COUNT(*) FROM extension_transactions", [], |r| r .get::<_, i64>(0)) .unwrap(), 0 ); let operation = Uuid::new_v4().to_string(); store.switch_prepared(&operation, &vault, &changes).unwrap(); assert!(store .active_installation(&slot) .unwrap() .unwrap() .pending_operation .is_some()); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); assert!(store.active_installation(&slot).unwrap().is_none()); assert_eq!( store.finish_installation(&operation, true).unwrap().state, "rolled_back" ); let operation = Uuid::new_v4().to_string(); store.switch_prepared(&operation, &vault, &changes).unwrap(); store.finish_installation(&operation, true).unwrap(); let setting = TrustSetting { source: "https://catalog.example/".into(), source_id: "catalog".into(), namespace: release.namespace.clone(), key_id: release.key_id.clone(), public_key: key, enabled: true, }; store .confirm_trust(&setting, None, &setting.fingerprint().unwrap()) .unwrap(); let runtime = store.runtime_package(&slot, &vault, None).unwrap(); assert_eq!(runtime.active.target, changes[0].target); assert_eq!(runtime.release.package_id, release.package_id); drop(store); let mut store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!( store.active_installation(&slot).unwrap().unwrap().target, changes[0].target ); fs::write( temp.path() .join("prepared") .join(&prepared.directory) .join("persona.json"), b"bad", ) .unwrap(); assert!(store.switch_prepared(&operation, &vault, &changes).is_err()); } #[test] fn schema_one_upgrade_preserves_versions_and_creates_readable_backup() { let temp = tempfile::tempdir().unwrap(); let (release, archive, key) = fixture(); let mut store = ExtensionStore::open(temp.path()).unwrap(); let receipt = store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); store .db .execute_batch("DROP TABLE prepared_packages; PRAGMA user_version=1;") .unwrap(); drop(store); let store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!(store.archive(&receipt.package_key).unwrap(), archive); let backups: Vec<_> = fs::read_dir(temp.path()) .unwrap() .map(|e| e.unwrap().path()) .filter(|p| { p.file_name() .unwrap() .to_string_lossy() .starts_with("extensions.schema1.") }) .collect(); assert_eq!(backups.len(), 1); let backup = Connection::open(&backups[0]).unwrap(); assert_eq!( backup .query_row("PRAGMA user_version", [], |r| r.get::<_, i64>(0)) .unwrap(), 1 ); assert_eq!( backup .query_row("SELECT COUNT(*) FROM versions", [], |r| r.get::<_, i64>(0)) .unwrap(), 1 ); drop(backup); drop(store); let _store = ExtensionStore::open(temp.path()).unwrap(); assert_eq!( fs::read_dir(temp.path()) .unwrap() .filter(|e| e .as_ref() .unwrap() .file_name() .to_string_lossy() .starts_with("extensions.schema1.")) .count(), 1 ); } #[test] fn preparation_replays_checks_policy_and_recovers_each_boundary() { let (release, archive, key) = fixture(); for boundary in [ "tree_prepared", "preparation_recorded", "preparation_committed", ] { for _ in 0..20 { let root = tempfile::tempdir().unwrap(); let mut store = ExtensionStore::open(root.path()).unwrap(); let staged = store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); let signer = || Signer { public_key: &key, key_id: "test-key", namespace: "examples", revoked: false, }; assert!(store .prepare_inner(&staged.package_key, signer(), false, |at| { if at == boundary { Err(HostError::new("INJECTED")) } else { Ok(()) } }) .is_err()); drop(store); let mut store = ExtensionStore::open(root.path()).unwrap(); let prepared = store.prepare(&staged.package_key, signer(), false).unwrap(); assert_eq!( prepared, store.prepare(&staged.package_key, signer(), false).unwrap() ); assert_eq!( store .db .query_row("SELECT COUNT(*) FROM prepared_packages", [], |r| r .get::<_, i64>(0)) .unwrap(), 1 ); assert!(store.prepare(&staged.package_key, signer(), true).is_err()); let mut revoked = signer(); revoked.revoked = true; assert!(store.prepare(&staged.package_key, revoked, false).is_err()); fs::write( root.path() .join("prepared") .join(&prepared.directory) .join("persona.json"), b"changed", ) .unwrap(); assert!(store.prepare(&staged.package_key, signer(), false).is_err()); } } } #[test] fn staged_dependency_preview_survives_reopen_and_rechecks_archive_integrity() { let (mut release, archive, key) = fixture(); let root = tempfile::tempdir().unwrap(); let mut store = ExtensionStore::open(root.path()).unwrap(); for version in ["1.0.0", "2.0.0"] { release.package_id = "dependency".into(); release.version = version.into(); release.signature = STANDARD.encode( SigningKey::from_bytes(&[7; 32]) .sign(&release.signed_payload().unwrap()) .to_bytes(), ); store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); } release.package_id = "test-package".into(); release.version = "1.0.0".into(); release .dependencies .insert("dependency".into(), "^1.0.0".into()); release.signature = STANDARD.encode( SigningKey::from_bytes(&[7; 32]) .sign(&release.signed_payload().unwrap()) .to_bytes(), ); let receipt = store .stage(request( &Uuid::new_v4().to_string(), &release, &archive, &key, )) .unwrap(); let plan = store .dependency_plan(&receipt.package_key, "0.3.0", "windows", "x86_64") .unwrap(); assert_eq!(plan.packages.len(), 2); assert_eq!(plan.packages[0].package_id, "dependency"); assert_eq!(plan.packages[0].version, "1.0.0"); drop(store); let store = ExtensionStore::open(root.path()).unwrap(); assert_eq!( plan.fingerprint, store .dependency_plan(&receipt.package_key, "0.3.0", "windows", "x86_64") .unwrap() .fingerprint ); assert!(store .staged(0, 100) .unwrap() .iter() .all(|v| v.state == "staged")); fs::write(store.object_path(&release.sha256).unwrap(), b"changed").unwrap(); assert!(store .dependency_plan(&receipt.package_key, "0.3.0", "windows", "x86_64") .is_err()); } #[test] fn staging_recovers_each_durable_boundary_twenty_times() { let (release, archive, key) = fixture(); for boundary in [ "object_stored", "version_recorded", "receipt_recorded", "committed", ] { for _ in 0..20 { let root = tempfile::tempdir().unwrap(); let mut store = ExtensionStore::open(root.path()).unwrap(); let id = Uuid::new_v4().to_string(); assert!(store .stage_inner(request(&id, &release, &archive, &key), |point| { if point == boundary { Err(HostError::new("INJECTED_FAILURE")) } else { Ok(()) } }) .is_err()); drop(store); let mut store = ExtensionStore::open(root.path()).unwrap(); let receipt = store.stage(request(&id, &release, &archive, &key)).unwrap(); assert_eq!(receipt.state, "staged"); let items = store.staged(0, 100).unwrap(); assert_eq!(items.len(), 1); assert_eq!(items[0].package_key, receipt.package_key); assert_eq!(items[0].source, "https://catalog.example/"); assert_eq!(items[0].state, "staged"); assert!(store.staged(1, 100).unwrap().is_empty()); assert_eq!(store.archive(&receipt.package_key).unwrap(), archive); assert_eq!( store.stage(request(&id, &release, &archive, &key)).unwrap(), receipt ); assert_eq!( store .db .query_row("SELECT COUNT(*) FROM versions", [], |r| r.get::<_, i64>(0)) .unwrap(), 1 ); assert_eq!( store .db .query_row("SELECT COUNT(*) FROM stage_operations", [], |r| r .get::<_, i64>(0)) .unwrap(), 1 ); } } } #[test] fn immutable_versions_and_revocation_are_not_bypassed_by_operation_replay() { let (release, archive, key) = fixture(); let root = tempfile::tempdir().unwrap(); let mut store = ExtensionStore::open(root.path()).unwrap(); let id = Uuid::new_v4().to_string(); let receipt = store.stage(request(&id, &release, &archive, &key)).unwrap(); let mut revoked = request(&id, &release, &archive, &key); revoked.signer.revoked = true; assert_eq!(store.stage(revoked).unwrap_err().code, "EXTENSION_REVOKED"); let mut changed = release.clone(); changed.name = "Different signed release".into(); changed.signature = STANDARD.encode( SigningKey::from_bytes(&[7; 32]) .sign(&changed.signed_payload().unwrap()) .to_bytes(), ); assert_eq!( store .stage(request(&id, &changed, &archive, &key)) .unwrap_err() .code, "OPERATION_REUSED" ); assert_eq!( store .stage(request( &Uuid::new_v4().to_string(), &changed, &archive, &key )) .unwrap_err() .code, "EXTENSION_VERSION_IMMUTABLE" ); fs::write(store.object_path(&release.sha256).unwrap(), b"corrupt").unwrap(); assert!(store.archive(&receipt.package_key).is_err()); assert!(store.stage(request(&id, &release, &archive, &key)).is_err()); } #[test] fn store_ownership_and_preexisting_hardlinks_are_rejected() { let root = tempfile::tempdir().unwrap(); let store = ExtensionStore::open(root.path()).unwrap(); assert!(ExtensionStore::open(root.path()).is_err()); drop(store); ExtensionStore::open(root.path()).unwrap(); let outside = tempfile::NamedTempFile::new().unwrap(); let linked = tempfile::tempdir().unwrap(); fs::hard_link(outside.path(), linked.path().join("extensions.sqlite3")).unwrap(); assert!(ExtensionStore::open(linked.path()).is_err()); assert_eq!(outside.as_file().metadata().unwrap().len(), 0); for suffix in ["-wal", "-shm", "-journal"] { let linked = tempfile::tempdir().unwrap(); fs::hard_link( outside.path(), linked.path().join(format!("extensions.sqlite3{suffix}")), ) .unwrap(); assert!(ExtensionStore::open(linked.path()).is_err()); } assert!(source("https://user:password@example.com").is_err()); assert!(source("https://example.com/?token=secret").is_err()); assert_eq!( source("https://catalog.example").unwrap(), "https://catalog.example/" ); } }