1#![forbid(unsafe_code)]
2use serde::{Deserialize, Serialize};
4use sim_platform_core::{
5 ContractProvenance, EvidenceLevel, FactPort, OpenSymbol, PlatformCard, ServiceOffer,
6 stable_digest,
7};
8pub use sim_platform_core::{
9 UbuntuArchitecture as Architecture, UbuntuProfile as UbuntuPcProfile,
10 UbuntuProfileKind as ProfileKind,
11};
12use std::{ffi::OsString, path::PathBuf};
13
14pub use sim_platform_linux as linux;
15mod compute;
16mod loader;
17pub use compute::UbuntuComputeProbe;
18pub use loader::UbuntuLoaderPort;
19mod process;
20mod sandbox;
21pub use process::UbuntuProcess;
22pub use sandbox::BwrapLauncher;
23
24#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct UbuntuProcessEnvelope {
30 pub argv: Vec<OsString>,
32 pub work_root: PathBuf,
34 pub cache_root: Option<PathBuf>,
36 pub registry_endpoint: Option<String>,
38 pub allow_insecure_registry: bool,
40}
41
42impl UbuntuProcessEnvelope {
43 pub fn capture() -> std::io::Result<Self> {
48 Ok(Self {
49 argv: std::env::args_os().collect(),
50 work_root: std::env::current_dir()?,
51 cache_root: std::env::var_os("SIM_CLI_CACHE_DIR").map(PathBuf::from),
52 registry_endpoint: std::env::var_os("SIM_GIT_REGISTRY_ENDPOINT")
53 .map(|value| value.to_string_lossy().into_owned()),
54 allow_insecure_registry: std::env::var_os("SIM_GIT_REGISTRY_ALLOW_INSECURE")
55 .is_some_and(|value| !value.is_empty()),
56 })
57 }
58}
59pub const BUNDLE_DESCRIPTOR: &str = "sim.platform-bundle.toml";
60pub const UBUNTU_BUNDLE_MANIFEST: &str = r#"
62schema = "sim.platform-bundle/v1"
63capsule = "platform/site/ubuntu-pc"
64artifact = "sim-platform-ubuntu-pc.so"
65loader = "loader/native-v1"
66artifact_content = "sha256:ubuntu-pc-release-content"
67entry = "sim_native_abi_v1"
68"#;
69
70pub const UBUNTU_CAPSULE_MANIFEST: &str = r#"
72schema = "sim.platform-capsule/v1"
73provider = "platform/site/ubuntu-pc"
74services = ["platform/monotonic", "platform/wall-clock", "platform/entropy"]
75shells = []
76loader_kinds = ["loader/native-v1", "loader/wasm-v1", "loader/source-v1", "loader/static-v1"]
77"#;
78#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
79pub struct RegisteredCard {
80 pub profile: UbuntuPcProfile,
81 pub card: PlatformCard,
82}
83
84const FACTS: &[(&str, FactPort)] = &[
85 ("wall-clock", FactPort::WallClock),
86 ("monotonic-clock", FactPort::MonotonicClock),
87 ("timer", FactPort::Timer),
88 ("entropy", FactPort::Entropy),
89 ("locale", FactPort::Locale),
90 ("timezone", FactPort::Timezone),
91 ("pressure", FactPort::LifecyclePressure),
92 ("limits", FactPort::MachineLimits),
93];
94const DESKTOP: &[&str] = &[
95 "open",
96 "share",
97 "notify",
98 "clipboard",
99 "permission",
100 "keep-awake",
101 "activation",
102];
103#[must_use]
108pub fn register(profile: UbuntuPcProfile) -> RegisteredCard {
109 let kind = profile.kind;
110 let mut services = FACTS
111 .iter()
112 .map(|(name, port)| ServiceOffer {
113 service: OpenSymbol(format!("platform/{name}")),
114 port: *port,
115 evidence: EvidenceLevel::Attested,
116 })
117 .collect::<Vec<_>>();
118 services.extend(
119 ["xdg-config", "xdg-cache", "xdg-data", "xdg-state", "temp"]
120 .into_iter()
121 .map(|name| ServiceOffer {
122 service: OpenSymbol(format!("platform/mount/{name}")),
123 port: FactPort::MachineLimits,
124 evidence: EvidenceLevel::Attested,
125 }),
126 );
127 if kind == ProfileKind::Desktop {
128 services.extend(DESKTOP.iter().map(|name| ServiceOffer {
129 service: OpenSymbol(format!("platform/{name}")),
130 port: FactPort::LifecyclePressure,
131 evidence: EvidenceLevel::Attested,
132 }));
133 }
134 let profile_bytes = serde_json::to_vec(&profile).expect("closed profile serializes");
135 RegisteredCard {
136 profile,
137 card: PlatformCard {
138 schema: OpenSymbol("platform/card-v1".into()),
139 site: OpenSymbol(
140 match kind {
141 ProfileKind::Desktop => "platform/site/ubuntu-pc-desktop",
142 ProfileKind::Headless => "platform/site/ubuntu-pc-headless",
143 }
144 .into(),
145 ),
146 services,
147 provenance: ContractProvenance {
148 contract: OpenSymbol("contract/ubuntu-pc-v1".into()),
149 content_digest: stable_digest(&profile_bytes),
150 issuer: OpenSymbol("issuer/sim-platform".into()),
151 },
152 },
153 }
154}
155
156#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
157pub struct PhysicalAttestation {
158 pub schema: String,
159 pub provider: String,
160 pub registered_capability: String,
161 pub source_content: String,
162 pub artifact_content: String,
163 pub card_content: String,
164 pub result_content: String,
165 pub source: String,
166 pub artifact: String,
167 pub card: String,
168 pub result: String,
169 pub checks: Vec<String>,
170}
171impl PhysicalAttestation {
172 pub fn validate(&self) -> Result<(), &'static str> {
177 if self.schema != "sim.platform-physical-attestation/v1"
178 || self.provider != "ubuntu-pc"
179 || self.registered_capability != "linux-x86_64"
180 {
181 return Err("wrong attestation identity");
182 }
183 for (claimed, payload) in [
184 (&self.source_content, &self.source),
185 (&self.artifact_content, &self.artifact),
186 (&self.card_content, &self.card),
187 (&self.result_content, &self.result),
188 ] {
189 if *claimed != stable_digest(payload.as_bytes()) {
190 return Err("content identity mismatch");
191 }
192 }
193 if self.checks.len() < 4
194 || self
195 .checks
196 .iter()
197 .any(|v| v.contains('/') || v.contains('@'))
198 {
199 return Err("unsanitized or incomplete evidence");
200 }
201 Ok(())
202 }
203}