feat: establish native desktop update trust root
This commit is contained in:
parent
a2302cfd13
commit
cff488bb03
29 changed files with 8485 additions and 60 deletions
12
product-source/hololake-native-desktop/src-tauri/src/lib.rs
Normal file
12
product-source/hololake-native-desktop/src-tauri/src/lib.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
mod release_trust;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
release_trust::install_updater_if_provisioned(app.handle())?;
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run HoloLake native desktop");
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
hololake_native_desktop_lib::run();
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Runtime, Url};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
const EMBEDDED_RELEASE_TRUST: &str = include_str!("../release-trust.json");
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct ReleaseTrust {
|
||||
schema: String,
|
||||
state: TrustState,
|
||||
endpoints: Vec<String>,
|
||||
public_key: String,
|
||||
allowed_release_hosts: Vec<String>,
|
||||
automatic_check_on_startup: bool,
|
||||
automatic_download: bool,
|
||||
human_opt_in_install_required: bool,
|
||||
automatic_restart: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
enum TrustState {
|
||||
UnprovisionedFailClosed,
|
||||
Provisioned,
|
||||
}
|
||||
|
||||
enum ValidatedReleaseTrust {
|
||||
Disabled,
|
||||
Enabled {
|
||||
endpoints: Vec<Url>,
|
||||
public_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn validate_release_trust(raw: &str) -> Result<ValidatedReleaseTrust, String> {
|
||||
let trust: ReleaseTrust = serde_json::from_str(raw)
|
||||
.map_err(|error| format!("invalid HoloLake release trust document: {error}"))?;
|
||||
|
||||
if trust.schema != "hololake.release-trust/v1" {
|
||||
return Err("unsupported HoloLake release trust schema".into());
|
||||
}
|
||||
if trust.automatic_check_on_startup || trust.automatic_download || trust.automatic_restart {
|
||||
return Err("automatic check, download and restart must remain disabled".into());
|
||||
}
|
||||
if !trust.human_opt_in_install_required {
|
||||
return Err("human opt-in installation is required".into());
|
||||
}
|
||||
|
||||
match trust.state {
|
||||
TrustState::UnprovisionedFailClosed => {
|
||||
if !trust.endpoints.is_empty()
|
||||
|| !trust.public_key.is_empty()
|
||||
|| !trust.allowed_release_hosts.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"unprovisioned trust must not contain endpoint or public key material".into(),
|
||||
);
|
||||
}
|
||||
Ok(ValidatedReleaseTrust::Disabled)
|
||||
}
|
||||
TrustState::Provisioned => {
|
||||
if trust.endpoints.len() != 1 {
|
||||
return Err("exactly one HoloLake release endpoint is required".into());
|
||||
}
|
||||
if trust.public_key.trim().is_empty() {
|
||||
return Err("the HoloLake updater public key is required".into());
|
||||
}
|
||||
if trust.allowed_release_hosts.len() != 1 {
|
||||
return Err("exactly one HoloLake-owned release host is required".into());
|
||||
}
|
||||
let allowed_host = trust.allowed_release_hosts[0].trim();
|
||||
if allowed_host.is_empty() || allowed_host.contains('/') || allowed_host.contains(':') {
|
||||
return Err("the HoloLake release host must be a bare DNS name".into());
|
||||
}
|
||||
let endpoint = Url::parse(&trust.endpoints[0])
|
||||
.map_err(|error| format!("invalid HoloLake release endpoint: {error}"))?;
|
||||
if endpoint.scheme() != "https" {
|
||||
return Err("the HoloLake release endpoint must use HTTPS".into());
|
||||
}
|
||||
if endpoint.host_str() != Some(allowed_host) {
|
||||
return Err(
|
||||
"the updater endpoint is not owned by the configured HoloLake host".into(),
|
||||
);
|
||||
}
|
||||
Ok(ValidatedReleaseTrust::Enabled {
|
||||
endpoints: vec![endpoint],
|
||||
public_key: trust.public_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_updater_if_provisioned<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match validate_release_trust(EMBEDDED_RELEASE_TRUST)? {
|
||||
ValidatedReleaseTrust::Disabled => Ok(()),
|
||||
ValidatedReleaseTrust::Enabled {
|
||||
endpoints,
|
||||
public_key,
|
||||
} => {
|
||||
let updater = tauri_plugin_updater::Builder::new()
|
||||
.pubkey(public_key)
|
||||
.build();
|
||||
app.plugin(updater)?;
|
||||
app.updater_builder().endpoints(endpoints)?.build()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{validate_release_trust, ValidatedReleaseTrust};
|
||||
|
||||
fn document(state: &str, endpoint: &str, public_key: &str, allowed_host: &str) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"schema":"hololake.release-trust/v1",
|
||||
"state":"{state}",
|
||||
"endpoints":[{endpoint}],
|
||||
"publicKey":"{public_key}",
|
||||
"allowedReleaseHosts":[{allowed_host}],
|
||||
"automaticCheckOnStartup":false,
|
||||
"automaticDownload":false,
|
||||
"humanOptInInstallRequired":true,
|
||||
"automaticRestart":false
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unprovisioned_document_disables_update_networking() {
|
||||
let trust =
|
||||
validate_release_trust(&document("UNPROVISIONED_FAIL_CLOSED", "", "", "")).unwrap();
|
||||
assert!(matches!(trust, ValidatedReleaseTrust::Disabled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_https_shape_but_rejects_insecure_endpoints() {
|
||||
let owned_shape = document(
|
||||
"PROVISIONED",
|
||||
"\"https://releases.example.test/latest.json\"",
|
||||
"public",
|
||||
"\"releases.example.test\"",
|
||||
);
|
||||
assert!(
|
||||
validate_release_trust(&owned_shape).is_ok(),
|
||||
"endpoint ownership is additionally checked by release provisioning"
|
||||
);
|
||||
let insecure = document(
|
||||
"PROVISIONED",
|
||||
"\"http://releases.example.test/latest.json\"",
|
||||
"public",
|
||||
"\"releases.example.test\"",
|
||||
);
|
||||
assert!(validate_release_trust(&insecure).is_err());
|
||||
let mismatched_host = document(
|
||||
"PROVISIONED",
|
||||
"\"https://upstream.example.test/latest.json\"",
|
||||
"public",
|
||||
"\"releases.example.test\"",
|
||||
);
|
||||
assert!(validate_release_trust(&mismatched_host).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_automatic_or_nonconsensual_install_policy() {
|
||||
let automatic = document("UNPROVISIONED_FAIL_CLOSED", "", "", "")
|
||||
.replace("\"automaticDownload\":false", "\"automaticDownload\":true");
|
||||
assert!(validate_release_trust(&automatic).is_err());
|
||||
let no_opt_in = document("UNPROVISIONED_FAIL_CLOSED", "", "", "").replace(
|
||||
"\"humanOptInInstallRequired\":true",
|
||||
"\"humanOptInInstallRequired\":false",
|
||||
);
|
||||
assert!(validate_release_trust(&no_opt_in).is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue