Skip to main content

sim_platform_ubuntu_pc/
lib.rs

1#![forbid(unsafe_code)]
2//! Ubuntu PC capsule registration and sanitized evidence contracts.
3use 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/// Owned process-entry facts captured by the Ubuntu capsule.
27///
28/// Portable bootloaders consume this value and never inspect argv, the current
29/// directory, or environment variables themselves.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct UbuntuProcessEnvelope {
32    /// Complete process argument vector.
33    pub argv: Vec<OsString>,
34    /// Explicit working root supplied to configuration and cache policy.
35    pub work_root: PathBuf,
36    /// Explicit cache root override, when supplied by the host.
37    pub cache_root: Option<PathBuf>,
38    /// Explicit registry artifact endpoint, when supplied by the host.
39    pub registry_endpoint: Option<String>,
40    /// Whether the host admits unauthenticated non-loopback registry access.
41    pub allow_insecure_registry: bool,
42}
43
44impl UbuntuProcessEnvelope {
45    /// Captures the bounded process facts owned by this concrete capsule.
46    ///
47    /// # Errors
48    /// Returns an error when the host does not expose a current working root.
49    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";
62/// Ubuntu PC distribution bundle. The one row admits only this capsule.
63pub 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
72/// Ubuntu PC capsule card paired with [`UBUNTU_BUNDLE_MANIFEST`].
73pub 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/// Construct the exact Card for a supported Ubuntu profile.
106///
107/// # Panics
108/// Panics only if serialization of the closed profile enum fails.
109#[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    /// Verify the sanitized, content-bound offline evidence envelope.
175    ///
176    /// # Errors
177    /// Refuses wrong identity, missing content identities, or leaking checks.
178    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}