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
5415
product-source/hololake-native-desktop/src-tauri/Cargo.lock
generated
Normal file
5415
product-source/hololake-native-desktop/src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
product-source/hololake-native-desktop/src-tauri/Cargo.toml
Normal file
26
product-source/hololake-native-desktop/src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "hololake-native-desktop"
|
||||
version = "0.1.0"
|
||||
description = "HoloLake native desktop foundation"
|
||||
authors = ["HoloLake"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[lib]
|
||||
name = "hololake_native_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "=2.10.2", features = [] }
|
||||
tauri-runtime = "=2.10.0"
|
||||
tauri-runtime-wry = "=2.10.0"
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Minimum HoloLake desktop foundation; updater actions are not exposed to the webview yet.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
BIN
product-source/hololake-native-desktop/src-tauri/icons/icon.png
Normal file
BIN
product-source/hololake-native-desktop/src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"schema": "hololake.release-trust/v1",
|
||||
"state": "UNPROVISIONED_FAIL_CLOSED",
|
||||
"endpoints": [],
|
||||
"publicKey": "",
|
||||
"allowedReleaseHosts": [],
|
||||
"automaticCheckOnStartup": false,
|
||||
"automaticDownload": false,
|
||||
"humanOptInInstallRequired": true,
|
||||
"automaticRestart": false
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "HoloLake",
|
||||
"version": "0.1.0",
|
||||
"identifier": "world.guanghu.hololake",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://127.0.0.1:5210",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "HoloLake",
|
||||
"width": 1180,
|
||||
"height": 760,
|
||||
"minWidth": 760,
|
||||
"minHeight": 520
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"createUpdaterArtifacts": true,
|
||||
"category": "Productivity"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue