// SPDX-License-Identifier: AGPL-3.0-or-later use crate::release_trust::provisioned_release_trust; use fs2::FileExt; use ring::digest::{digest, SHA256}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs::{self, OpenOptions}; use std::io::Write; use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Manager}; use tauri_plugin_updater::{Update, UpdaterExt}; use uuid::Uuid; const CANDIDATE_SCHEMA: &str = "hololake.release-candidate/v1"; const RECEIPT_SCHEMA: &str = "hololake.release-install-receipt/v1"; const RECOVERY_SCHEMA: &str = "hololake.release-recovery/v1"; const CANDIDATE_TTL_MS: u128 = 30 * 60 * 1000; const EXPECTED_BUNDLE_IDENTIFIER: &str = "world.guanghu.hololake"; const PUBLIC_RELEASE_PACKAGE_PREFIX: &str = "/hololake/releases/"; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct ReleaseCandidateRecord { schema: String, state: String, candidate_id: String, confirmation_token_sha256: String, release_id: String, current_version: String, version: String, raw_broadcast_sha256: String, download_url: String, package_sha256: String, package_size: u64, issued_at_unix_ms: u128, expires_at_unix_ms: u128, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct ReleaseRecoveryRecord { schema: String, state: String, release_id: String, previous_version: String, installed_version: String, backup_bundle_path: String, backup_identifier: String, backup_team_identifier: String, backup_cdhash: String, failed_bundle_path: Option, observed_at_unix_ms: u128, receipt_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] struct CodeSignatureEvidence { identifier: String, team_identifier: String, cdhash: String, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ConfirmReleaseInstallInput { candidate_id: String, confirmation_token: String, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ReleaseEnvelope { schema: String, release_id: String, version: String, notes: String, platforms: serde_json::Map, hololake: HoloLakeMetadata, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct HoloLakeMetadata { features: Vec, #[serde(default)] fixes: Vec, compatibility: CompatibilityMetadata, restart: RestartMetadata, rollback: RollbackMetadata, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct CompatibilityMetadata { minimum_version: String, data_migration_required: bool, notes: Option, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct RestartMetadata { required: bool, automatic_allowed: bool, message: Option, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct RollbackMetadata { supported: bool, health_receipt_required: bool, previous_version: Option, } #[derive(Clone, Debug, Deserialize)] struct PlatformRelease { url: String, signature: String, size: u64, sha256: String, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReleaseCandidateReceipt { schema: &'static str, state: &'static str, candidate_id: String, confirmation_token: String, release_id: String, current_version: String, version: String, notes: String, features: Vec, fixes: Vec, minimum_version: String, data_migration_required: bool, compatibility_notes: Option, restart_message: Option, rollback_supported: bool, previous_version: Option, expires_at_unix_ms: u128, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReleaseCheckReceipt { schema: &'static str, state: &'static str, candidate: Option, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReleaseInstallReceipt { schema: &'static str, state: &'static str, release_id: String, previous_version: String, installed_version: String, package_sha256: String, package_size: u64, automatic_restart: bool, health_receipt_required: bool, rollback_declared_by_broadcast: bool, receipt_id: String, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReleaseRecoveryStatus { pub schema: &'static str, pub state: String, pub previous_version: Option, pub installed_version: Option, pub backup_ready: bool, pub human_action_required: bool, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReleaseRecoveryReceipt { pub schema: &'static str, pub state: String, pub previous_version: String, pub installed_version: String, pub automatic_restart: bool, pub receipt_id: String, } #[tauri::command] pub async fn check_hololake_update(app: AppHandle) -> Result { let trust = provisioned_release_trust()? .ok_or_else(|| "HOLOLAKE_RELEASE_TRUST_UNPROVISIONED_NO_NETWORK_REQUEST".to_string())?; let root = release_update_root(&app)?; let _install_lease = install_lease(&root)?; ensure_no_unresolved_recovery_at(&root)?; let update = app .updater_builder() .endpoints(trust.endpoints.clone()) .map_err(|error| format!("HOLOLAKE_RELEASE_UPDATER_INVALID: {error}"))? .build() .map_err(|error| format!("HOLOLAKE_RELEASE_UPDATER_INVALID: {error}"))? .check() .await .map_err(|error| format!("HOLOLAKE_RELEASE_CHECK_FAILED: {error}"))?; let Some(update) = update else { return Ok(ReleaseCheckReceipt { schema: CANDIDATE_SCHEMA, state: "CURRENT_VERSION_IS_LATEST", candidate: None, }); }; let snapshot = validate_update(&update, &trust.allowed_release_host)?; let candidate = create_candidate_at(&root, &snapshot)?; Ok(ReleaseCheckReceipt { schema: CANDIDATE_SCHEMA, state: "UPDATE_AVAILABLE_AWAITING_HUMAN_CONFIRMATION", candidate: Some(candidate), }) } #[tauri::command] pub async fn confirm_hololake_update_install( app: AppHandle, input: ConfirmReleaseInstallInput, ) -> Result { let trust = provisioned_release_trust()? .ok_or_else(|| "HOLOLAKE_RELEASE_TRUST_UNPROVISIONED_NO_NETWORK_REQUEST".to_string())?; let root = release_update_root(&app)?; let _install_lease = install_lease(&root)?; ensure_no_unresolved_recovery_at(&root)?; let record = authorize_candidate_at(&root, &input)?; let update = app .updater_builder() .endpoints(trust.endpoints.clone()) .map_err(|error| format!("HOLOLAKE_RELEASE_UPDATER_INVALID: {error}"))? .build() .map_err(|error| format!("HOLOLAKE_RELEASE_UPDATER_INVALID: {error}"))? .check() .await .map_err(|error| format!("HOLOLAKE_RELEASE_RECHECK_FAILED: {error}"))? .ok_or_else(|| "HOLOLAKE_RELEASE_CANDIDATE_NO_LONGER_AVAILABLE".to_string())?; let snapshot = validate_update(&update, &trust.allowed_release_host)?; require_same_candidate(&record, &snapshot)?; let bytes = update .download(|_, _| {}, || {}) .await .map_err(|error| format!("HOLOLAKE_RELEASE_DOWNLOAD_OR_SIGNATURE_FAILED: {error}"))?; if bytes.len() as u64 != snapshot.package_size || sha256_hex(&bytes) != snapshot.package_sha256 { return Err("HOLOLAKE_RELEASE_PACKAGE_DIGEST_OR_SIZE_MISMATCH".into()); } let current_bundle = current_app_bundle_path()?; let backup = prepare_last_known_good_at(&root, ¤t_bundle)?; let mut recovery = ReleaseRecoveryRecord { schema: RECOVERY_SCHEMA.into(), state: "LAST_KNOWN_GOOD_VERIFIED_INSTALLING".into(), release_id: snapshot.envelope.release_id.clone(), previous_version: snapshot.current_version.clone(), installed_version: snapshot.envelope.version.clone(), backup_bundle_path: backup.0.to_string_lossy().into_owned(), backup_identifier: backup.1.identifier, backup_team_identifier: backup.1.team_identifier, backup_cdhash: backup.1.cdhash, failed_bundle_path: None, observed_at_unix_ms: now_unix_ms()?, receipt_id: sha256_hex( format!( "{}\n{}\n{}\n{}", snapshot.envelope.release_id, snapshot.current_version, snapshot.envelope.version, snapshot.package_sha256 ) .as_bytes(), ), }; write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?; let receipt = ReleaseInstallReceipt { schema: RECEIPT_SCHEMA, state: "SIGNED_PACKAGE_VERIFIED_INSTALL_STARTED_RESTART_REQUIRED", release_id: snapshot.envelope.release_id.clone(), previous_version: snapshot.current_version.clone(), installed_version: snapshot.envelope.version.clone(), package_sha256: snapshot.package_sha256.clone(), package_size: snapshot.package_size, automatic_restart: false, health_receipt_required: snapshot.envelope.hololake.rollback.health_receipt_required, rollback_declared_by_broadcast: snapshot.envelope.hololake.rollback.supported, receipt_id: sha256_hex( format!( "{}\n{}\n{}\n{}", snapshot.envelope.release_id, snapshot.current_version, snapshot.envelope.version, snapshot.package_sha256 ) .as_bytes(), ), }; write_json_atomic( &root.join("pending-install-receipt.json"), &receipt, "RECEIPT", )?; if let Err(error) = mark_candidate_installing_at(&root, &record.candidate_id) { recovery.state = "INSTALL_FAILED_BACKUP_RETAINED".into(); recovery.observed_at_unix_ms = now_unix_ms()?; write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?; return Err(error); } if let Err(error) = update.install(&bytes) { recovery.state = "INSTALL_FAILED_BACKUP_RETAINED".into(); recovery.observed_at_unix_ms = now_unix_ms()?; write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?; return Err(format!("HOLOLAKE_RELEASE_INSTALL_FAILED: {error}")); } recovery.state = "INSTALL_REPLACED_AWAITING_MANUAL_RESTART".into(); recovery.observed_at_unix_ms = now_unix_ms()?; write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?; Ok(receipt) } #[tauri::command] pub fn get_hololake_release_recovery_status( app: AppHandle, ) -> Result { release_recovery_status_at(&release_update_root(&app)?) } #[tauri::command] pub fn confirm_hololake_release_health(app: AppHandle) -> Result { let root = release_update_root(&app)?; let _lock = lock(&root)?; let path = recovery_path(&root); let mut record: ReleaseRecoveryRecord = read_json(&path)?; if !matches!( record.state.as_str(), "AWAITING_HUMAN_HEALTH_CONFIRMATION" | "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" | "INSTALL_FAILED_BACKUP_RETAINED" | "INSTALL_INTERRUPTED_BACKUP_RETAINED" ) { return Err("HOLOLAKE_RELEASE_HEALTH_CONFIRMATION_NOT_AVAILABLE".into()); } let running_version = app.package_info().version.to_string(); let expected = if record.state == "AWAITING_HUMAN_HEALTH_CONFIRMATION" { &record.installed_version } else { &record.previous_version }; if &running_version != expected { return Err("HOLOLAKE_RELEASE_RUNNING_VERSION_MISMATCH".into()); } remove_owned_bundle_path(&root, Path::new(&record.backup_bundle_path))?; if let Some(failed) = record.failed_bundle_path.as_deref() { remove_owned_failed_bundle(Path::new(failed), ¤t_app_parent()?)?; } record.state = match record.state.as_str() { "AWAITING_HUMAN_HEALTH_CONFIRMATION" => "HEALTH_CONFIRMED_BACKUP_REMOVED", "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" => "ROLLBACK_CONFIRMED_BACKUP_REMOVED", _ => "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED", } .into(); record.observed_at_unix_ms = now_unix_ms()?; write_json_atomic(&path, &record, "RECOVERY")?; Ok(recovery_receipt(&record)) } #[tauri::command] pub fn rollback_hololake_update(app: AppHandle) -> Result { let root = release_update_root(&app)?; let _lock = lock(&root)?; let path = recovery_path(&root); let mut record: ReleaseRecoveryRecord = read_json(&path)?; if record.state != "AWAITING_HUMAN_HEALTH_CONFIRMATION" { return Err("HOLOLAKE_RELEASE_ROLLBACK_NOT_AVAILABLE".into()); } if app.package_info().version.to_string() != record.installed_version { return Err("HOLOLAKE_RELEASE_RUNNING_VERSION_MISMATCH".into()); } let backup = validated_backup_path(&root, &record)?; let current = current_app_bundle_path()?; let failed = replace_bundle_with_backup(¤t, &backup, &record)?; record.failed_bundle_path = Some(failed.to_string_lossy().into_owned()); record.state = "ROLLBACK_REPLACED_AWAITING_MANUAL_RESTART".into(); record.observed_at_unix_ms = now_unix_ms()?; write_json_atomic(&path, &record, "RECOVERY")?; Ok(recovery_receipt(&record)) } pub(crate) fn observe_release_startup(app: &AppHandle) -> Result<(), String> { let root = release_update_root(app)?; let path = recovery_path(&root); if !path.exists() { return Ok(()); } let _lock = lock(&root)?; let mut record: ReleaseRecoveryRecord = read_json(&path)?; let version = app.package_info().version.to_string(); let next = match record.state.as_str() { "INSTALL_REPLACED_AWAITING_MANUAL_RESTART" if version == record.installed_version => { Some("AWAITING_HUMAN_HEALTH_CONFIRMATION") } "ROLLBACK_REPLACED_AWAITING_MANUAL_RESTART" if version == record.previous_version => { if let Some(failed) = record.failed_bundle_path.as_deref() { remove_owned_failed_bundle(Path::new(failed), ¤t_app_parent()?)?; record.failed_bundle_path = None; } Some("ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT") } "LAST_KNOWN_GOOD_VERIFIED_INSTALLING" if version == record.previous_version => { Some("INSTALL_INTERRUPTED_BACKUP_RETAINED") } "LAST_KNOWN_GOOD_VERIFIED_INSTALLING" if version == record.installed_version => { Some("AWAITING_HUMAN_HEALTH_CONFIRMATION") } "AWAITING_HUMAN_HEALTH_CONFIRMATION" if version == record.previous_version => { Some("ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT") } _ => None, }; if let Some(next) = next { record.state = next.into(); record.observed_at_unix_ms = now_unix_ms()?; write_json_atomic(&path, &record, "RECOVERY")?; } Ok(()) } pub(crate) fn release_recovery_state(app: &AppHandle) -> Result { let root = release_update_root(app)?; Ok(release_recovery_state_at(&root)) } fn release_recovery_state_at(root: &Path) -> String { release_recovery_status_at(root) .map(|status| status.state) .unwrap_or_else(|_| "RECOVERY_STATE_UNREADABLE_ACTION_REQUIRED".into()) } struct UpdateSnapshot { current_version: String, envelope: ReleaseEnvelope, raw_broadcast_sha256: String, download_url: String, package_sha256: String, package_size: u64, } fn validate_update(update: &Update, allowed_host: &str) -> Result { let raw = serde_json::to_vec(&update.raw_json) .map_err(|error| format!("HOLOLAKE_RELEASE_BROADCAST_INVALID: {error}"))?; let envelope: ReleaseEnvelope = serde_json::from_value(update.raw_json.clone()) .map_err(|error| format!("HOLOLAKE_RELEASE_BROADCAST_INVALID: {error}"))?; if envelope.schema != "hololake.release-broadcast/v1" || envelope.release_id.trim().is_empty() || envelope.version != update.version || envelope.notes.trim().is_empty() || envelope.hololake.features.is_empty() || !envelope.hololake.restart.required || envelope.hololake.restart.automatic_allowed || !envelope.hololake.rollback.supported || !envelope.hololake.rollback.health_receipt_required { return Err("HOLOLAKE_RELEASE_BROADCAST_POLICY_INVALID".into()); } let url = &update.download_url; if url.scheme() != "https" || url.host_str() != Some(allowed_host) || !url.path().starts_with(PUBLIC_RELEASE_PACKAGE_PREFIX) || url.query().is_some() || url.fragment().is_some() { return Err("HOLOLAKE_RELEASE_PACKAGE_HOST_NOT_TRUSTED".into()); } let platform = envelope .platforms .values() .filter_map(|value| serde_json::from_value::(value.clone()).ok()) .find(|platform| platform.url == url.as_str() && platform.signature == update.signature) .ok_or("HOLOLAKE_RELEASE_PLATFORM_EVIDENCE_MISSING")?; validate_sha256(&platform.sha256)?; if platform.size == 0 { return Err("HOLOLAKE_RELEASE_PACKAGE_SIZE_INVALID".into()); } Ok(UpdateSnapshot { current_version: update.current_version.clone(), envelope, raw_broadcast_sha256: sha256_hex(&raw), download_url: url.to_string(), package_sha256: platform.sha256, package_size: platform.size, }) } fn create_candidate_at( root: &Path, snapshot: &UpdateSnapshot, ) -> Result { let _lock = lock(root)?; let issued_at_unix_ms = now_unix_ms()?; let expires_at_unix_ms = issued_at_unix_ms + CANDIDATE_TTL_MS; let candidate_id = format!("release-{}", Uuid::new_v4()); let confirmation_token = format!("install-{}-{}", Uuid::new_v4(), Uuid::new_v4()); let record = ReleaseCandidateRecord { schema: CANDIDATE_SCHEMA.into(), state: "PENDING_HUMAN_CONFIRMATION".into(), candidate_id: candidate_id.clone(), confirmation_token_sha256: sha256_hex(confirmation_token.as_bytes()), release_id: snapshot.envelope.release_id.clone(), current_version: snapshot.current_version.clone(), version: snapshot.envelope.version.clone(), raw_broadcast_sha256: snapshot.raw_broadcast_sha256.clone(), download_url: snapshot.download_url.clone(), package_sha256: snapshot.package_sha256.clone(), package_size: snapshot.package_size, issued_at_unix_ms, expires_at_unix_ms, }; write_json_atomic(&candidate_path(root, &candidate_id), &record, "CANDIDATE")?; Ok(ReleaseCandidateReceipt { schema: CANDIDATE_SCHEMA, state: "AWAITING_HUMAN_DOWNLOAD_AND_INSTALL_CONFIRMATION", candidate_id, confirmation_token, release_id: snapshot.envelope.release_id.clone(), current_version: snapshot.current_version.clone(), version: snapshot.envelope.version.clone(), notes: snapshot.envelope.notes.clone(), features: snapshot.envelope.hololake.features.clone(), fixes: snapshot.envelope.hololake.fixes.clone(), minimum_version: snapshot .envelope .hololake .compatibility .minimum_version .clone(), data_migration_required: snapshot .envelope .hololake .compatibility .data_migration_required, compatibility_notes: snapshot.envelope.hololake.compatibility.notes.clone(), restart_message: snapshot.envelope.hololake.restart.message.clone(), rollback_supported: snapshot.envelope.hololake.rollback.supported, previous_version: snapshot.envelope.hololake.rollback.previous_version.clone(), expires_at_unix_ms, }) } fn authorize_candidate_at( root: &Path, input: &ConfirmReleaseInstallInput, ) -> Result { validate_machine_id(&input.candidate_id)?; if input.confirmation_token.len() < 32 || input.confirmation_token.len() > 256 { return Err("HOLOLAKE_RELEASE_CONFIRMATION_TOKEN_INVALID".into()); } let _lock = lock(root)?; let record: ReleaseCandidateRecord = read_json(&candidate_path(root, &input.candidate_id))?; if record.schema != CANDIDATE_SCHEMA || record.candidate_id != input.candidate_id { return Err("HOLOLAKE_RELEASE_CANDIDATE_INVALID".into()); } if record.confirmation_token_sha256 != sha256_hex(input.confirmation_token.as_bytes()) { return Err("HOLOLAKE_RELEASE_CANDIDATE_NOT_AUTHORIZED".into()); } if record.state != "PENDING_HUMAN_CONFIRMATION" || now_unix_ms()? > record.expires_at_unix_ms { return Err("HOLOLAKE_RELEASE_CANDIDATE_EXPIRED_OR_CONSUMED".into()); } Ok(record) } fn require_same_candidate( record: &ReleaseCandidateRecord, snapshot: &UpdateSnapshot, ) -> Result<(), String> { if record.release_id != snapshot.envelope.release_id || record.current_version != snapshot.current_version || record.version != snapshot.envelope.version || record.raw_broadcast_sha256 != snapshot.raw_broadcast_sha256 || record.download_url != snapshot.download_url || record.package_sha256 != snapshot.package_sha256 || record.package_size != snapshot.package_size { return Err("HOLOLAKE_RELEASE_BROADCAST_CHANGED_RECONFIRM_REQUIRED".into()); } Ok(()) } fn mark_candidate_installing_at(root: &Path, candidate_id: &str) -> Result<(), String> { let _lock = lock(root)?; let path = candidate_path(root, candidate_id); let mut record: ReleaseCandidateRecord = read_json(&path)?; if record.state != "PENDING_HUMAN_CONFIRMATION" { return Err("HOLOLAKE_RELEASE_CANDIDATE_EXPIRED_OR_CONSUMED".into()); } record.state = "INSTALLING".into(); write_json_atomic(&path, &record, "CANDIDATE") } fn ensure_no_unresolved_recovery_at(root: &Path) -> Result<(), String> { let path = recovery_path(root); if !path.exists() { return Ok(()); } let record: ReleaseRecoveryRecord = read_json(&path)?; if matches!( record.state.as_str(), "HEALTH_CONFIRMED_BACKUP_REMOVED" | "ROLLBACK_CONFIRMED_BACKUP_REMOVED" | "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED" ) { Ok(()) } else { Err("HOLOLAKE_RELEASE_RECOVERY_ACTION_REQUIRED_BEFORE_NEXT_UPDATE".into()) } } fn release_recovery_status_at(root: &Path) -> Result { let path = recovery_path(root); if !path.exists() { return Ok(ReleaseRecoveryStatus { schema: RECOVERY_SCHEMA, state: "NONE".into(), previous_version: None, installed_version: None, backup_ready: false, human_action_required: false, }); } let record: ReleaseRecoveryRecord = read_json(&path)?; let human_action_required = matches!( record.state.as_str(), "AWAITING_HUMAN_HEALTH_CONFIRMATION" | "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" | "INSTALL_FAILED_BACKUP_RETAINED" | "INSTALL_INTERRUPTED_BACKUP_RETAINED" ); let backup_ready = Path::new(&record.backup_bundle_path).exists(); Ok(ReleaseRecoveryStatus { schema: RECOVERY_SCHEMA, state: record.state, previous_version: Some(record.previous_version), installed_version: Some(record.installed_version), backup_ready, human_action_required, }) } fn recovery_receipt(record: &ReleaseRecoveryRecord) -> ReleaseRecoveryReceipt { ReleaseRecoveryReceipt { schema: RECOVERY_SCHEMA, state: record.state.clone(), previous_version: record.previous_version.clone(), installed_version: record.installed_version.clone(), automatic_restart: false, receipt_id: sha256_hex( format!( "{}\n{}\n{}\n{}", record.receipt_id, record.state, record.previous_version, record.installed_version ) .as_bytes(), ), } } fn prepare_last_known_good_at( root: &Path, current_bundle: &Path, ) -> Result<(PathBuf, CodeSignatureEvidence), String> { let current = current_bundle .canonicalize() .map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_BUNDLE_UNAVAILABLE: {error}"))?; if current.extension().and_then(|value| value.to_str()) != Some("app") { return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_INVALID".into()); } let current_evidence = verify_bundle_signature(¤t)?; if current_evidence.identifier != EXPECTED_BUNDLE_IDENTIFIER || current_evidence.team_identifier.is_empty() { return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_IDENTITY_INVALID".into()); } let next_root = root.join(format!(".last-known-good-next-{}", Uuid::new_v4())); fs::create_dir(&next_root) .map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_CREATE_FAILED: {error}"))?; let next_bundle = next_root.join("HoloLake.app"); let copy = trusted_command("/usr/bin/ditto") .arg(¤t) .arg(&next_bundle) .output(); let copy = match copy { Ok(output) => output, Err(error) => { let _ = fs::remove_dir_all(&next_root); return Err(format!("HOLOLAKE_RELEASE_BACKUP_COPY_FAILED: {error}")); } }; if let Err(error) = require_command_success(copy, "HOLOLAKE_RELEASE_BACKUP_COPY") { let _ = fs::remove_dir_all(&next_root); return Err(error); } let copied_evidence = match verify_bundle_signature(&next_bundle) { Ok(evidence) => evidence, Err(error) => { let _ = fs::remove_dir_all(&next_root); return Err(error); } }; if copied_evidence != current_evidence { let _ = fs::remove_dir_all(&next_root); return Err("HOLOLAKE_RELEASE_BACKUP_SIGNATURE_DRIFT".into()); } let final_root = root.join("last-known-good"); let previous_root = root.join(format!(".last-known-good-previous-{}", Uuid::new_v4())); if final_root.exists() { fs::rename(&final_root, &previous_root) .map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_ROTATE_FAILED: {error}"))?; } if let Err(error) = fs::rename(&next_root, &final_root) { if previous_root.exists() { let _ = fs::rename(&previous_root, &final_root); } let _ = fs::remove_dir_all(&next_root); return Err(format!("HOLOLAKE_RELEASE_BACKUP_ACTIVATE_FAILED: {error}")); } if previous_root.exists() { fs::remove_dir_all(&previous_root) .map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_ROTATE_FAILED: {error}"))?; } Ok((final_root.join("HoloLake.app"), copied_evidence)) } fn validated_backup_path(root: &Path, record: &ReleaseRecoveryRecord) -> Result { let backup = PathBuf::from(&record.backup_bundle_path) .canonicalize() .map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_UNAVAILABLE: {error}"))?; let owned_root = root .join("last-known-good") .canonicalize() .map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_UNAVAILABLE: {error}"))?; if backup != owned_root.join("HoloLake.app") { return Err("HOLOLAKE_RELEASE_BACKUP_BOUNDARY_INVALID".into()); } let evidence = verify_bundle_signature(&backup)?; if evidence.identifier != record.backup_identifier || evidence.team_identifier != record.backup_team_identifier || evidence.cdhash != record.backup_cdhash { return Err("HOLOLAKE_RELEASE_BACKUP_SIGNATURE_MISMATCH".into()); } Ok(backup) } fn replace_bundle_with_backup( current: &Path, backup: &Path, record: &ReleaseRecoveryRecord, ) -> Result { let current = current .canonicalize() .map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_BUNDLE_UNAVAILABLE: {error}"))?; let current_evidence = verify_bundle_signature(¤t)?; if current_evidence.identifier != EXPECTED_BUNDLE_IDENTIFIER || current_evidence.team_identifier != record.backup_team_identifier { return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_IDENTITY_INVALID".into()); } let parent = current .parent() .ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?; let failed = parent.join(format!(".HoloLake.failed-{}.app", Uuid::new_v4())); fs::rename(¤t, &failed) .map_err(|error| format!("HOLOLAKE_RELEASE_ROLLBACK_STAGE_FAILED: {error}"))?; let copied = trusted_command("/usr/bin/ditto") .arg(backup) .arg(¤t) .output(); let copied = match copied { Ok(output) => output, Err(error) => { restore_staged_current(&failed, ¤t)?; return Err(format!("HOLOLAKE_RELEASE_ROLLBACK_COPY_FAILED: {error}")); } }; if let Err(error) = require_command_success(copied, "HOLOLAKE_RELEASE_ROLLBACK_COPY") { restore_staged_current(&failed, ¤t)?; return Err(error); } let restored = verify_bundle_signature(¤t); if restored.as_ref() != Ok(&CodeSignatureEvidence { identifier: record.backup_identifier.clone(), team_identifier: record.backup_team_identifier.clone(), cdhash: record.backup_cdhash.clone(), }) { restore_staged_current(&failed, ¤t)?; return Err("HOLOLAKE_RELEASE_ROLLBACK_SIGNATURE_MISMATCH".into()); } Ok(failed) } fn restore_staged_current(staged: &Path, current: &Path) -> Result<(), String> { if current.exists() { fs::remove_dir_all(current) .map_err(|error| format!("HOLOLAKE_RELEASE_ROLLBACK_RECOVERY_FAILED: {error}"))?; } fs::rename(staged, current) .map_err(|error| format!("HOLOLAKE_RELEASE_ROLLBACK_RECOVERY_FAILED: {error}")) } fn remove_owned_bundle_path(root: &Path, bundle: &Path) -> Result<(), String> { let expected = root.join("last-known-good").join("HoloLake.app"); if bundle != expected { return Err("HOLOLAKE_RELEASE_BACKUP_BOUNDARY_INVALID".into()); } let owner = root.join("last-known-good"); if owner.exists() { fs::remove_dir_all(&owner) .map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_REMOVE_FAILED: {error}"))?; } Ok(()) } fn remove_owned_failed_bundle(bundle: &Path, expected_parent: &Path) -> Result<(), String> { let name = bundle .file_name() .and_then(|value| value.to_str()) .ok_or("HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID")?; if !name.starts_with(".HoloLake.failed-") || !name.ends_with(".app") { return Err("HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID".into()); } if bundle.parent() != Some(expected_parent) { return Err("HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID".into()); } if bundle.exists() { fs::remove_dir_all(bundle) .map_err(|error| format!("HOLOLAKE_RELEASE_FAILED_BUNDLE_REMOVE_FAILED: {error}"))?; } Ok(()) } fn verify_bundle_signature(bundle: &Path) -> Result { let verify = trusted_command("/usr/bin/codesign") .args(["--verify", "--deep", "--strict", "--verbose=4"]) .arg(bundle) .output() .map_err(|error| format!("HOLOLAKE_RELEASE_CODESIGN_VERIFY_FAILED: {error}"))?; require_command_success(verify, "HOLOLAKE_RELEASE_CODESIGN_VERIFY")?; let display = trusted_command("/usr/bin/codesign") .args(["-dv", "--verbose=4"]) .arg(bundle) .output() .map_err(|error| format!("HOLOLAKE_RELEASE_CODESIGN_READ_FAILED: {error}"))?; let text = String::from_utf8_lossy(&display.stderr); let value = |name: &str| { text.lines() .find_map(|line| line.strip_prefix(&format!("{name}="))) .map(str::to_owned) .ok_or_else(|| format!("HOLOLAKE_RELEASE_CODESIGN_{name}_MISSING")) }; Ok(CodeSignatureEvidence { identifier: value("Identifier")?, team_identifier: value("TeamIdentifier")?, cdhash: value("CDHash")?, }) } fn current_app_bundle_path() -> Result { let executable = std::env::current_exe() .map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_EXECUTABLE_UNAVAILABLE: {error}"))? .canonicalize() .map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_EXECUTABLE_UNAVAILABLE: {error}"))?; let contents = executable .parent() .and_then(Path::parent) .ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?; let bundle = contents .parent() .ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?; if contents.file_name().and_then(|value| value.to_str()) != Some("Contents") || bundle.extension().and_then(|value| value.to_str()) != Some("app") { return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID".into()); } Ok(bundle.to_path_buf()) } fn current_app_parent() -> Result { let bundle = current_app_bundle_path()?; let parent = bundle .parent() .ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")? .to_path_buf(); Ok(parent) } fn trusted_command(program: &str) -> Command { let mut command = Command::new(program); command .env_clear() .env("PATH", "/usr/bin:/bin") .env("LANG", "C"); command } fn require_command_success(output: Output, label: &str) -> Result<(), String> { if output.status.success() { Ok(()) } else { Err(format!( "{label}_FAILED: {}", String::from_utf8_lossy(&output.stderr).trim() )) } } fn release_update_root(app: &AppHandle) -> Result { let root = app .path() .app_data_dir() .map_err(|error| format!("HOLOLAKE_RELEASE_STORAGE_UNAVAILABLE: {error}"))? .join("release-update-v1"); fs::create_dir_all(&root) .map_err(|error| format!("HOLOLAKE_RELEASE_STORAGE_UNAVAILABLE: {error}"))?; root.canonicalize() .map_err(|error| format!("HOLOLAKE_RELEASE_STORAGE_UNAVAILABLE: {error}")) } fn candidate_path(root: &Path, candidate_id: &str) -> PathBuf { root.join(format!("{candidate_id}.json")) } fn recovery_path(root: &Path) -> PathBuf { root.join("recovery.json") } fn lock(root: &Path) -> Result { let lock = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .mode(0o600) .open(root.join("release.lock")) .map_err(|error| format!("HOLOLAKE_RELEASE_LOCK_FAILED: {error}"))?; lock.lock_exclusive() .map_err(|error| format!("HOLOLAKE_RELEASE_LOCK_FAILED: {error}"))?; Ok(lock) } fn install_lease(root: &Path) -> Result { let lease = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .mode(0o600) .open(root.join("install.lock")) .map_err(|error| format!("HOLOLAKE_RELEASE_INSTALL_LEASE_FAILED: {error}"))?; lease .try_lock_exclusive() .map_err(|_| "HOLOLAKE_RELEASE_INSTALL_ALREADY_IN_PROGRESS".to_string())?; Ok(lease) } fn read_json Deserialize<'de>>(path: &Path) -> Result { let metadata = fs::symlink_metadata(path) .map_err(|error| format!("HOLOLAKE_RELEASE_CANDIDATE_READ_FAILED: {error}"))?; if !metadata.is_file() || metadata.file_type().is_symlink() { return Err("HOLOLAKE_RELEASE_CANDIDATE_INVALID".into()); } serde_json::from_slice( &fs::read(path) .map_err(|error| format!("HOLOLAKE_RELEASE_CANDIDATE_READ_FAILED: {error}"))?, ) .map_err(|error| format!("HOLOLAKE_RELEASE_CANDIDATE_INVALID: {error}")) } fn write_json_atomic(path: &Path, value: &T, label: &str) -> Result<(), String> { let parent = path .parent() .ok_or_else(|| format!("HOLOLAKE_RELEASE_{label}_PATH_INVALID"))?; let temporary = parent.join(format!(".{label}-{}.tmp", Uuid::new_v4())); let bytes = serde_json::to_vec_pretty(value) .map_err(|error| format!("HOLOLAKE_RELEASE_{label}_INVALID: {error}"))?; let mut file = OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&temporary) .map_err(|error| format!("HOLOLAKE_RELEASE_{label}_WRITE_FAILED: {error}"))?; file.write_all(&bytes) .and_then(|_| file.sync_all()) .map_err(|error| format!("HOLOLAKE_RELEASE_{label}_WRITE_FAILED: {error}"))?; fs::rename(&temporary, path) .map_err(|error| format!("HOLOLAKE_RELEASE_{label}_WRITE_FAILED: {error}")) } fn validate_machine_id(value: &str) -> Result<(), String> { if value.is_empty() || value.len() > 128 || !value .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) { return Err("HOLOLAKE_RELEASE_CANDIDATE_ID_INVALID".into()); } Ok(()) } fn validate_sha256(value: &str) -> Result<(), String> { if value.len() != 64 || !value .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) { return Err("HOLOLAKE_RELEASE_PACKAGE_SHA256_INVALID".into()); } Ok(()) } fn now_unix_ms() -> Result { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis()) .map_err(|error| format!("HOLOLAKE_RELEASE_CLOCK_INVALID: {error}")) } fn sha256_hex(bytes: &[u8]) -> String { digest(&SHA256, bytes) .as_ref() .iter() .map(|byte| format!("{byte:02x}")) .collect() } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; fn snapshot() -> UpdateSnapshot { UpdateSnapshot { current_version: "0.1.0".into(), envelope: ReleaseEnvelope { schema: "hololake.release-broadcast/v1".into(), release_id: "release-1".into(), version: "0.2.0".into(), notes: "First signed update".into(), platforms: serde_json::Map::new(), hololake: HoloLakeMetadata { features: vec!["Personal channel".into()], fixes: vec![], compatibility: CompatibilityMetadata { minimum_version: "0.1.0".into(), data_migration_required: false, notes: None, }, restart: RestartMetadata { required: true, automatic_allowed: false, message: Some("Restart yourself".into()), }, rollback: RollbackMetadata { supported: true, health_receipt_required: true, previous_version: Some("0.1.0".into()), }, }, }, raw_broadcast_sha256: "1".repeat(64), download_url: "https://release.guanghu.test/HoloLake.tar.gz".into(), package_sha256: "2".repeat(64), package_size: 42, } } #[test] fn installation_requires_the_exact_short_lived_human_candidate() { let root = TempDir::new().unwrap(); let candidate = create_candidate_at(root.path(), &snapshot()).unwrap(); let wrong = authorize_candidate_at( root.path(), &ConfirmReleaseInstallInput { candidate_id: candidate.candidate_id.clone(), confirmation_token: "wrong-confirmation-token-long-enough-123".into(), }, ) .unwrap_err(); assert_eq!(wrong, "HOLOLAKE_RELEASE_CANDIDATE_NOT_AUTHORIZED"); let authorized = authorize_candidate_at( root.path(), &ConfirmReleaseInstallInput { candidate_id: candidate.candidate_id, confirmation_token: candidate.confirmation_token, }, ) .unwrap(); assert_eq!(authorized.version, "0.2.0"); } #[test] fn any_broadcast_or_package_change_requires_a_new_confirmation() { let root = TempDir::new().unwrap(); let original = snapshot(); let candidate = create_candidate_at(root.path(), &original).unwrap(); let record = authorize_candidate_at( root.path(), &ConfirmReleaseInstallInput { candidate_id: candidate.candidate_id, confirmation_token: candidate.confirmation_token, }, ) .unwrap(); let mut changed = snapshot(); changed.package_sha256 = "3".repeat(64); assert_eq!( require_same_candidate(&record, &changed).unwrap_err(), "HOLOLAKE_RELEASE_BROADCAST_CHANGED_RECONFIRM_REQUIRED" ); } #[test] fn unresolved_recovery_blocks_the_next_update_until_human_resolution() { let root = TempDir::new().unwrap(); let backup = root.path().join("last-known-good/HoloLake.app"); fs::create_dir_all(&backup).unwrap(); let recovery = ReleaseRecoveryRecord { schema: RECOVERY_SCHEMA.into(), state: "AWAITING_HUMAN_HEALTH_CONFIRMATION".into(), release_id: "release-1".into(), previous_version: "0.1.0".into(), installed_version: "0.2.0".into(), backup_bundle_path: backup.to_string_lossy().into_owned(), backup_identifier: EXPECTED_BUNDLE_IDENTIFIER.into(), backup_team_identifier: "TEAM".into(), backup_cdhash: "cdhash".into(), failed_bundle_path: None, observed_at_unix_ms: now_unix_ms().unwrap(), receipt_id: "receipt".into(), }; write_json_atomic(&recovery_path(root.path()), &recovery, "RECOVERY").unwrap(); assert_eq!( ensure_no_unresolved_recovery_at(root.path()).unwrap_err(), "HOLOLAKE_RELEASE_RECOVERY_ACTION_REQUIRED_BEFORE_NEXT_UPDATE" ); let status = release_recovery_status_at(root.path()).unwrap(); assert_eq!(status.state, "AWAITING_HUMAN_HEALTH_CONFIRMATION"); assert!(status.backup_ready); assert!(status.human_action_required); } #[test] fn failed_bundle_cleanup_is_confined_to_the_exact_current_parent() { let root = TempDir::new().unwrap(); let parent = root.path().join("Applications"); let elsewhere = root.path().join("elsewhere"); fs::create_dir_all(&parent).unwrap(); fs::create_dir_all(&elsewhere).unwrap(); let misleading = elsewhere.join(".HoloLake.failed-test.app"); fs::create_dir(&misleading).unwrap(); assert_eq!( remove_owned_failed_bundle(&misleading, &parent).unwrap_err(), "HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID" ); assert!(misleading.exists()); } #[test] fn unreadable_recovery_record_keeps_the_app_available_but_locks_updates() { let root = TempDir::new().unwrap(); fs::write(recovery_path(root.path()), b"not-json").unwrap(); assert_eq!( release_recovery_state_at(root.path()), "RECOVERY_STATE_UNREADABLE_ACTION_REQUIRED" ); assert!(ensure_no_unresolved_recovery_at(root.path()).is_err()); } #[test] fn only_one_install_operation_can_hold_the_release_lease() { let root = TempDir::new().unwrap(); let first = install_lease(root.path()).unwrap(); assert_eq!( install_lease(root.path()).unwrap_err(), "HOLOLAKE_RELEASE_INSTALL_ALREADY_IN_PROGRESS" ); drop(first); assert!(install_lease(root.path()).is_ok()); } #[test] fn partial_rollback_copy_is_removed_before_the_running_bundle_is_restored() { let root = TempDir::new().unwrap(); let current = root.path().join("HoloLake.app"); let staged = root.path().join(".HoloLake.failed-test.app"); fs::create_dir(¤t).unwrap(); fs::write(current.join("partial"), b"partial").unwrap(); fs::create_dir(&staged).unwrap(); fs::write(staged.join("original"), b"original").unwrap(); restore_staged_current(&staged, ¤t).unwrap(); assert!(current.join("original").exists()); assert!(!current.join("partial").exists()); assert!(!staged.exists()); } }