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